Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DeepClone mapping control not generating second tier functions to clone for objects stored in collection-type content #3516

Open
ludgerb opened this issue Jan 29, 2024 · 3 comments
Labels
Milestone

Comments

@ludgerb
Copy link

ludgerb commented Jan 29, 2024

Expected behavior

In 1.5.3.Final DeepClone is not working if second thier objects are stored in collection type content.

Let's say, we have nested objects as follows:

Person -> List

@Data
@NoArgsConstructor
public class Lead {
    private List<Address> addresses;
}

@Data
@NoArgsConstructor
public class Address {
    private String formattedAddress;
}

Expectation is, when calling clonePerson on person, since it's a deepCopy, the addresses will also be cloned.
i.e. the following test should hold:

@Test
void testAddressClone() {
    var address = new Address();
    address.setFormattedAddress("original address");
    var person = new Person();
    person.setAddresses(List.of(address));

    // perform deep clone
    var clonedPerson = mapper.clonePerson(person);
    
    // modify original => should not reflect on the copy
    address.setFormattedAddress("new address");
    
   assertNotEquals(address.getFormattedAddress(), clonedPerson.getAddresses().get(0).getFormattedAddress());
}

Please compare this to the behavior in #3135

Actual behavior

Generated code is:

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2024-01-29T12:25:17+0100",
    comments = "version: 1.5.5.Final, compiler: javac, environment: Java 17.0.9"
)
@Component
public class LeadMapperImpl extends PersonMapper {
    @Override
    public Lead copyLead(Lead person) {
        if ( person == null ) {
            return null;
        }
        Person person1 = new Person();

        lead1.setAddresses( addressListToAddressList( lead.getAddresses() ) );
    }

    protected List<Address> addressListToAddressList(List<Address> list) {
        if ( list == null ) {
            return null;
        }

        List<Address> list1 = new ArrayList<Address>( list.size() );
        for ( Address address : list ) {
            list1.add( address );
        }

        return list1;
    }
}

I.e. an addressToAddress method is missing and not called in the for loop copying the list.

In contrast, when replacing the original Person by this definition, an addressToAddress method is generated (so here we have expected behavior):

@Data
@NoArgsConstructor
public class Lead {
    private Address address;
}

@Data
@NoArgsConstructor
public class Address {
    private String formattedAddress;
}

Steps to reproduce the problem

@Mapper(componentModel = "spring")
public abstract class PersonMapper {
  @BeanMapping(
      mappingControl = DeepClone.class,
      nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS)
    public abstract Person clonePerson(Person person);
}

MapStruct Version

1.5.5-Final

@ludgerb ludgerb added the bug label Jan 29, 2024
@filiphr filiphr added this to the 1.6.0.Beta2 milestone Feb 11, 2024
@filiphr
Copy link
Member

filiphr commented Feb 11, 2024

Thanks for the detailed example @ludgerb this does looks similar to #3135. We'll look into it and fix it.

@Zegveld
Copy link
Contributor

Zegveld commented Apr 28, 2024

Looked into it a bit, but not sure how to fix this.

Here is a work-around:

@Mapper( componentModel = "spring", mappingControl = DeepClone.class )
public abstract class PersonMapper {
  @BeanMapping( nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS )
    public abstract Person clonePerson(Person person);
}

The problem is that the IterableMappingOptions do not fall-back to the configuration for mappingControl in the BeanMappingOptions.
I am not certain if we want this even, and what the impact would be for other behavior.

If the mappingControl is defined on the @Mapper itself however it is inherited by both BeanMappingOptions and IterableMappingOptions.

@filiphr filiphr modified the milestones: 1.6.0.Beta2, 1.6.0.RC1 May 9, 2024
@filiphr
Copy link
Member

filiphr commented Jul 20, 2024

Thanks for looking into this @Zegveld. I also think that it would be a bit tricky to solve this. I'll move it to 1.7 to see if we can do something there.

In any case, the approach by using the mapping control in @Mapping is a good workaround for now.

@filiphr filiphr modified the milestones: 1.6.0.RC1, 1.7.0 Jul 20, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants