#1958: Add support for ignoring multiple target properties at once

This commit is contained in:
Aleksey Ivashin 2025-05-25 18:05:18 +03:00 committed by GitHub
parent 0badba7003
commit 6b6600c370
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 589 additions and 1 deletions

View File

@ -0,0 +1,67 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Configures the ignored of one bean attribute.
*
* <p>
* The name all attributes of for ignored is to be specified via {@link #targets()}.
* </p>
*
* <p>
* <strong>Example 1:</strong> Implicitly mapping fields with the same name:
* </p>
*
* <pre><code class='java'>
* // We need ignored Human.name and Human.lastName
* // we can use &#64;Ignored with parameters "name", "lastName" {@link #targets()}
* &#64;Mapper
* public interface HumanMapper {
* &#64;Ignored( targets = { "name", "lastName" } )
* HumanDto toHumanDto(Human human)
* }
* </code></pre>
* <pre><code class='java'>
* // generates:
* &#64;Override
* public HumanDto toHumanDto(Human human) {
* humanDto.setFullName( human.getFullName() );
* // ...
* }
* </code></pre>
*
* @author Ivashin Aleksey
*/
@Repeatable(IgnoredList.class)
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
public @interface Ignored {
/**
* Whether the specified properties should be ignored by the generated mapping method.
* This can be useful when certain attributes should not be propagated from source to target or when properties in
* the target object are populated using a decorator and thus would be reported as unmapped target property by
* default.
*
* @return The target names of the configured properties that should be ignored
*/
String[] targets();
/**
* The prefix that should be applied to all the properties specified via {@link #targets()}.
*
* @return The target prefix to be applied to the defined properties
*/
String prefix() default "";
}

View File

@ -0,0 +1,54 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Configures the ignored list for several bean attributes.
* <p>
* <strong>TIP: When using Java 8 or later, you can omit the {@code @IgnoredList}
* wrapper annotation and directly specify several {@code @Ignored} annotations on one method.</strong>
*
* <p>These two examples are equal.
* </p>
* <pre><code class='java'>
* // before Java 8
* &#64;Mapper
* public interface MyMapper {
* &#64;IgnoredList({
* &#64;Ignored(targets = { "firstProperty" } ),
* &#64;Ignored(targets = { "secondProperty" } )
* })
* HumanDto toHumanDto(Human human);
* }
* </code></pre>
* <pre><code class='java'>
* // Java 8 and later
* &#64;Mapper
* public interface MyMapper {
* &#64;Ignored(targets = { "firstProperty" } ),
* &#64;Ignored(targets = { "secondProperty" } )
* HumanDto toHumanDto(Human human);
* }
* </code></pre>
*
* @author Ivashin Aleksey
*/
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE })
public @interface IgnoredList {
/**
* The configuration of the bean attributes.
*
* @return The configuration of the bean attributes.
*/
Ignored[] value();
}

View File

