diff --git a/core/src/main/java/org/mapstruct/ConditionStrategy.java b/core/src/main/java/org/mapstruct/ConditionStrategy.java index 6b042017c2..d0a06cf8f7 100644 --- a/core/src/main/java/org/mapstruct/ConditionStrategy.java +++ b/core/src/main/java/org/mapstruct/ConditionStrategy.java @@ -20,4 +20,8 @@ public enum ConditionStrategy { * The condition method should be applied to check if a source parameters should be mapped. */ SOURCE_PARAMETERS, + /** + * The condition method should be applied whether an element should be added to the iterable target. + */ + ITERABLE_ELEMENTS, } diff --git a/core/src/main/java/org/mapstruct/IterableElementCondition.java b/core/src/main/java/org/mapstruct/IterableElementCondition.java new file mode 100644 index 0000000000..491d47a64c --- /dev/null +++ b/core/src/main/java/org/mapstruct/IterableElementCondition.java @@ -0,0 +1,25 @@ +/* + * 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; + +/** + * FIXME + * + * @author Oliver Erhart + * @since 1.7 + * @see Condition @Condition + */ +@Target({ ElementType.METHOD }) +@Retention(RetentionPolicy.CLASS) +@Condition(appliesTo = ConditionStrategy.ITERABLE_ELEMENTS) +public @interface IterableElementCondition { + +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/gem/ConditionStrategyGem.java b/processor/src/main/java/org/mapstruct/ap/internal/gem/ConditionStrategyGem.java index adea4b4c1f..e9432cafd4 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/gem/ConditionStrategyGem.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/gem/ConditionStrategyGem.java @@ -11,5 +11,6 @@ public enum ConditionStrategyGem { PROPERTIES, - SOURCE_PARAMETERS + SOURCE_PARAMETERS, + ITERABLE_ELEMENTS } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/IterableMappingMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/IterableMappingMethod.java index 10e64b7008..d638082ca6 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/IterableMappingMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/IterableMappingMethod.java @@ -5,8 +5,7 @@ */ package org.mapstruct.ap.internal.model; -import static org.mapstruct.ap.internal.util.Collections.first; - +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Set; @@ -14,10 +13,13 @@ import org.mapstruct.ap.internal.model.assignment.LocalVarWrapper; import org.mapstruct.ap.internal.model.assignment.SetterWrapper; import org.mapstruct.ap.internal.model.common.Assignment; +import org.mapstruct.ap.internal.model.common.ParameterBinding; import org.mapstruct.ap.internal.model.common.Type; import org.mapstruct.ap.internal.model.source.Method; import org.mapstruct.ap.internal.model.source.SelectionParameters; +import static org.mapstruct.ap.internal.util.Collections.first; + /** * A {@link MappingMethod} implemented by a {@link Mapper} class which maps one iterable type to another. The collection * elements are mapped either by a {@link TypeConversion} or another mapping method. @@ -26,12 +28,21 @@ */ public class IterableMappingMethod extends ContainerMappingMethod { + private Method iterableConditionMethod; + public static class Builder extends ContainerMappingMethodBuilder { + private Method iterableConditionMethod; + public Builder() { super( Builder.class, "collection element" ); } + public Builder iterableConditionMethod(Method iterableConditionMethod) { + this.iterableConditionMethod = iterableConditionMethod; + return this; + } + @Override protected Type getElementType(Type parameterType) { return parameterType.isArrayType() ? parameterType.getComponentType() : first( @@ -65,17 +76,36 @@ protected IterableMappingMethod instantiateMappingMethod(Method method, Collecti loopVariableName, beforeMappingMethods, afterMappingMethods, - selectionParameters + selectionParameters, + iterableConditionMethod ); } } + public boolean hasIterableConditionMethod() { + return iterableConditionMethod != null; + } + + public MethodReference getIterableConditionMethodReference() { + + ArrayList parameterBindings = new ArrayList<>(); + + parameterBindings.add( + ParameterBinding.fromTypeAndName( + iterableConditionMethod.getParameters().get( 0 ).getType(), + getLoopVariableName() + ) + ); + + return MethodReference.forForgedMethod( iterableConditionMethod, parameterBindings ); + } + private IterableMappingMethod(Method method, List annotations, Collection existingVariables, Assignment parameterAssignment, MethodReference factoryMethod, boolean mapNullToDefault, String loopVariableName, List beforeMappingReferences, List afterMappingReferences, - SelectionParameters selectionParameters) { + SelectionParameters selectionParameters, Method iterableConditionMethod) { super( method, annotations, @@ -88,6 +118,7 @@ private IterableMappingMethod(Method method, List annotations, afterMappingReferences, selectionParameters ); + this.iterableConditionMethod = iterableConditionMethod; } @Override diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/ConditionOptions.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/ConditionOptions.java index 936d049af8..3140982f99 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/ConditionOptions.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/ConditionOptions.java @@ -93,6 +93,9 @@ protected static boolean isValid(ConditionStrategyGem strategy, ConditionGem con else if ( strategy == ConditionStrategyGem.PROPERTIES ) { return hasValidStrategyForProperties( condition, method, parameters, messager ); } + else if ( strategy == ConditionStrategyGem.ITERABLE_ELEMENTS ) { + return true; + } else { throw new IllegalStateException( "Invalid condition strategy: " + strategy ); } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/processor/MapperCreationProcessor.java b/processor/src/main/java/org/mapstruct/ap/internal/processor/MapperCreationProcessor.java index 5fc1b07823..debaea36fe 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/processor/MapperCreationProcessor.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/processor/MapperCreationProcessor.java @@ -25,6 +25,7 @@ import javax.lang.model.util.ElementFilter; import org.mapstruct.ap.internal.gem.BuilderGem; +import org.mapstruct.ap.internal.gem.ConditionStrategyGem; import org.mapstruct.ap.internal.gem.DecoratedWithGem; import org.mapstruct.ap.internal.gem.InheritConfigurationGem; import org.mapstruct.ap.internal.gem.InheritInverseConfigurationGem; @@ -340,10 +341,18 @@ private List getMappingMethods(MapperOptions mapperAnnotation, Li this.messager.note( 1, Message.ITERABLEMAPPING_CREATE_NOTE, method ); + Method iterableConditionMethod = methods.stream() + .filter( m -> m.getConditionOptions() + .isStrategyApplicable( ConditionStrategyGem.ITERABLE_ELEMENTS ) ) + .findFirst() + .orElse( null ); + + // TODO here are all methods - select them via MethodResolver (like PresenceCheckMethodResolver) and add it to the builder IterableMappingMethod iterableMappingMethod = createWithElementMappingMethod( method, mappingOptions, new IterableMappingMethod.Builder() + .iterableConditionMethod( iterableConditionMethod ) ); hasFactoryMethod = iterableMappingMethod.getFactoryMethod() != null; diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/IterableMappingMethod.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/IterableMappingMethod.ftl index 4d08c44b38..3c03881a65 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/IterableMappingMethod.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/IterableMappingMethod.ftl @@ -11,7 +11,7 @@ <#if overridden>@Override <#lt>${accessibility.keyword} <@includeModel object=returnType/> ${name}(<#list parameters as param><@includeModel object=param/><#if param_has_next>, )<@throws/> { - <#list beforeMappingReferencesWithoutMappingTarget as callback> +<#list beforeMappingReferencesWithoutMappingTarget as callback> <@includeModel object=callback targetBeanName=resultName targetType=resultType/> <#if !callback_has_next> @@ -24,7 +24,6 @@ <#if resultType.arrayType> <#if existingInstanceMapping> <#-- we can't clear an existing array, so we've got to clear by setting values to default --> - for (int ${index2Name} = 0; ${index2Name} < ${resultName}.length; ${index2Name}++ ) { ${resultName}[${index2Name}] = ${resultElementType.null}; } return<#if returnType.name != "void"> ${resultName}; @@ -42,19 +41,19 @@ } - <#if resultType.arrayType> - <#if !existingInstanceMapping> - <#assign elementTypeString><@includeModel object=resultElementType/> - ${elementTypeString}[] ${resultName} = new ${elementTypeString?keep_before('[]')}[<@iterableSize/>]${elementTypeString?replace('[^\\[\\]]+', '', 'r')}; - - <#else> +<#-- <#if resultType.arrayType>--> +<#-- <#if !existingInstanceMapping>--> +<#-- <#assign elementTypeString><@includeModel object=resultElementType/>--> +<#-- ${elementTypeString}[] ${resultName} = new ${elementTypeString?keep_before('[]')}[<@iterableSize/>]${elementTypeString?replace('[^\\[\\]]+', '', 'r')};--> +<#-- --> +<#-- <#else>--> <#if existingInstanceMapping> ${resultName}.clear(); <#else> <#-- Use the interface type on the left side, except it is java.lang.Iterable; use the implementation type - if present - on the right side --> <@iterableLocalVarDef/> ${resultName} = <@includeModel object=iterableCreation useSizeIfPossible=true/>; - +<#-- --> <#list beforeMappingReferencesWithMappingTarget as callback> <@includeModel object=callback targetBeanName=resultName targetType=resultType/> <#if !callback_has_next> @@ -62,19 +61,40 @@ <#if resultType.arrayType> - int ${index1Name} = 0; + + + <#if true> +<#-- <#if resultType.arrayType>--> +<#-- int ${index1Name} = 0;--> +<#-- for ( <@includeModel object=sourceElementType/> ${loopVariableName} : ${sourceParameter.name} ) {--> +<#-- <#if existingInstanceMapping>--> +<#-- if ( ( ${index1Name} >= ${resultName}.length ) || ( ${index1Name} >= <@iterableSize/> ) ) {--> +<#-- break;--> +<#-- }--> +<#-- --> +<#-- <#if hasIterableConditionMethod()>--> +<#-- if ( <@includeModel object=iterableConditionMethodReference /> ) {--> +<#-- <@includeModel object=elementAssignment targetWriteAccessorName=resultName+"[${index1Name}]" targetType=resultElementType isTargetDefined=true/>--> +<#-- ${index1Name}++;--> +<#-- }--> +<#-- <#else>--> +<#-- <@includeModel object=elementAssignment targetWriteAccessorName=resultName+"[${index1Name}]" targetType=resultElementType isTargetDefined=true/>--> +<#-- ${index1Name}++;--> +<#-- --> +<#-- }--> +<#-- Employee[] employeeTmp = employees.stream()--> +<#-- .filter(this::countryIsNotNull)--> +<#-- .map(this::map)--> +<#-- .toArray(Employee[]::new);--> +<#-- <#else>--> for ( <@includeModel object=sourceElementType/> ${loopVariableName} : ${sourceParameter.name} ) { - <#if existingInstanceMapping> - if ( ( ${index1Name} >= ${resultName}.length ) || ( ${index1Name} >= <@iterableSize/> ) ) { - break; - } + <#if hasIterableConditionMethod()> + if ( <@includeModel object=iterableConditionMethodReference /> ) { + <@includeModel object=elementAssignment targetBeanName=resultName targetWriteAccessorName="add" targetType=resultElementType/> + } + <#else> + <@includeModel object=elementAssignment targetBeanName=resultName targetWriteAccessorName="add" targetType=resultElementType/> - <@includeModel object=elementAssignment targetWriteAccessorName=resultName+"[${index1Name}]" targetType=resultElementType isTargetDefined=true/> - ${index1Name}++; - } - <#else> - for ( <@includeModel object=sourceElementType/> ${loopVariableName} : ${sourceParameter.name} ) { - <@includeModel object=elementAssignment targetBeanName=resultName targetWriteAccessorName="add" targetType=resultElementType/> } <#list afterMappingReferences as callback> @@ -85,7 +105,11 @@ <#if returnType.name != "void"> - return ${resultName}; + <#if hasIterableConditionMethod() && resultType.arrayType> + ${resultName}.toArray( new <@includeModel object=resultElementType/>[0] ); + <#else> + return ${resultName}; + } <#macro throws> diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/iterable/IterableElementConditionMapper.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/iterable/IterableElementConditionMapper.java new file mode 100644 index 0000000000..e27fa022f3 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/iterable/IterableElementConditionMapper.java @@ -0,0 +1,44 @@ +/* + * 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.conditional.iterable; + +import java.util.List; + +import org.mapstruct.IterableElementCondition; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ap.test.conditional.Employee; +import org.mapstruct.ap.test.conditional.EmployeeDto; +import org.mapstruct.factory.Mappers; + +/** + * @author Oliver Erhart + */ +@Mapper +public interface IterableElementConditionMapper { + + IterableElementConditionMapper INSTANCE = + Mappers.getMapper( IterableElementConditionMapper.class ); + + @Mapping(target = "nin", source = "name") + @Mapping(target = "ssid", source = "uniqueIdNumber") + Employee map(EmployeeDto employee); + + List mapListToList(List employees); + + List mapArrayToList(EmployeeDto[] employees); + +// Employee[] mapListToArray(List employees); +// +// Employee[] mapArrayToArray(EmployeeDto[] employees); + + + @IterableElementCondition + default boolean countryIsNotNull(EmployeeDto value) { + return value.getCountry() != null; + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/conditional/iterable/IterableElementConditionTest.java b/processor/src/test/java/org/mapstruct/ap/test/conditional/iterable/IterableElementConditionTest.java new file mode 100644 index 0000000000..3cd7fb0020 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/conditional/iterable/IterableElementConditionTest.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.conditional.iterable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.mapstruct.ap.test.conditional.Employee; +import org.mapstruct.ap.test.conditional.EmployeeDto; +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; + +/** + * @author Oliver Erhart + */ +@IssueKey("1610") +@WithClasses({ + Employee.class, + EmployeeDto.class, + IterableElementConditionMapper.class, +}) +public class IterableElementConditionTest { + + @ProcessorTest + public void conditionalMethodListToList() { + + List result = IterableElementConditionMapper.INSTANCE.mapListToList( setupList() ); + + assertThatOnlyFilteredValuesMapped( result ); + } + + @ProcessorTest + public void conditionalMethodArrayToList() { + + List result = IterableElementConditionMapper.INSTANCE.mapArrayToList( setupArray() ); + + assertThatOnlyFilteredValuesMapped( result ); + } + +// @ProcessorTest +// public void conditionalMethodListToArray() { +// +// Employee[] result = IterableElementConditionMapper.INSTANCE.mapListToArray( setupList() ); +// +// assertThatOnlyFilteredValuesMapped( result ); +// } +// +// @ProcessorTest +// public void conditionalMethodArrayToArray() { +// +// Employee[] result = IterableElementConditionMapper.INSTANCE.mapArrayToArray( setupArray() ); +// +// assertThatOnlyFilteredValuesMapped( result ); +// } + + private static void assertThatOnlyFilteredValuesMapped(List result) { + assertThat( result ) + .singleElement() + .satisfies( + d -> assertThat( d.getName() ).isEqualTo( "Tester" ) + ); + } + + private static void assertThatOnlyFilteredValuesMapped(Employee[] result) { + assertThatOnlyFilteredValuesMapped( + Arrays.stream( result ).collect( Collectors.toList() ) + ); + } + + private static ArrayList setupList() { + + EmployeeDto dtoWithoutCountry = new EmployeeDto(); + dtoWithoutCountry.setName( "Tester" ); + dtoWithoutCountry.setUniqueIdNumber( "SSID-001" ); + dtoWithoutCountry.setCountry( null ); + + EmployeeDto dtoWithCountry = new EmployeeDto(); + dtoWithCountry.setName( "Tester" ); + dtoWithCountry.setUniqueIdNumber( "SSID-001" ); + dtoWithCountry.setCountry( "Austria" ); + + ArrayList employees = new ArrayList<>(); + employees.add( dtoWithoutCountry ); + employees.add( dtoWithCountry ); + + return employees; + } + + private static EmployeeDto[] setupArray() { + return setupList().toArray( new EmployeeDto[0] ); + } + +}