diff --git a/core/src/main/java/org/mapstruct/MappingSource.java b/core/src/main/java/org/mapstruct/MappingSource.java new file mode 100644 index 0000000000..aaeb88349a --- /dev/null +++ b/core/src/main/java/org/mapstruct/MappingSource.java @@ -0,0 +1,91 @@ +/* + * 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; + +/** + * Marks a parameter as a source parameter, providing fine-grained control over mapping behavior. + * This annotation controls how parameters are handled during the mapping process. + *

+ * Key features: + *

+ *

+ * Standard mapping behavior (without this annotation): + *

+ * + * @since 1.6.0 + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.CLASS) +public @interface MappingSource { + + /** + * Controls whether this parameter participates in implicit mapping. + * The effect depends on the parameter type: + *

+ * Bean/Collection parameters: + *

+ *

+ * Map parameters: + *

+ *

+ * Note: This setting only affects implicit mapping. Explicit mappings defined with + * {@literal @}Mapping annotations are always processed regardless of this setting. + * + * @return whether implicit mapping should be enabled for this parameter + */ + boolean implicitMapping() default true; + + /** + * Marks this parameter as primary in multi-source mapping scenarios. + *

+ * When multiple source parameters contain properties with the same name that could be mapped to + * a target property, MapStruct normally reports an error due to the ambiguity. When a parameter + * is marked as primary: + *

+ *

+ *

+ * Note: This setting affects both implicit and explicit mappings when resolving conflicts. + * Explicit {@literal @}Mapping annotations always take precedence over primary parameter selection. + * + * @return whether this parameter should be considered primary when resolving conflicts + * @since 1.7.0 + */ + boolean primary() default false; +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/gem/GemGenerator.java b/processor/src/main/java/org/mapstruct/ap/internal/gem/GemGenerator.java index a8b62babe9..60dfbf15e7 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/gem/GemGenerator.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/gem/GemGenerator.java @@ -18,6 +18,8 @@ import org.mapstruct.Context; import org.mapstruct.DecoratedWith; import org.mapstruct.EnumMapping; +import org.mapstruct.Ignored; +import org.mapstruct.IgnoredList; import org.mapstruct.InheritConfiguration; import org.mapstruct.InheritInverseConfiguration; import org.mapstruct.IterableMapping; @@ -26,8 +28,7 @@ import org.mapstruct.Mapper; import org.mapstruct.MapperConfig; import org.mapstruct.Mapping; -import org.mapstruct.Ignored; -import org.mapstruct.IgnoredList; +import org.mapstruct.MappingSource; import org.mapstruct.MappingTarget; import org.mapstruct.Mappings; import org.mapstruct.Named; @@ -68,6 +69,7 @@ @GemDefinition(TargetType.class) @GemDefinition(TargetPropertyName.class) @GemDefinition(MappingTarget.class) +@GemDefinition(MappingSource.class) @GemDefinition(DecoratedWith.class) @GemDefinition(MapperConfig.class) @GemDefinition(InheritConfiguration.class) diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java index b5208d302b..d4cc61f089 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java @@ -1420,31 +1420,15 @@ else if ( mapping.getJavaExpression() != null ) { // When we implicitly map we first do property name based mapping // i.e. look for matching properties in the source types // and then do parameter name based mapping - for ( Parameter sourceParameter : method.getSourceParameters() ) { - SourceReference matchingSourceRef = getSourceRefByTargetName( - sourceParameter, - targetPropertyName - ); - if ( matchingSourceRef != null ) { - if ( sourceRef != null ) { - errorOccured = true; - // This can only happen when the target property matches multiple properties - // within the different source parameters - ctx.getMessager() - .printMessage( - method.getExecutable(), - mappingRef.getMapping().getMirror(), - Message.BEANMAPPING_SEVERAL_POSSIBLE_SOURCES, - targetPropertyName - ); - break; - } - // We can't break here since it is possible that the same property exists in multiple - // source parameters - sourceRef = matchingSourceRef; - } + SourceReferenceResult matchingSourceRefResult = findSourceReferenceForTargetProperty( + method.getSourceParameters(), + targetPropertyName, + mappingRef.getMapping().getMirror() + ); + sourceRef = matchingSourceRefResult.getSourceReference(); + if (matchingSourceRefResult.isErrorOccurred() ) { + errorOccured = true; } - } if ( sourceRef == null ) { @@ -1568,34 +1552,103 @@ private void applyTargetThisMapping() { } /** - * Iterates over all target properties and all source parameters. + * Iterates over all target properties and all source parameters to find property name matches. *

- * When a property name match occurs, the remainder will be checked for duplicates. Matches will be removed from - * the set of remaining target properties. + * For each target property, the method attempts to find a matching source property + * using {@link #findSourceReferenceForTargetProperty}. + *

+ * When a match is found, it's added to the list of source references for further processing. + * Primary parameters take precedence when multiple source parameters have properties with the same name. */ private void applyPropertyNameBasedMapping() { List sourceReferences = new ArrayList<>(); + for ( String targetPropertyName : unprocessedTargetProperties.keySet() ) { - for ( Parameter sourceParameter : method.getSourceParameters() ) { - SourceReference sourceRef = getSourceRefByTargetName( sourceParameter, targetPropertyName ); + SourceReferenceResult matchingSourceRefResult = findSourceReferenceForTargetProperty( + method.getSourceParameters(), + targetPropertyName, + null + ); + if ( matchingSourceRefResult.getSourceReference() != null ) { + sourceReferences.add( matchingSourceRefResult.getSourceReference() ); + } + } + applyPropertyNameBasedMapping( sourceReferences ); + } + + /** + * Finds a source reference for a target property name, handling potential conflicts. + *

+ * This method iterates through source parameters to find properties matching the target property name, + * applying the following rules: + *

+ * + * @param sourceParameters the source parameters to search through + * @param targetPropertyName the target property name to match + * @param positionHint annotation mirror used for error reporting position, can be null + * @return a SourceReferenceResult containing the selected source reference and error status + */ + private SourceReferenceResult findSourceReferenceForTargetProperty(List sourceParameters, + String targetPropertyName, + AnnotationMirror positionHint) { + List sortedSourceParameters = + sourceParameters + .stream() + .sorted( Comparator.comparing( Parameter::isPrimary ).reversed() ) + .collect( Collectors.toList() ); + + SourceReference sourceRef = null; + boolean errorOccurred = false; + for ( Parameter sourceParameter : sortedSourceParameters ) { + SourceReference matchingSourceRef = getSourceRefByTargetName( sourceParameter, targetPropertyName ); + if ( matchingSourceRef != null ) { if ( sourceRef != null ) { - sourceReferences.add( sourceRef ); + if ( sourceRef.getParameter().isPrimary() == matchingSourceRef.getParameter().isPrimary() ) { + // Conflict detected - both parameters have the same primary status + // Either: + // 1. Both parameters are marked with @MappingSource(primary = true) + // 2. Neither parameter has primary status + errorOccurred = true; + ctx.getMessager() + .printMessage( + method.getExecutable(), + positionHint, + Message.BEANMAPPING_SEVERAL_POSSIBLE_SOURCES, + targetPropertyName + ); + } + break; } + // We can't break here since it is possible that the same property exists in multiple + // source parameters + sourceRef = matchingSourceRef; } } - applyPropertyNameBasedMapping( sourceReferences ); + return new SourceReferenceResult( sourceRef, errorOccurred ); } /** - * Iterates over all target properties and all source parameters. + * Processes a list of source references to create property mappings. + *

+ * Each source reference is used to create a property mapping for its target property. + * The referenced target property is removed from the set of unprocessed properties. *

- * When a property name match occurs, the remainder will be checked for duplicates. Matches will be removed from - * the set of remaining target properties. + * Note: This method assumes that conflicts between multiple source references for the same target property + * have already been resolved by {@link #findSourceReferenceForTargetProperty}. + * + * @param sourceReferences the list of source references to process */ private void applyPropertyNameBasedMapping(List sourceReferences) { - for ( SourceReference sourceRef : sourceReferences ) { - String targetPropertyName = sourceRef.getDeepestPropertyName(); Accessor targetPropertyWriteAccessor = unprocessedTargetProperties.remove( targetPropertyName ); unprocessedConstructorProperties.remove( targetPropertyName ); @@ -1696,12 +1749,16 @@ private SourceReference getSourceRefByTargetName(Parameter sourceParameter, Stri SourceReference sourceRef = null; - if ( sourceParameter.getType().isPrimitive() || sourceParameter.getType().isArrayType() ) { - return sourceRef; + if ( ( sourceParameter.isMappingSource() && !sourceParameter.isImplicitMapping() ) + || sourceParameter.getType().isPrimitive() + || sourceParameter.getType().isArrayType() ) { + return null; } + boolean allowedMapToBean = + method.getSourceParameters().size() == 1 || sourceParameter.isImplicitMapping(); ReadAccessor sourceReadAccessor = sourceParameter.getType() - .getReadAccessor( targetPropertyName, method.getSourceParameters().size() == 1 ); + .getReadAccessor( targetPropertyName, allowedMapToBean ); if ( sourceReadAccessor != null ) { // property mapping PresenceCheckAccessor sourcePresenceChecker = @@ -1926,6 +1983,24 @@ private void reportErrorForUnusedSourceParameters() { } } + private static class SourceReferenceResult { + private final SourceReference sourceReference; + private final boolean errorOccurred; + + private SourceReferenceResult(SourceReference sourceReference, boolean errorOccurred) { + this.sourceReference = sourceReference; + this.errorOccurred = errorOccurred; + } + + SourceReference getSourceReference() { + return sourceReference; + } + + boolean isErrorOccurred() { + return errorOccurred; + } + } + private static class ConstructorAccessor { private final boolean hasError; private final List parameterBindings; diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/common/Parameter.java b/processor/src/main/java/org/mapstruct/ap/internal/model/common/Parameter.java index aaab7f46ca..184764d177 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/common/Parameter.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/common/Parameter.java @@ -13,6 +13,7 @@ import javax.lang.model.element.VariableElement; import org.mapstruct.ap.internal.gem.ContextGem; +import org.mapstruct.ap.internal.gem.MappingSourceGem; import org.mapstruct.ap.internal.gem.MappingTargetGem; import org.mapstruct.ap.internal.gem.SourcePropertyNameGem; import org.mapstruct.ap.internal.gem.TargetPropertyNameGem; @@ -35,6 +36,9 @@ public class Parameter extends ModelElement { private final boolean mappingContext; private final boolean sourcePropertyName; private final boolean targetPropertyName; + private final boolean mappingSource; + private final boolean implicitMapping; + private final boolean primary; private final boolean varArgs; @@ -48,6 +52,13 @@ private Parameter(Element element, Type type, boolean varArgs) { this.mappingContext = ContextGem.instanceOn( element ) != null; this.sourcePropertyName = SourcePropertyNameGem.instanceOn( element ) != null; this.targetPropertyName = TargetPropertyNameGem.instanceOn( element ) != null; + + // Handle MappingSource annotation + MappingSourceGem mappingSourceGem = MappingSourceGem.instanceOn( element ); + this.mappingSource = mappingSourceGem != null; + this.implicitMapping = this.mappingSource && mappingSourceGem.implicitMapping().get(); + this.primary = this.mappingSource && mappingSourceGem.primary().get(); + this.varArgs = varArgs; } @@ -64,6 +75,9 @@ private Parameter(String name, Type type, boolean mappingTarget, boolean targetT this.sourcePropertyName = sourcePropertyName; this.targetPropertyName = targetPropertyName; this.varArgs = varArgs; + this.mappingSource = false; + this.implicitMapping = false; + this.primary = false; } public Parameter(String name, Type type) { @@ -105,7 +119,8 @@ private String format() { + ( mappingContext ? "@Context " : "" ) + ( sourcePropertyName ? "@SourcePropertyName " : "" ) + ( targetPropertyName ? "@TargetPropertyName " : "" ) - + "%s " + name; + + ( mappingSource ? "@MappingSource " : "" ) + + "%s " + name; } @Override @@ -129,6 +144,18 @@ public boolean isSourcePropertyName() { return sourcePropertyName; } + public boolean isMappingSource() { + return mappingSource; + } + + public boolean isImplicitMapping() { + return implicitMapping; + } + + public boolean isPrimary() { + return primary; + } + public boolean isVarArgs() { return varArgs; } @@ -136,9 +163,9 @@ public boolean isVarArgs() { public boolean isSourceParameter() { return !isMappingTarget() && !isTargetType() && - !isMappingContext() && !isSourcePropertyName() && - !isTargetPropertyName(); + !isTargetPropertyName() && + ( !isMappingContext() || isMappingSource() ); } @Override diff --git a/processor/src/test/java/org/mapstruct/ap/test/mappingsource/context/MappingSourceWithContextMapper.java b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/context/MappingSourceWithContextMapper.java new file mode 100644 index 0000000000..f2bd5da64d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/context/MappingSourceWithContextMapper.java @@ -0,0 +1,111 @@ +/* + * 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.mappingsource.context; + +import java.util.List; +import java.util.stream.Collectors; + +import org.mapstruct.Context; +import org.mapstruct.Mapper; +import org.mapstruct.MappingSource; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface MappingSourceWithContextMapper { + + MappingSourceWithContextMapper INSTANCE = Mappers.getMapper( MappingSourceWithContextMapper.class ); + + default List toDto(Model model) { + return model + .getItems() + .stream() + .map( item -> this.toDto( item, model.getModelId() ) ) + .collect( Collectors.toList() ); + } + + ItemDTO toDto(Item item, @MappingSource @Context String modelId); + + class Model { + private List items; + private String modelId; + + public Model(String modelId) { + this.modelId = modelId; + } + + public List getItems() { + return items; + } + + public void setItems(List items) { + this.items = items; + } + + public String getModelId() { + return modelId; + } + + public void setModelId(String modelId) { + this.modelId = modelId; + } + } + + class Item { + private String itemId; + private String name; + + public Item(String itemId, String name) { + this.itemId = itemId; + this.name = name; + } + + public String getItemId() { + return itemId; + } + + public void setItemId(String itemId) { + this.itemId = itemId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + class ItemDTO { + private String itemId; + private String name; + private String modelId; // This comes from @Context parameter + + public String getItemId() { + return itemId; + } + + public void setItemId(String itemId) { + this.itemId = itemId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getModelId() { + return modelId; + } + + public void setModelId(String modelId) { + this.modelId = modelId; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/mappingsource/context/MappingSourceWithContextTest.java b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/context/MappingSourceWithContextTest.java new file mode 100644 index 0000000000..c585b84e86 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/context/MappingSourceWithContextTest.java @@ -0,0 +1,49 @@ +/* + * 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.mappingsource.context; + +import java.util.Arrays; +import java.util.List; + +import org.mapstruct.ap.test.mappingsource.context.MappingSourceWithContextMapper.Item; +import org.mapstruct.ap.test.mappingsource.context.MappingSourceWithContextMapper.ItemDTO; +import org.mapstruct.ap.test.mappingsource.context.MappingSourceWithContextMapper.Model; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +@IssueKey("3665") +@WithClasses(MappingSourceWithContextMapper.class) +public class MappingSourceWithContextTest { + + @ProcessorTest + @WithClasses(MappingSourceWithContextMapper.class) + public void testMappingSourceWithContext() { + Model model = new Model( "MODEL-123" ); + List items = Arrays.asList( + new Item( "ITEM-1", "First Item" ), + new Item( "ITEM-2", "Second Item" ) + ); + model.setItems( items ); + + List result = MappingSourceWithContextMapper.INSTANCE.toDto( model ); + + assertThat( result ).isNotNull(); + assertThat( result ).hasSize( 2 ); + + ItemDTO dto1 = result.getFirst(); + assertThat( dto1.getItemId() ).isEqualTo( "ITEM-1" ); + assertThat( dto1.getName() ).isEqualTo( "First Item" ); + assertThat( dto1.getModelId() ).isEqualTo( "MODEL-123" ); + + ItemDTO dto2 = result.get( 1 ); + assertThat( dto2.getItemId() ).isEqualTo( "ITEM-2" ); + assertThat( dto2.getName() ).isEqualTo( "Second Item" ); + assertThat( dto2.getModelId() ).isEqualTo( "MODEL-123" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/mappingsource/implicitmapping/MappingSourceImplicitMappingMapper.java b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/implicitmapping/MappingSourceImplicitMappingMapper.java new file mode 100644 index 0000000000..288a984766 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/implicitmapping/MappingSourceImplicitMappingMapper.java @@ -0,0 +1,101 @@ +/* + * 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.mappingsource.implicitmapping; + +import java.util.Map; + +import org.mapstruct.Mapper; +import org.mapstruct.MappingSource; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.factory.Mappers; + +@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface MappingSourceImplicitMappingMapper { + + MappingSourceImplicitMappingMapper INSTANCE = Mappers.getMapper( MappingSourceImplicitMappingMapper.class ); + + MapTarget multiSourceWithImplicitMap(@MappingSource Map mapSource, OtherSource otherSource); + + BeanTarget multiSourceWithImplicitBean(@MappingSource(implicitMapping = false) BeanSource beanSource, + OtherSource otherSource); + + MapTarget singleWithImplicitMap(@MappingSource(implicitMapping = false) Map map); + + class MapTarget { + private String mapId; + private String mapName; + + public String getMapId() { + return mapId; + } + + public void setMapId(String mapId) { + this.mapId = mapId; + } + + public String getMapName() { + return mapName; + } + + public void setMapName(String mapName) { + this.mapName = mapName; + } + } + + class BeanTarget { + private Integer beanId; + private String beanName; + + public Integer getBeanId() { + return beanId; + } + + public void setBeanId(Integer beanId) { + this.beanId = beanId; + } + + public String getBeanName() { + return beanName; + } + + public void setBeanName(String beanName) { + this.beanName = beanName; + } + } + + class BeanSource { + private Integer beanId; + private String beanName; + + public Integer getBeanId() { + return beanId; + } + + public void setBeanId(Integer beanId) { + this.beanId = beanId; + } + + public String getBeanName() { + return beanName; + } + + public void setBeanName(String beanName) { + this.beanName = beanName; + } + } + + class OtherSource { + private final Long id; + + public OtherSource(Long id) { + this.id = id; + } + + public Long getId() { + return id; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/mappingsource/implicitmapping/MappingSourceImplicitMappingTest.java b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/implicitmapping/MappingSourceImplicitMappingTest.java new file mode 100644 index 0000000000..53a8ab4ab8 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/implicitmapping/MappingSourceImplicitMappingTest.java @@ -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.mappingsource.implicitmapping; + +import java.util.HashMap; +import java.util.Map; + +import org.mapstruct.ap.test.mappingsource.implicitmapping.MappingSourceImplicitMappingMapper.BeanSource; +import org.mapstruct.ap.test.mappingsource.implicitmapping.MappingSourceImplicitMappingMapper.BeanTarget; +import org.mapstruct.ap.test.mappingsource.implicitmapping.MappingSourceImplicitMappingMapper.MapTarget; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThat; + +@IssueKey("2559") +@WithClasses(MappingSourceImplicitMappingMapper.class) +public class MappingSourceImplicitMappingTest { + + @ProcessorTest + public void testMultiSourceWithImplicitMapping() { + Map mapSource = new HashMap<>(); + mapSource.put( "mapId", "1" ); + mapSource.put( "mapName", "MapTest" ); + + BeanSource beanSource = new MappingSourceImplicitMappingMapper.BeanSource(); + beanSource.setBeanId( 2 ); + beanSource.setBeanName( "BeanTest" ); + + MapTarget mapTarget = MappingSourceImplicitMappingMapper.INSTANCE.multiSourceWithImplicitMap( + mapSource, + null + ); + + assertThat( mapTarget ).isNotNull(); + assertThat( mapTarget.getMapId() ).isEqualTo( "1" ); + assertThat( mapTarget.getMapName() ).isEqualTo( "MapTest" ); + + BeanTarget beanTarget = MappingSourceImplicitMappingMapper.INSTANCE.multiSourceWithImplicitBean( + beanSource, + null + ); + assertThat( beanTarget ).isNotNull(); + assertThat( beanTarget.getBeanId() ).isNull(); + assertThat( beanTarget.getBeanName() ).isNull(); + } + + @ProcessorTest + public void shouldDisableImplicitMappingForSingleMapSource() { + Map mapSource = Map.of( "mapId", "1", "mapName", "MapTest" ); + + MapTarget mapTarget = MappingSourceImplicitMappingMapper.INSTANCE.singleWithImplicitMap( mapSource ); + + assertThat( mapTarget ).isNotNull(); + assertThat( mapTarget.getMapId() ).isNull(); + assertThat( mapTarget.getMapName() ).isNull(); + } +} + diff --git a/processor/src/test/java/org/mapstruct/ap/test/mappingsource/primary/ErroneousMappingSourcePrimaryConflictingMapper.java b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/primary/ErroneousMappingSourcePrimaryConflictingMapper.java new file mode 100644 index 0000000000..cc84ea6ea1 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/primary/ErroneousMappingSourcePrimaryConflictingMapper.java @@ -0,0 +1,90 @@ +/* + * 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.mappingsource.primary; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingSource; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface ErroneousMappingSourcePrimaryConflictingMapper { + + ErroneousMappingSourcePrimaryConflictingMapper INSTANCE = Mappers.getMapper( + ErroneousMappingSourcePrimaryConflictingMapper.class ); + + @Mapping(target = "value", ignore = true) + Target mapFromMultiplePrimarySources(@MappingSource(primary = true) Source1 source1, + @MappingSource(primary = true) Source2 source2); + + @Mapping(target = "name", ignore = true) + @Mapping(target = "value") + Target mapWithExplicitTargetMultiplePrimary(@MappingSource(primary = true) Source1 source1, + @MappingSource(primary = true) Source2 source2); + + class Source1 { + private String name; + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + class Source2 { + private String name; + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + class Target { + private String name; + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/mappingsource/primary/MappingSourcePrimaryMapper.java b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/primary/MappingSourcePrimaryMapper.java new file mode 100644 index 0000000000..1e7647edb1 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/primary/MappingSourcePrimaryMapper.java @@ -0,0 +1,151 @@ +/* + * 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.mappingsource.primary; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingSource; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.factory.Mappers; + +@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface MappingSourcePrimaryMapper { + + MappingSourcePrimaryMapper INSTANCE = Mappers.getMapper( MappingSourcePrimaryMapper.class ); + + Target mapWithPrimarySource(Source1 source1, @MappingSource(primary = true) Source2 source2); + + Target mapWithPrimaryMissingProperty(Source1 source1, @MappingSource(primary = true) Source3 source3); + + @Mapping(target = "name") + Target mapExplicitTargetWithPrimary(Source1 source1, @MappingSource(primary = true) Source2 source2); + + class Source1 { + private String name; + private Integer value; + + public Source1(String name, Integer value) { + this.name = name; + this.value = value; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getValue() { + return value; + } + + public void setValue(Integer value) { + this.value = value; + } + } + + class Source2 { + private String name; + private Long count; + + public Source2(String name, Long count) { + this.name = name; + this.count = count; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Long getCount() { + return count; + } + + public void setCount(Long count) { + this.count = count; + } + } + + class Source3 { + private Integer id; + private Boolean active; + + public Source3(Integer id, Boolean active) { + this.id = id; + this.active = active; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Boolean getActive() { + return active; + } + + public void setActive(Boolean active) { + this.active = active; + } + } + + class Target { + private String name; + private Integer value; + private Long count; + private Integer id; + private Boolean active; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getValue() { + return value; + } + + public void setValue(Integer value) { + this.value = value; + } + + public Long getCount() { + return count; + } + + public void setCount(Long count) { + this.count = count; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Boolean getActive() { + return active; + } + + public void setActive(Boolean active) { + this.active = active; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/mappingsource/primary/MappingSourcePrimaryTest.java b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/primary/MappingSourcePrimaryTest.java new file mode 100644 index 0000000000..2ff4dd6ac1 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/mappingsource/primary/MappingSourcePrimaryTest.java @@ -0,0 +1,87 @@ +/* + * 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.mappingsource.primary; + +import org.mapstruct.ap.test.mappingsource.primary.MappingSourcePrimaryMapper.Source1; +import org.mapstruct.ap.test.mappingsource.primary.MappingSourcePrimaryMapper.Source2; +import org.mapstruct.ap.test.mappingsource.primary.MappingSourcePrimaryMapper.Source3; +import org.mapstruct.ap.test.mappingsource.primary.MappingSourcePrimaryMapper.Target; +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 org.mapstruct.ap.testutil.runner.Compiler; + +import static org.assertj.core.api.Assertions.assertThat; + +@IssueKey("3136") +public class MappingSourcePrimaryTest { + + // The error message is too long and causes line wrapping. + // Eclipse has some bugs and cannot obtain the correct error message. + @ProcessorTest(Compiler.JDK) + @WithClasses(ErroneousMappingSourcePrimaryConflictingMapper.class) + @ExpectedCompilationOutcome( + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic(type = ErroneousMappingSourcePrimaryConflictingMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 20, + message = "Several possible source properties for target property \"name\"."), + @Diagnostic(type = ErroneousMappingSourcePrimaryConflictingMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 25, + message = "Several possible source properties for target property \"value\".") + } + ) + public void shouldFailOnMultiplePrimarySourcesWithConflictingProperties() { + } + + @ProcessorTest + @WithClasses(MappingSourcePrimaryMapper.class) + public void shouldUsePropertyFromPrimarySourceInImplicitMapping() { + Source1 source1 = new Source1( "source1Name", 42 ); + Source2 source2 = new Source2( "source2Name", 100L ); + + Target target = MappingSourcePrimaryMapper.INSTANCE.mapWithPrimarySource( source1, source2 ); + + assertThat( target ).isNotNull(); + assertThat( target.getName() ).isEqualTo( "source2Name" ); + assertThat( target.getValue() ).isEqualTo( 42 ); + assertThat( target.getCount() ).isEqualTo( 100L ); + } + + @ProcessorTest + @WithClasses(MappingSourcePrimaryMapper.class) + public void shouldUseNonPrimarySourceWhenPropertyIsMissingInPrimary() { + Source1 source1 = new Source1( "source1Name", 42 ); + Source3 source3 = new Source3( 123, true ); + + Target target = MappingSourcePrimaryMapper.INSTANCE.mapWithPrimaryMissingProperty( source1, source3 ); + + assertThat( target ).isNotNull(); + assertThat( target.getName() ).isEqualTo( "source1Name" ); + assertThat( target.getValue() ).isEqualTo( 42 ); + assertThat( target.getId() ).isEqualTo( 123 ); + assertThat( target.getActive() ).isEqualTo( true ); + } + + @ProcessorTest + @WithClasses(MappingSourcePrimaryMapper.class) + public void shouldUsePropertyFromPrimarySourceInExplicitTargetOnlyMapping() { + Source1 source1 = new Source1( "source1Name", 42 ); + Source2 source2 = new Source2( "source2Name", 100L ); + + Target target = MappingSourcePrimaryMapper.INSTANCE.mapExplicitTargetWithPrimary( source1, source2 ); + + assertThat( target ).isNotNull(); + assertThat( target.getName() ).isEqualTo( "source2Name" ); + assertThat( target.getValue() ).isEqualTo( 42 ); + assertThat( target.getCount() ).isEqualTo( 100L ); + } +}