@ -306,6 +306,9 @@ public @interface Mapping {
* This can be useful when certain attributes should not be propagated from source to target or when properties in
* the target object are populated using a decorator and thus would be reported as unmapped target property by
* default.
* <p>
* If you have multiple properties to ignore,
* you can use the {@link Ignored} annotation instead and group them all at once.
*
* @return {@code true} if the given property should be ignored, {@code false} otherwise
*/

View File

@ -280,6 +280,13 @@ This puts the configuration of the nested mapping into one place (method) where
instead of re-configuring the same things on all of those upper methods.
====
[TIP]
====
When ignoring multiple properties instead of defining multiple `@Mapping` annotations, you can use the `@Ignored` annotation to group them together.
e.g. for the `FishTankMapperWithDocument` example above, you could write:
`@Ignored(targets = { "plant", "ornament", "material" })`
====
[NOTE]
====
In some cases the `ReportingPolicy` that is going to be used for the generated nested method would be `IGNORE`.

View File

@ -26,6 +26,8 @@ import org.mapstruct.MapMapping;
import org.mapstruct.Mapper;
import org.mapstruct.MapperConfig;
import org.mapstruct.Mapping;
import org.mapstruct.Ignored;
import org.mapstruct.IgnoredList;
import org.mapstruct.MappingTarget;
import org.mapstruct.Mappings;
import org.mapstruct.Named;
@ -53,6 +55,8 @@ import org.mapstruct.tools.gem.GemDefinition;
@GemDefinition(AnnotateWiths.class)
@GemDefinition(Mapper.class)
@GemDefinition(Mapping.class)
@GemDefinition(Ignored.class)
@GemDefinition(IgnoredList.class)
@GemDefinition(Mappings.class)
@GemDefinition(IterableMapping.class)
@GemDefinition(BeanMapping.class)

View File

@ -21,6 +21,8 @@ import javax.lang.model.type.TypeKind;
import org.mapstruct.ap.internal.gem.BeanMappingGem;
import org.mapstruct.ap.internal.gem.ConditionGem;
import org.mapstruct.ap.internal.gem.IgnoredGem;
import org.mapstruct.ap.internal.gem.IgnoredListGem;
import org.mapstruct.ap.internal.gem.IterableMappingGem;
import org.mapstruct.ap.internal.gem.MapMappingGem;
import org.mapstruct.ap.internal.gem.MappingGem;
@ -76,6 +78,8 @@ public class MethodRetrievalProcessor implements ModelElementProcessor<Void, Lis
private static final String VALUE_MAPPING_FQN = "org.mapstruct.ValueMapping";
private static final String VALUE_MAPPINGS_FQN = "org.mapstruct.ValueMappings";
private static final String CONDITION_FQN = "org.mapstruct.Condition";
private static final String IGNORED_FQN = "org.mapstruct.Ignored";
private static final String IGNORED_LIST_FQN = "org.mapstruct.IgnoredList";
private FormattingMessager messager;
private TypeFactory typeFactory;
private AccessorNamingUtils accessorNaming;
@ -624,7 +628,11 @@ public class MethodRetrievalProcessor implements ModelElementProcessor<Void, Lis
* @return The mappings for the given method, keyed by target property name
*/
private Set<MappingOptions> getMappings(ExecutableElement method, BeanMappingOptions beanMapping) {
return new RepeatableMappings( beanMapping ).getProcessedAnnotations( method );
Set<MappingOptions> processedAnnotations = new RepeatableMappings( beanMapping )
.getProcessedAnnotations( method );
processedAnnotations.addAll( new IgnoredConditions( processedAnnotations )
.getProcessedAnnotations( method ) );
return processedAnnotations;
}
/**
@ -823,4 +831,55 @@ public class MethodRetrievalProcessor implements ModelElementProcessor<Void, Lis
}
}
}
private class IgnoredConditions extends RepeatableAnnotations<IgnoredGem, IgnoredListGem, MappingOptions> {
protected final Set<MappingOptions> processedAnnotations;
protected IgnoredConditions( Set<MappingOptions> processedAnnotations ) {
super( elementUtils, IGNORED_FQN, IGNORED_LIST_FQN );
this.processedAnnotations = processedAnnotations;
}
@Override
protected IgnoredGem singularInstanceOn(Element element) {
return IgnoredGem.instanceOn( element );
}
@Override
protected IgnoredListGem multipleInstanceOn(Element element) {
return IgnoredListGem.instanceOn( element );
}
@Override
protected void addInstance(IgnoredGem gem, Element method, Set<MappingOptions> mappings) {
IgnoredGem ignoredGem = IgnoredGem.instanceOn( method );
if ( ignoredGem == null ) {
ignoredGem = gem;
}
String prefix = ignoredGem.prefix().get();
for ( String target : ignoredGem.targets().get() ) {
String realTarget = target;
if ( !prefix.isEmpty() ) {
realTarget = prefix + "." + target;
}
MappingOptions mappingOptions = MappingOptions.forIgnore( realTarget );
if ( processedAnnotations.contains( mappingOptions ) || mappings.contains( mappingOptions ) ) {
messager.printMessage( method, Message.PROPERTYMAPPING_DUPLICATE_TARGETS, realTarget );
}
else {
mappings.add( mappingOptions );
}
}
}
@Override
protected void addInstances(IgnoredListGem gem, Element method, Set<MappingOptions> mappings) {
IgnoredListGem ignoredListGem = IgnoredListGem.instanceOn( method );
for ( IgnoredGem ignoredGem : ignoredListGem.value().get() ) {
addInstance( ignoredGem, method, mappings );
}
}
}
}

View File

@ -0,0 +1,64 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.ap.test.ignored;
public class Animal {
//CHECKSTYLE:OFF
public Integer publicAge;
public String publicColour;
//CHECKSTYLE:OFN
private String colour;
private String name;
private int size;
private Integer age;
// private String colour;
public Animal() {
}
public Animal(String name, int size, Integer age, String colour) {
this.name = name;
this.size = size;
this.publicAge = age;
this.age = age;
this.publicColour = colour;
this.colour = colour;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getSize() {
return size;
}
public void setSize(int size) {
this.size = size;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getColour() {
return colour;
}
public void setColour( String colour ) {
this.colour = colour;
}
}

View File

@ -0,0 +1,63 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.ap.test.ignored;
public class AnimalDto {
//CHECKSTYLE:OFF
public Integer publicAge;
public String publicColor;
//CHECKSTYLE:ON
private String name;
private Integer size;
private Integer age;
private String color;
public AnimalDto() {
}
public AnimalDto(String name, Integer size, Integer age, String color) {
this.name = name;
this.size = size;
this.publicAge = age;
this.age = age;
this.publicColor = color;
this.color = color;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getSize() {
return size;
}
public void setSize(Integer size) {
this.size = size;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
}

View File

@ -0,0 +1,20 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.ap.test.ignored;
import org.mapstruct.Ignored;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper
public interface AnimalMapper {
AnimalMapper INSTANCE = Mappers.getMapper( AnimalMapper.class );
@Ignored( targets = { "publicAge", "age", "publicColor", "color" } )
AnimalDto animalToDto( Animal animal );
}

View File

@ -0,0 +1,26 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.ap.test.ignored;
import org.mapstruct.Ignored;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
@Mapper
public interface ErroneousMapper {
ErroneousMapper INSTANCE = Mappers.getMapper( ErroneousMapper.class );
@Mapping(target = "name", ignore = true)
@Ignored(targets = { "name", "color", "publicColor" })
AnimalDto ignoredAndMappingAnimalToDto( Animal animal );
@Mapping(target = "publicColor", source = "publicColour")
@Ignored(targets = { "publicColor", "color" })
AnimalDto ignoredAndMappingAnimalToDtoMap( Animal animal );
}

View File

@ -0,0 +1,102 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.ap.test.ignored;
import org.mapstruct.ap.testutil.IssueKey;
import org.mapstruct.ap.testutil.ProcessorTest;
import org.mapstruct.ap.testutil.WithClasses;
import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult;
import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic;
import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Test for ignoring properties during the mapping.
*
* @author Ivashin Aleksey
*/
@WithClasses({ Animal.class, AnimalDto.class, Zoo.class, ZooDto.class, ZooMapper.class})
public class IgnoredPropertyTest {
@ProcessorTest
@IssueKey("1958")
@WithClasses( { AnimalMapper.class } )
public void shouldNotPropagateIgnoredPropertyGivenViaTargetAttribute() {
Animal animal = new Animal( "Bruno", 100, 23, "black" );
AnimalDto animalDto = AnimalMapper.INSTANCE.animalToDto( animal );
assertThat( animalDto ).isNotNull();
assertThat( animalDto.getName() ).isEqualTo( "Bruno" );
assertThat( animalDto.getSize() ).isEqualTo( 100 );
assertThat( animalDto.getAge() ).isNull();
assertThat( animalDto.publicAge ).isNull();
assertThat( animalDto.getColor() ).isNull();
assertThat( animalDto.publicColor ).isNull();
}
@ProcessorTest
@IssueKey("1958")
@WithClasses( { ErroneousMapper.class } )
@ExpectedCompilationOutcome(
value = CompilationResult.FAILED,
diagnostics = {
@Diagnostic(type = ErroneousMapper.class,
kind = javax.tools.Diagnostic.Kind.ERROR,
line = 20,
message = "Target property \"name\" must not be mapped more than once." ),
@Diagnostic(type = ErroneousMapper.class,
kind = javax.tools.Diagnostic.Kind.ERROR,
line = 24,
message = "Target property \"publicColor\" must not be mapped more than once." )
}
)
public void shouldFailToGenerateMappings() {
}
@ProcessorTest
@IssueKey("1958")
@WithClasses( { AnimalMapper.class } )
public void shouldNotPropagateIgnoredInnerPropertyGivenViaTargetAttribute() {
Animal animal = new Animal( "Bruno", 100, 23, "black" );
Zoo zoo = new Zoo(animal, "Test name", "test address");
ZooDto zooDto = ZooMapper.INSTANCE.zooToDto( zoo );
assertThat( zooDto ).isNotNull();
assertThat( zooDto.getName() ).isEqualTo( "Test name" );
assertThat( zooDto.getAddress() ).isEqualTo( "test address" );
assertThat( zooDto.getAnimal() ).isNotNull();
assertThat( zooDto.getAnimal().getName() ).isEqualTo( "Bruno" );
assertThat( zooDto.getAnimal().getAge() ).isNull();
assertThat( zooDto.getAnimal().publicAge ).isNull();
assertThat( zooDto.getAnimal().getColor() ).isNull();
assertThat( zooDto.getAnimal().publicColor ).isNull();
assertThat( zooDto.getAnimal().getSize() ).isNull();
}
@ProcessorTest
@IssueKey("1958")
@WithClasses( { AnimalMapper.class } )
public void shouldNotPropagateIgnoredInnerPropertyGivenViaTargetAttribute2() {
Animal animal = new Animal( "Bruno", 100, 23, "black" );
Zoo zoo = new Zoo(animal, "Test name", "test address");
ZooDto zooDto = ZooMapper.INSTANCE.zooToDto2( zoo );
assertThat( zooDto ).isNotNull();
assertThat( zooDto.getName() ).isEqualTo( "Test name" );
assertThat( zooDto.getAddress() ).isNull();
assertThat( zooDto.getAnimal() ).isNotNull();
assertThat( zooDto.getAnimal().getName() ).isEqualTo( "Bruno" );
assertThat( zooDto.getAnimal().getAge() ).isNull();
assertThat( zooDto.getAnimal().publicAge ).isNull();
assertThat( zooDto.getAnimal().getColor() ).isNull();
assertThat( zooDto.getAnimal().publicColor ).isNull();
assertThat( zooDto.getAnimal().getSize() ).isNull();
}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.ap.test.ignored;
public class Zoo {
private Animal animal;
private String name;
private String address;
public Zoo() {
}
public Zoo(Animal animal, String name, String address ) {
this.animal = animal;
this.name = name;
this.address = address;
}
public Animal getAnimal() {
return animal;
}
public void setAnimal(Animal animal) {
this.animal = animal;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.ap.test.ignored;
public class ZooDto {
private AnimalDto animal;
private String name;
private String address;
public ZooDto() {
}
public ZooDto(AnimalDto animal, String name, String address) {
this.animal = animal;
this.name = name;
this.address = address;
}
public AnimalDto getAnimal() {
return animal;
}
public void setAnimal(AnimalDto animal) {
this.animal = animal;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}

View File

@ -0,0 +1,23 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.ap.test.ignored;
import org.mapstruct.Ignored;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper
public interface ZooMapper {
ZooMapper INSTANCE = Mappers.getMapper( ZooMapper.class );
@Ignored( prefix = "animal", targets = { "publicAge", "size", "publicColor", "age", "color" } )
ZooDto zooToDto( Zoo zoo );
@Ignored( targets = { "address" } )
@Ignored( prefix = "animal", targets = { "publicAge", "size", "publicColor", "age", "color" } )
ZooDto zooToDto2( Zoo zoo );
}