diff --git a/build-config/src/main/resources/build-config/checkstyle.xml b/build-config/src/main/resources/build-config/checkstyle.xml index 945015d5c..a4591c39d 100644 --- a/build-config/src/main/resources/build-config/checkstyle.xml +++ b/build-config/src/main/resources/build-config/checkstyle.xml @@ -122,9 +122,6 @@ - - - diff --git a/documentation/src/main/asciidoc/chapter-3-defining-a-mapper.asciidoc b/documentation/src/main/asciidoc/chapter-3-defining-a-mapper.asciidoc index 5255ac6f1..ad5d8111f 100644 --- a/documentation/src/main/asciidoc/chapter-3-defining-a-mapper.asciidoc +++ b/documentation/src/main/asciidoc/chapter-3-defining-a-mapper.asciidoc @@ -528,3 +528,80 @@ Otherwise, you would need to write a custom `BuilderProvider` ==== In case you want to disable using builders then you can use the `NoOpBuilderProvider` by creating a `org.mapstruct.ap.spi.BuilderProvider` file in the `META-INF/services` directory with `org.mapstruct.ap.spi.NoOpBuilderProvider` as it's content. ==== + +[[mapping-with-constructors]] +=== Using Constructors + +MapStruct supports using constructors for mapping target types. +When doing a mapping MapStruct checks if there is a builder for the type being mapped. +If there is no builder, then MapStruct looks for a single accessible constructor. +When there are multiple constructors then the following is done to pick the one which should be used: + +* If a parameterless constructor exists then it would be used to construct the object, and the other constructors will be ignored +* If there are multiple constructors then the one annotated with annotation named `@Default` (from any package) will be used + +When using a constructor then the names of the parameters of the constructor will be used and matched to the target properties. +When the constructor has an annotation named `@ConstructorProperties` (from any package) then this annotation will be used to get the names of the parameters. + +[NOTE] +==== +When an object factory method or a method annotated with `@ObjectFactory` exists, it will take precedence over any constructor defined in the target. +The target object constructor will not be used in that case. +==== + + +.Person with constructor parameters +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +public class Person { + + private final String name; + private final String surname; + + public Person(String name, String surname) { + this.name = name; + this.surname = surname; + } +} +---- +==== + +.Person With Constructor Mapper definition +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +public interface PersonMapper { + + Person map(PersonDto dto); +} +---- +==== + +.Generated mapper with constructor +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +// GENERATED CODE +public class PersonMapperImpl implements PersonMapper { + + public Person map(PersonDto dto) { + if (dto == null) { + return null; + } + + String name; + String surname; + name = dto.getName(); + surname = dto.getSurname(); + + Person person = new Person( name, surname ); + + return person; + } +} +---- +==== diff --git a/documentation/src/main/asciidoc/chapter-5-data-type-conversions.asciidoc b/documentation/src/main/asciidoc/chapter-5-data-type-conversions.asciidoc index 7a22aad55..725c69bad 100644 --- a/documentation/src/main/asciidoc/chapter-5-data-type-conversions.asciidoc +++ b/documentation/src/main/asciidoc/chapter-5-data-type-conversions.asciidoc @@ -174,6 +174,12 @@ During the generation of automatic sub-mapping methods <> Follow issue https://github.com/mapstruct/mapstruct/issues/1086[#1086] for more information. ==== +[NOTE] +==== +Constructor properties of the target object are also considered as target properties. +You can read more about that in <> +==== + [[controlling-nested-bean-mappings]] === Controlling nested bean mappings diff --git a/integrationtest/src/test/java/org/mapstruct/itest/tests/MavenIntegrationTest.java b/integrationtest/src/test/java/org/mapstruct/itest/tests/MavenIntegrationTest.java index 0789e3051..76a72b4b7 100644 --- a/integrationtest/src/test/java/org/mapstruct/itest/tests/MavenIntegrationTest.java +++ b/integrationtest/src/test/java/org/mapstruct/itest/tests/MavenIntegrationTest.java @@ -107,6 +107,14 @@ public class MavenIntegrationTest { void recordsTest() { } + @ProcessorTest(baseDir = "kotlinDataTest", processorTypes = { + ProcessorTest.ProcessorType.JAVAC + }, forkJvm = true) + // We have to fork the jvm because there is an NPE in com.intellij.openapi.util.SystemInfo.getRtVersion + // and the kotlin-maven-plugin uses that. See also https://youtrack.jetbrains.com/issue/IDEA-238907 + void kotlinDataTest() { + } + @ProcessorTest(baseDir = "simpleTest") void simpleTest() { } diff --git a/integrationtest/src/test/java/org/mapstruct/itest/testutil/extension/ProcessorInvocationInterceptor.java b/integrationtest/src/test/java/org/mapstruct/itest/testutil/extension/ProcessorInvocationInterceptor.java index f6f9186ba..839bf86f1 100644 --- a/integrationtest/src/test/java/org/mapstruct/itest/testutil/extension/ProcessorInvocationInterceptor.java +++ b/integrationtest/src/test/java/org/mapstruct/itest/testutil/extension/ProcessorInvocationInterceptor.java @@ -71,6 +71,9 @@ public class ProcessorInvocationInterceptor implements InvocationInterceptor { } else { verifier = new Verifier( destination.getCanonicalPath() ); + if ( processorTestContext.isForkJvm() ) { + verifier.setForkJvm( true ); + } } List goals = new ArrayList<>( 3 ); diff --git a/integrationtest/src/test/java/org/mapstruct/itest/testutil/extension/ProcessorTest.java b/integrationtest/src/test/java/org/mapstruct/itest/testutil/extension/ProcessorTest.java index 1db77ae6b..eb8553960 100644 --- a/integrationtest/src/test/java/org/mapstruct/itest/testutil/extension/ProcessorTest.java +++ b/integrationtest/src/test/java/org/mapstruct/itest/testutil/extension/ProcessorTest.java @@ -106,4 +106,7 @@ public @interface ProcessorTest { * @return the {@link CommandLineEnhancer} implementation. Must have a default constructor. */ Class commandLineEnhancer() default CommandLineEnhancer.class; + + boolean forkJvm() default false; + } diff --git a/integrationtest/src/test/java/org/mapstruct/itest/testutil/extension/ProcessorTestContext.java b/integrationtest/src/test/java/org/mapstruct/itest/testutil/extension/ProcessorTestContext.java index be1e4647c..feb5c7dc0 100644 --- a/integrationtest/src/test/java/org/mapstruct/itest/testutil/extension/ProcessorTestContext.java +++ b/integrationtest/src/test/java/org/mapstruct/itest/testutil/extension/ProcessorTestContext.java @@ -13,13 +13,16 @@ public class ProcessorTestContext { private final String baseDir; private final ProcessorTest.ProcessorType processor; private final Class cliEnhancerClass; + private final boolean forkJvm; public ProcessorTestContext(String baseDir, ProcessorTest.ProcessorType processor, - Class cliEnhancerClass) { + Class cliEnhancerClass, + boolean forkJvm) { this.baseDir = baseDir; this.processor = processor; this.cliEnhancerClass = cliEnhancerClass; + this.forkJvm = forkJvm; } public String getBaseDir() { @@ -33,4 +36,8 @@ public class ProcessorTestContext { public Class getCliEnhancerClass() { return cliEnhancerClass; } + + public boolean isForkJvm() { + return forkJvm; + } } diff --git a/integrationtest/src/test/java/org/mapstruct/itest/testutil/extension/ProcessorTestTemplateInvocationContextProvider.java b/integrationtest/src/test/java/org/mapstruct/itest/testutil/extension/ProcessorTestTemplateInvocationContextProvider.java index b7a4f331c..5ceb44804 100644 --- a/integrationtest/src/test/java/org/mapstruct/itest/testutil/extension/ProcessorTestTemplateInvocationContextProvider.java +++ b/integrationtest/src/test/java/org/mapstruct/itest/testutil/extension/ProcessorTestTemplateInvocationContextProvider.java @@ -34,7 +34,8 @@ public class ProcessorTestTemplateInvocationContextProvider implements TestTempl .map( processorType -> new ProcessorTestTemplateInvocationContext( new ProcessorTestContext( processorTest.baseDir(), processorType, - processorTest.commandLineEnhancer() + processorTest.commandLineEnhancer(), + processorTest.forkJvm() ) ) ); } } diff --git a/integrationtest/src/test/resources/kotlinDataTest/pom.xml b/integrationtest/src/test/resources/kotlinDataTest/pom.xml new file mode 100644 index 000000000..8a43a0419 --- /dev/null +++ b/integrationtest/src/test/resources/kotlinDataTest/pom.xml @@ -0,0 +1,102 @@ + + + + 4.0.0 + + + org.mapstruct + mapstruct-it-parent + 1.0.0 + ../pom.xml + + + kotlinDataTest + jar + + + 1.3.70 + + + + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + + + + + + generate-via-compiler-plugin + + false + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + compile + + + \${project.basedir}/src/main/kotlin + \${project.basedir}/src/main/java + + + + + test-compile + test-compile + + + \${project.basedir}/src/test/kotlin + \${project.basedir}/src/test/java + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + default-compile + none + + + + default-testCompile + none + + + java-compile + compile + compile + + + java-test-compile + test-compile + testCompile + + + + + + + + + diff --git a/integrationtest/src/test/resources/kotlinDataTest/src/main/java/org/mapstruct/itest/kotlin/data/CustomerEntity.java b/integrationtest/src/test/resources/kotlinDataTest/src/main/java/org/mapstruct/itest/kotlin/data/CustomerEntity.java new file mode 100644 index 000000000..e992eb0e6 --- /dev/null +++ b/integrationtest/src/test/resources/kotlinDataTest/src/main/java/org/mapstruct/itest/kotlin/data/CustomerEntity.java @@ -0,0 +1,31 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.kotlin.data; + +/** + * @author Filip Hrisafov + */ +public class CustomerEntity { + + private String name; + private String mail; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getMail() { + return mail; + } + + public void setMail(String mail) { + this.mail = mail; + } +} diff --git a/integrationtest/src/test/resources/kotlinDataTest/src/main/java/org/mapstruct/itest/kotlin/data/CustomerMapper.java b/integrationtest/src/test/resources/kotlinDataTest/src/main/java/org/mapstruct/itest/kotlin/data/CustomerMapper.java new file mode 100644 index 000000000..b2157252d --- /dev/null +++ b/integrationtest/src/test/resources/kotlinDataTest/src/main/java/org/mapstruct/itest/kotlin/data/CustomerMapper.java @@ -0,0 +1,27 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.kotlin.data; + +import org.mapstruct.InheritInverseConfiguration; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface CustomerMapper { + + CustomerMapper INSTANCE = Mappers.getMapper( CustomerMapper.class ); + + @Mapping(target = "mail", source = "email") + CustomerEntity fromRecord(CustomerDto record); + + @InheritInverseConfiguration + CustomerDto toRecord(CustomerEntity entity); + +} diff --git a/integrationtest/src/test/resources/kotlinDataTest/src/main/kotlin/org/mapstruct/itest/kotlin/data/CustomerDto.kt b/integrationtest/src/test/resources/kotlinDataTest/src/main/kotlin/org/mapstruct/itest/kotlin/data/CustomerDto.kt new file mode 100644 index 000000000..820a8b56c --- /dev/null +++ b/integrationtest/src/test/resources/kotlinDataTest/src/main/kotlin/org/mapstruct/itest/kotlin/data/CustomerDto.kt @@ -0,0 +1,10 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.kotlin.data; + +data class CustomerDto(var name: String?, var email: String?) { + +} diff --git a/integrationtest/src/test/resources/kotlinDataTest/src/test/java/org/mapstruct/itest/kotlin/data/KotlinDataTest.java b/integrationtest/src/test/resources/kotlinDataTest/src/test/java/org/mapstruct/itest/kotlin/data/KotlinDataTest.java new file mode 100644 index 000000000..513f080a8 --- /dev/null +++ b/integrationtest/src/test/resources/kotlinDataTest/src/test/java/org/mapstruct/itest/kotlin/data/KotlinDataTest.java @@ -0,0 +1,38 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.itest.kotlin.data; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; +import org.mapstruct.itest.kotlin.data.CustomerDto; +import org.mapstruct.itest.kotlin.data.CustomerEntity; +import org.mapstruct.itest.kotlin.data.CustomerMapper; + +public class KotlinDataTest { + + @Test + public void shouldMapData() { + CustomerEntity customer = CustomerMapper.INSTANCE.fromRecord( new CustomerDto( "Kermit", "kermit@test.com" ) ); + + assertThat( customer ).isNotNull(); + assertThat( customer.getName() ).isEqualTo( "Kermit" ); + assertThat( customer.getMail() ).isEqualTo( "kermit@test.com" ); + } + + @Test + public void shouldMapIntoData() { + CustomerEntity entity = new CustomerEntity(); + entity.setName( "Kermit" ); + entity.setMail( "kermit@test.com" ); + + CustomerDto customer = CustomerMapper.INSTANCE.toRecord( entity ); + + assertThat( customer ).isNotNull(); + assertThat( customer.getName() ).isEqualTo( "Kermit" ); + assertThat( customer.getEmail() ).isEqualTo( "kermit@test.com" ); + } +} diff --git a/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/CustomerMapper.java b/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/CustomerMapper.java index 0e8a0d256..a3c7037f0 100644 --- a/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/CustomerMapper.java +++ b/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/CustomerMapper.java @@ -5,6 +5,7 @@ */ package org.mapstruct.itest.records; +import org.mapstruct.InheritInverseConfiguration; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.factory.Mappers; @@ -20,4 +21,7 @@ public interface CustomerMapper { @Mapping(target = "mail", source = "email") CustomerEntity fromRecord(CustomerDto record); + @InheritInverseConfiguration + CustomerDto toRecord(CustomerEntity entity); + } diff --git a/integrationtest/src/test/resources/recordsTest/src/test/java/org/mapstruct/itest/records/RecordsTest.java b/integrationtest/src/test/resources/recordsTest/src/test/java/org/mapstruct/itest/records/RecordsTest.java index 5a7a62f78..824a763fc 100644 --- a/integrationtest/src/test/resources/recordsTest/src/test/java/org/mapstruct/itest/records/RecordsTest.java +++ b/integrationtest/src/test/resources/recordsTest/src/test/java/org/mapstruct/itest/records/RecordsTest.java @@ -22,4 +22,17 @@ public class RecordsTest { assertThat( customer.getName() ).isEqualTo( "Kermit" ); assertThat( customer.getMail() ).isEqualTo( "kermit@test.com" ); } + + @Test + public void shouldMapIntoRecord() { + CustomerEntity entity = new CustomerEntity(); + entity.setName( "Kermit" ); + entity.setMail( "kermit@test.com" ); + + CustomerDto customer = CustomerMapper.INSTANCE.toRecord( entity ); + + assertThat( customer ).isNotNull(); + assertThat( customer.name() ).isEqualTo( "Kermit" ); + assertThat( customer.email() ).isEqualTo( "kermit@test.com" ); + } } 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 53a271858..ea4f3fbde 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 @@ -8,18 +8,26 @@ package org.mapstruct.ap.internal.model; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; import javax.lang.model.type.DeclaredType; +import javax.lang.model.util.ElementFilter; import javax.tools.Diagnostic; import org.mapstruct.ap.internal.gem.CollectionMappingStrategyGem; @@ -33,6 +41,7 @@ import org.mapstruct.ap.internal.model.beanmapping.SourceReference; import org.mapstruct.ap.internal.model.beanmapping.TargetReference; import org.mapstruct.ap.internal.model.common.BuilderType; import org.mapstruct.ap.internal.model.common.Parameter; +import org.mapstruct.ap.internal.model.common.ParameterBinding; import org.mapstruct.ap.internal.model.common.Type; import org.mapstruct.ap.internal.model.dependency.GraphAnalyzer; import org.mapstruct.ap.internal.model.dependency.GraphAnalyzer.GraphAnalyzerBuilder; @@ -41,15 +50,20 @@ import org.mapstruct.ap.internal.model.source.MappingOptions; import org.mapstruct.ap.internal.model.source.Method; import org.mapstruct.ap.internal.model.source.SelectionParameters; import org.mapstruct.ap.internal.model.source.SourceMethod; +import org.mapstruct.ap.internal.model.source.selector.SelectedMethod; import org.mapstruct.ap.internal.util.Message; import org.mapstruct.ap.internal.util.Strings; import org.mapstruct.ap.internal.util.accessor.Accessor; +import org.mapstruct.ap.internal.util.accessor.AccessorType; +import org.mapstruct.ap.internal.util.accessor.ParameterElementAccessor; import static org.mapstruct.ap.internal.model.beanmapping.MappingReferences.forSourceMethod; import static org.mapstruct.ap.internal.util.Collections.first; import static org.mapstruct.ap.internal.util.Message.BEANMAPPING_ABSTRACT; import static org.mapstruct.ap.internal.util.Message.BEANMAPPING_NOT_ASSIGNABLE; import static org.mapstruct.ap.internal.util.Message.GENERAL_ABSTRACT_RETURN_TYPE; +import static org.mapstruct.ap.internal.util.Message.GENERAL_AMBIGIOUS_CONSTRUCTORS; +import static org.mapstruct.ap.internal.util.Message.GENERAL_CONSTRUCTOR_PROPERTIES_NOT_MATCHING_PARAMETERS; /** * A {@link MappingMethod} implemented by a {@link Mapper} class which maps one bean type to another, optionally @@ -61,7 +75,9 @@ public class BeanMappingMethod extends NormalTypeMappingMethod { private final List propertyMappings; private final Map> mappingsByParameter; + private final Map> constructorMappingsByParameter; private final List constantMappings; + private final List constructorConstantMappings; private final Type returnTypeToConstruct; private final BuilderType returnTypeBuilder; private final MethodReference finalizerMethod; @@ -73,6 +89,7 @@ public class BeanMappingMethod extends NormalTypeMappingMethod { /* returnType to construct can have a builder */ private BuilderType returnTypeBuilder; + private Map unprocessedConstructorProperties; private Map unprocessedTargetProperties; private Map unprocessedSourceProperties; private Set targetProperties; @@ -82,6 +99,8 @@ public class BeanMappingMethod extends NormalTypeMappingMethod { private final Map> unprocessedDefinedTargets = new LinkedHashMap<>(); private MappingReferences mappingReferences; + private MethodReference factoryMethod; + private boolean hasFactoryMethod; public Builder mappingContext(MappingBuilderContext mappingContext) { this.ctx = mappingContext; @@ -125,15 +144,12 @@ public class BeanMappingMethod extends NormalTypeMappingMethod { /* 3) or the builder whenever the return type is immutable */ Type returnTypeToConstruct = null; - /* factory or builder method to construct the returnTypeToConstruct */ - MethodReference factoryMethod = null; - // determine which return type to construct boolean cannotConstructReturnType = false; if ( !method.getReturnType().isVoid() ) { Type returnTypeImpl = getReturnTypeToConstructFromSelectionParameters( selectionParameters ); if ( returnTypeImpl != null ) { - factoryMethod = getFactoryMethod( returnTypeImpl, selectionParameters ); + initializeFactoryMethod( returnTypeImpl, selectionParameters ); if ( factoryMethod != null || canResultTypeFromBeanMappingBeConstructed( returnTypeImpl ) ) { returnTypeToConstruct = returnTypeImpl; } @@ -143,7 +159,7 @@ public class BeanMappingMethod extends NormalTypeMappingMethod { } else if ( isBuilderRequired() ) { returnTypeImpl = returnTypeBuilder.getBuilder(); - factoryMethod = getFactoryMethod( returnTypeImpl, selectionParameters ); + initializeFactoryMethod( returnTypeImpl, selectionParameters ); if ( factoryMethod != null || canReturnTypeBeConstructed( returnTypeImpl ) ) { returnTypeToConstruct = returnTypeImpl; } @@ -153,7 +169,7 @@ public class BeanMappingMethod extends NormalTypeMappingMethod { } else if ( !method.isUpdateMethod() ) { returnTypeImpl = method.getReturnType(); - factoryMethod = getFactoryMethod( returnTypeImpl, selectionParameters ); + initializeFactoryMethod( returnTypeImpl, selectionParameters ); if ( factoryMethod != null || canReturnTypeBeConstructed( returnTypeImpl ) ) { returnTypeToConstruct = returnTypeImpl; } @@ -171,13 +187,39 @@ public class BeanMappingMethod extends NormalTypeMappingMethod { /* the type that needs to be used in the mapping process as target */ Type resultTypeToMap = returnTypeToConstruct == null ? method.getResultType() : returnTypeToConstruct; + existingVariableNames.addAll( method.getParameterNames() ); + CollectionMappingStrategyGem cms = this.method.getOptions().getMapper().getCollectionMappingStrategy(); // determine accessors Map accessors = resultTypeToMap.getPropertyWriteAccessors( cms ); - this.targetProperties = accessors.keySet(); + this.targetProperties = new LinkedHashSet<>( accessors.keySet() ); this.unprocessedTargetProperties = new LinkedHashMap<>( accessors ); + + if ( !method.isUpdateMethod() && !hasFactoryMethod ) { + ConstructorAccessor constructorAccessor = getConstructorAccessor( resultTypeToMap ); + if ( constructorAccessor != null ) { + + this.unprocessedConstructorProperties = constructorAccessor.constructorAccessors; + + factoryMethod = MethodReference.forConstructorInvocation( + resultTypeToMap, + constructorAccessor.parameterBindings + ); + + } + else { + this.unprocessedConstructorProperties = new LinkedHashMap<>(); + } + + this.targetProperties.addAll( this.unprocessedConstructorProperties.keySet() ); + this.unprocessedTargetProperties.putAll( this.unprocessedConstructorProperties ); + } + else { + unprocessedConstructorProperties = new LinkedHashMap<>(); + } + this.unprocessedSourceProperties = new LinkedHashMap<>(); for ( Parameter sourceParameter : method.getSourceParameters() ) { unprocessedSourceParameters.add( sourceParameter ); @@ -191,7 +233,6 @@ public class BeanMappingMethod extends NormalTypeMappingMethod { unprocessedSourceProperties.put( entry.getKey(), entry.getValue() ); } } - existingVariableNames.addAll( method.getParameterNames() ); // get bean mapping (when specified as annotation ) if ( beanMapping != null ) { @@ -223,6 +264,9 @@ public class BeanMappingMethod extends NormalTypeMappingMethod { // Process the unprocessed defined targets handleUnprocessedDefinedTargets(); + // Initialize unprocessed constructor properties + handleUnmappedConstructorProperties(); + // report errors on unmapped properties reportErrorForUnmappedTargetPropertiesIfRequired(); reportErrorForUnmappedSourcePropertiesIfRequired(); @@ -364,6 +408,7 @@ public class BeanMappingMethod extends NormalTypeMappingMethod { if ( propertyMapping != null ) { unprocessedTargetProperties.remove( propertyName ); + unprocessedConstructorProperties.remove( propertyName ); unprocessedSourceProperties.remove( propertyName ); iterator.remove(); propertyMappings.add( propertyMapping ); @@ -374,6 +419,28 @@ public class BeanMappingMethod extends NormalTypeMappingMethod { } } + private void handleUnmappedConstructorProperties() { + for ( Entry entry : unprocessedConstructorProperties.entrySet() ) { + Accessor accessor = entry.getValue(); + Type accessedType = ctx.getTypeFactory() + .getType( accessor.getAccessedType() ); + String targetPropertyName = entry.getKey(); + + propertyMappings.add( new JavaExpressionMappingBuilder() + .mappingContext( ctx ) + .sourceMethod( method ) + .javaExpression( accessedType.getNull() ) + .existingVariableNames( existingVariableNames ) + .target( targetPropertyName, null, accessor ) + .dependsOn( Collections.emptySet() ) + .mirror( null ) + .build() + ); + } + + unprocessedConstructorProperties.clear(); + } + /** * Sources the given mappings as per the dependency relationships given via {@code dependsOn()}. If a cycle is * detected, an error is reported. @@ -434,7 +501,7 @@ public class BeanMappingMethod extends NormalTypeMappingMethod { ); error = false; } - else if ( !resultType.hasEmptyAccessibleConstructor() ) { + else if ( !resultType.hasAccessibleConstructor() ) { ctx.getMessager().printMessage( method.getExecutable(), method.getOptions().getBeanMapping().getMirror(), @@ -456,7 +523,7 @@ public class BeanMappingMethod extends NormalTypeMappingMethod { ); error = false; } - else if ( !returnType.hasEmptyAccessibleConstructor() ) { + else if ( !returnType.hasAccessibleConstructor() ) { ctx.getMessager().printMessage( method.getExecutable(), Message.GENERAL_NO_SUITABLE_CONSTRUCTOR, @@ -470,20 +537,225 @@ public class BeanMappingMethod extends NormalTypeMappingMethod { /** * Find a factory method for a return type or for a builder. * @param returnTypeImpl the return type implementation to construct - * @param selectionParameters + * @param @selectionParameters * @return */ - private MethodReference getFactoryMethod(Type returnTypeImpl, SelectionParameters selectionParameters) { - MethodReference factoryMethod = ObjectFactoryMethodResolver.getFactoryMethod( method, - returnTypeImpl, - selectionParameters, - ctx - ); - if ( factoryMethod == null && returnTypeBuilder != null ) { - factoryMethod = ObjectFactoryMethodResolver.getBuilderFactoryMethod( method, returnTypeBuilder ); + private void initializeFactoryMethod(Type returnTypeImpl, SelectionParameters selectionParameters) { + List> matchingFactoryMethods = + ObjectFactoryMethodResolver.getMatchingFactoryMethods( + method, + returnTypeImpl, + selectionParameters, + ctx + ); + + if ( matchingFactoryMethods.isEmpty() ) { + if ( factoryMethod == null && returnTypeBuilder != null ) { + factoryMethod = ObjectFactoryMethodResolver.getBuilderFactoryMethod( method, returnTypeBuilder ); + hasFactoryMethod = factoryMethod != null; + } + } + else if ( matchingFactoryMethods.size() == 1 ) { + factoryMethod = ObjectFactoryMethodResolver.getFactoryMethodReference( + method, + first( matchingFactoryMethods ), + ctx + ); + hasFactoryMethod = true; + } + else { + ctx.getMessager().printMessage( + method.getExecutable(), + Message.GENERAL_AMBIGIOUS_FACTORY_METHOD, + returnTypeImpl, + Strings.join( matchingFactoryMethods, ", " ) + ); + hasFactoryMethod = true; + } + } + + private ConstructorAccessor getConstructorAccessor(Type type) { + + if ( type.isRecord() ) { + // If the type is a record then just get the record components and use then + List recordComponents = type.getRecordComponents(); + List parameterBindings = new ArrayList<>( recordComponents.size() ); + Map constructorAccessors = new LinkedHashMap<>(); + for ( Element recordComponent : recordComponents ) { + String parameterName = recordComponent.getSimpleName().toString(); + Accessor accessor = createConstructorAccessor( recordComponent, parameterName ); + constructorAccessors.put( + parameterName, + accessor + ); + + parameterBindings.add( ParameterBinding.fromTypeAndName( + ctx.getTypeFactory().getType( recordComponent.asType() ), + accessor.getSimpleName() + ) ); + } + return new ConstructorAccessor( parameterBindings, constructorAccessors ); } - return factoryMethod; + List constructors = ElementFilter.constructorsIn( type.getTypeElement() + .getEnclosedElements() ); + + ExecutableElement defaultConstructor = null; + List accessibleConstructors = new ArrayList<>( constructors.size() ); + + for ( ExecutableElement constructor : constructors ) { + if ( constructor.getModifiers().contains( Modifier.PRIVATE ) ) { + continue; + } + + if ( constructor.getParameters().isEmpty() ) { + defaultConstructor = constructor; + } + else { + accessibleConstructors.add( constructor ); + } + } + + if ( defaultConstructor != null ) { + return null; + } + + if ( accessibleConstructors.isEmpty() ) { + return null; + } + + ExecutableElement constructor = null; + if ( accessibleConstructors.size() > 1 ) { + + for ( ExecutableElement accessibleConstructor : accessibleConstructors ) { + for ( AnnotationMirror annotationMirror : accessibleConstructor.getAnnotationMirrors() ) { + if ( annotationMirror.getAnnotationType() + .asElement() + .getSimpleName() + .contentEquals( "Default" ) ) { + constructor = accessibleConstructor; + break; + } + } + } + + if ( constructor == null ) { + ctx.getMessager().printMessage( + method.getExecutable(), + GENERAL_AMBIGIOUS_CONSTRUCTORS, + type, + Strings.join( constructors, ", " ) + ); + return null; + } + } + else { + constructor = accessibleConstructors.get( 0 ); + } + + List constructorParameters = ctx.getTypeFactory() + .getParameters( (DeclaredType) type.getTypeMirror(), constructor ); + + + List constructorProperties = null; + for ( AnnotationMirror annotationMirror : constructor.getAnnotationMirrors() ) { + if ( annotationMirror.getAnnotationType() + .asElement() + .getSimpleName() + .contentEquals( "ConstructorProperties" ) ) { + for ( Entry entry : annotationMirror + .getElementValues() + .entrySet() ) { + if ( entry.getKey().getSimpleName().contentEquals( "value" ) ) { + constructorProperties = getArrayValues( entry.getValue() ); + break; + } + } + break; + } + } + + if ( constructorProperties == null ) { + Map constructorAccessors = new LinkedHashMap<>(); + List parameterBindings = new ArrayList<>( constructorParameters.size() ); + for ( Parameter constructorParameter : constructorParameters ) { + String parameterName = constructorParameter.getName(); + Element parameterElement = constructorParameter.getElement(); + Accessor constructorAccessor = createConstructorAccessor( parameterElement, parameterName ); + constructorAccessors.put( + parameterName, + constructorAccessor + ); + parameterBindings.add( ParameterBinding.fromTypeAndName( + constructorParameter.getType(), + constructorAccessor.getSimpleName() + ) ); + } + + return new ConstructorAccessor( parameterBindings, constructorAccessors ); + } + else if ( constructorProperties.size() != constructorParameters.size() ) { + ctx.getMessager().printMessage( + method.getExecutable(), + GENERAL_CONSTRUCTOR_PROPERTIES_NOT_MATCHING_PARAMETERS, + type + ); + return null; + } + else { + Map constructorAccessors = new LinkedHashMap<>(); + List parameterBindings = new ArrayList<>( constructorProperties.size() ); + for ( int i = 0; i < constructorProperties.size(); i++ ) { + String parameterName = constructorProperties.get( i ); + Parameter constructorParameter = constructorParameters.get( i ); + Element parameterElement = constructorParameter.getElement(); + Accessor constructorAccessor = createConstructorAccessor( parameterElement, parameterName ); + constructorAccessors.put( + parameterName, + constructorAccessor + ); + parameterBindings.add( ParameterBinding.fromTypeAndName( + constructorParameter.getType(), + constructorAccessor.getSimpleName() + ) ); + } + + return new ConstructorAccessor( parameterBindings, constructorAccessors ); + } + } + + private Accessor createConstructorAccessor(Element element, String parameterName) { + String safeParameterName = Strings.getSafeVariableName( + parameterName, + existingVariableNames + ); + existingVariableNames.add( safeParameterName ); + return new ParameterElementAccessor( element, safeParameterName ); + } + + private List getArrayValues(AnnotationValue av) { + + if ( av.getValue() instanceof List ) { + List result = new ArrayList<>(); + for ( AnnotationValue v : getValueAsList( av ) ) { + Object value = v.getValue(); + if ( value instanceof String ) { + result.add( (String) value ); + } + else { + return null; + } + } + return result; + } + else { + return null; + } + } + + @SuppressWarnings("unchecked") + private List getValueAsList(AnnotationValue av) { + return (List) av.getValue(); } /** @@ -531,6 +803,7 @@ public class BeanMappingMethod extends NormalTypeMappingMethod { // remove the remaining name based properties for ( String handledTarget : handledTargets ) { unprocessedTargetProperties.remove( handledTarget ); + unprocessedConstructorProperties.remove( handledTarget ); unprocessedDefinedTargets.remove( handledTarget ); } @@ -672,7 +945,22 @@ public class BeanMappingMethod extends NormalTypeMappingMethod { // check the mapping options // its an ignored property mapping if ( mapping.isIgnored() ) { - propertyMapping = null; + if ( targetWriteAccessor != null && targetWriteAccessor.getAccessorType() == AccessorType.PARAMETER ) { + // Even though the property is ignored this is a constructor parameter. + // Therefore we have to initialize it + Type accessedType = ctx.getTypeFactory() + .getType( targetWriteAccessor.getAccessedType() ); + + propertyMapping = new JavaExpressionMappingBuilder() + .mappingContext( ctx ) + .sourceMethod( method ) + .javaExpression( accessedType.getNull() ) + .existingVariableNames( existingVariableNames ) + .target( targetPropertyName, targetReadAccessor, targetWriteAccessor ) + .dependsOn( mapping.getDependsOn() ) + .mirror( mapping.getMirror() ) + .build(); + } handledTargets.add( targetPropertyName ); } @@ -821,6 +1109,7 @@ public class BeanMappingMethod extends NormalTypeMappingMethod { String targetPropertyName = sourceRef.getDeepestPropertyName(); Accessor targetPropertyWriteAccessor = unprocessedTargetProperties.remove( targetPropertyName ); + unprocessedConstructorProperties.remove( targetPropertyName ); if ( targetPropertyWriteAccessor == null ) { // TODO improve error message ctx.getMessager() @@ -1056,6 +1345,18 @@ public class BeanMappingMethod extends NormalTypeMappingMethod { } } + private static class ConstructorAccessor { + private final List parameterBindings; + private final Map constructorAccessors; + + private ConstructorAccessor( + List parameterBindings, + Map constructorAccessors) { + this.parameterBindings = parameterBindings; + this.constructorAccessors = constructorAccessors; + } + } + private BeanMappingMethod(Method method, Collection existingVariableNames, List propertyMappings, @@ -1082,15 +1383,30 @@ public class BeanMappingMethod extends NormalTypeMappingMethod { // intialize constant mappings as all mappings, but take out the ones that can be contributed to a // parameter mapping. this.mappingsByParameter = new HashMap<>(); - this.constantMappings = new ArrayList<>( propertyMappings ); - for ( Parameter sourceParameter : getSourceParameters() ) { - ArrayList mappingsOfParameter = new ArrayList<>(); - mappingsByParameter.put( sourceParameter.getName(), mappingsOfParameter ); - for ( PropertyMapping mapping : propertyMappings ) { - if ( sourceParameter.getName().equals( mapping.getSourceBeanName() ) ) { - mappingsOfParameter.add( mapping ); - constantMappings.remove( mapping ); + this.constantMappings = new ArrayList<>( propertyMappings.size() ); + this.constructorMappingsByParameter = new LinkedHashMap<>(); + this.constructorConstantMappings = new ArrayList<>(); + Set sourceParameterNames = getSourceParameters().stream() + .map( Parameter::getName ) + .collect( Collectors.toSet() ); + for ( PropertyMapping mapping : propertyMappings ) { + if ( mapping.isConstructorMapping() ) { + if ( sourceParameterNames.contains( mapping.getSourceBeanName() ) ) { + constructorMappingsByParameter.computeIfAbsent( + mapping.getSourceBeanName(), + key -> new ArrayList<>() + ).add( mapping ); } + else { + constructorConstantMappings.add( mapping ); + } + } + else if ( sourceParameterNames.contains( mapping.getSourceBeanName() ) ) { + mappingsByParameter.computeIfAbsent( mapping.getSourceBeanName(), key -> new ArrayList<>() ) + .add( mapping ); + } + else { + constantMappings.add( mapping ); } } this.returnTypeToConstruct = returnTypeToConstruct; @@ -1100,15 +1416,28 @@ public class BeanMappingMethod extends NormalTypeMappingMethod { return constantMappings; } + public List getConstructorConstantMappings() { + return constructorConstantMappings; + } + public List propertyMappingsByParameter(Parameter parameter) { // issues: #909 and #1244. FreeMarker has problem getting values from a map when the search key is size or value - return mappingsByParameter.get( parameter.getName() ); + return mappingsByParameter.getOrDefault( parameter.getName(), Collections.emptyList() ); + } + + public List constructorPropertyMappingsByParameter(Parameter parameter) { + // issues: #909 and #1244. FreeMarker has problem getting values from a map when the search key is size or value + return constructorMappingsByParameter.getOrDefault( parameter.getName(), Collections.emptyList() ); } public Type getReturnTypeToConstruct() { return returnTypeToConstruct; } + public boolean hasConstructorMappings() { + return !constructorMappingsByParameter.isEmpty() || !constructorConstantMappings.isEmpty(); + } + public MethodReference getFinalizerMethod() { return finalizerMethod; } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/CollectionAssignmentBuilder.java b/processor/src/main/java/org/mapstruct/ap/internal/model/CollectionAssignmentBuilder.java index 04331c4e1..7ab9814e2 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/CollectionAssignmentBuilder.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/CollectionAssignmentBuilder.java @@ -130,7 +130,7 @@ public class CollectionAssignmentBuilder { CollectionMappingStrategyGem cms = method.getOptions().getMapper().getCollectionMappingStrategy(); boolean targetImmutable = cms == CollectionMappingStrategyGem.TARGET_IMMUTABLE || targetReadAccessor == null; - if ( targetAccessorType == AccessorType.SETTER || targetAccessorType == AccessorType.FIELD ) { + if ( targetAccessorType == AccessorType.SETTER || targetAccessorType.isFieldAssignment() ) { if ( result.isCallingUpdateMethod() && !targetImmutable ) { @@ -149,7 +149,7 @@ public class CollectionAssignmentBuilder { result, method.getThrownTypes(), factoryMethod, - targetAccessorType == AccessorType.FIELD, + targetAccessorType.isFieldAssignment(), targetType, true, nvpms == SET_TO_NULL && !targetType.isPrimitive(), @@ -165,7 +165,7 @@ public class CollectionAssignmentBuilder { nvcs, nvpms, ctx.getTypeFactory(), - targetAccessorType == AccessorType.FIELD + targetAccessorType.isFieldAssignment() ); } else if ( result.getType() == Assignment.AssignmentType.DIRECT || @@ -176,16 +176,18 @@ public class CollectionAssignmentBuilder { method.getThrownTypes(), targetType, ctx.getTypeFactory(), - targetAccessorType == AccessorType.FIELD + targetAccessorType.isFieldAssignment() ); } else { + //TODO init default value + // target accessor is setter, so wrap the setter in setter map/ collection handling result = new SetterWrapperForCollectionsAndMaps( result, method.getThrownTypes(), targetType, - targetAccessorType == AccessorType.FIELD + targetAccessorType.isFieldAssignment() ); } } @@ -203,7 +205,7 @@ public class CollectionAssignmentBuilder { result, method.getThrownTypes(), targetType, - targetAccessorType == AccessorType.FIELD + targetAccessorType.isFieldAssignment() ); } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/MethodReference.java b/processor/src/main/java/org/mapstruct/ap/internal/model/MethodReference.java index dc2bf5218..58c4ba890 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/MethodReference.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/MethodReference.java @@ -8,6 +8,7 @@ package org.mapstruct.ap.internal.model; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; import java.util.Set; @@ -57,6 +58,7 @@ public class MethodReference extends ModelElement implements Assignment { private final List parameterBindings; private final Parameter providingParameter; private final boolean isStatic; + private final boolean isConstructor; /** * Creates a new reference to the given method. @@ -91,6 +93,7 @@ public class MethodReference extends ModelElement implements Assignment { this.definingType = method.getDefiningType(); this.isStatic = method.isStatic(); this.name = method.getName(); + this.isConstructor = false; } private MethodReference(BuiltInMethod method, ConversionContext contextParam) { @@ -106,6 +109,7 @@ public class MethodReference extends ModelElement implements Assignment { this.parameterBindings = ParameterBinding.fromParameters( method.getParameters() ); this.isStatic = method.isStatic(); this.name = method.getName(); + this.isConstructor = false; } private MethodReference(String name, Type definingType, boolean isStatic) { @@ -121,6 +125,37 @@ public class MethodReference extends ModelElement implements Assignment { this.parameterBindings = Collections.emptyList(); this.providingParameter = null; this.isStatic = isStatic; + this.isConstructor = false; + } + + private MethodReference(Type definingType, List parameterBindings) { + this.name = null; + this.definingType = definingType; + this.sourceParameters = Collections.emptyList(); + this.returnType = null; + this.declaringMapper = null; + this.thrownTypes = Collections.emptyList(); + this.isUpdateMethod = false; + this.contextParam = null; + this.parameterBindings = parameterBindings; + this.providingParameter = null; + this.isStatic = false; + this.isConstructor = true; + + if ( parameterBindings.isEmpty() ) { + this.importTypes = Collections.emptySet(); + } + else { + Set imported = new LinkedHashSet<>(); + + for ( ParameterBinding binding : parameterBindings ) { + imported.add( binding.getType() ); + } + + imported.add( definingType ); + + this.importTypes = Collections.unmodifiableSet( imported ); + } } public MapperReference getDeclaringMapper() { @@ -271,6 +306,10 @@ public class MethodReference extends ModelElement implements Assignment { return isStatic; } + public boolean isConstructor() { + return isConstructor; + } + public List getParameterBindings() { return parameterBindings; } @@ -341,6 +380,10 @@ public class MethodReference extends ModelElement implements Assignment { return new MethodReference( methodName, null, false ); } + public static MethodReference forConstructorInvocation(Type type, List parameterBindings) { + return new MethodReference( type, parameterBindings ); + } + @Override public String toString() { String mapper = declaringMapper != null ? declaringMapper.getType().getName() : ""; diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/NormalTypeMappingMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/NormalTypeMappingMethod.java index 4687a626d..fcd8d70d5 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/NormalTypeMappingMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/NormalTypeMappingMethod.java @@ -42,6 +42,9 @@ public abstract class NormalTypeMappingMethod extends MappingMethod { types.addAll( getReturnType().getImplementationType().getImportTypes() ); } } + else if ( factoryMethod != null ) { + types.addAll( factoryMethod.getImportTypes() ); + } return types; } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/ObjectFactoryMethodResolver.java b/processor/src/main/java/org/mapstruct/ap/internal/model/ObjectFactoryMethodResolver.java index f4b39ab1a..34d294427 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/ObjectFactoryMethodResolver.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/ObjectFactoryMethodResolver.java @@ -67,16 +67,12 @@ public class ObjectFactoryMethodResolver { MappingBuilderContext ctx) { - MethodSelectors selectors = - new MethodSelectors( ctx.getTypeUtils(), ctx.getElementUtils(), ctx.getTypeFactory(), ctx.getMessager() ); - - List> matchingFactoryMethods = - selectors.getMatchingMethods( - method, - getAllAvailableMethods( method, ctx.getSourceModel() ), - java.util.Collections.emptyList(), - alternativeTarget, - SelectionCriteria.forFactoryMethods( selectionParameters ) ); + List> matchingFactoryMethods = getMatchingFactoryMethods( + method, + alternativeTarget, + selectionParameters, + ctx + ); if (matchingFactoryMethods.isEmpty()) { return null; @@ -94,6 +90,11 @@ public class ObjectFactoryMethodResolver { SelectedMethod matchingFactoryMethod = first( matchingFactoryMethods ); + return getFactoryMethodReference( method, matchingFactoryMethod, ctx ); + } + + public static MethodReference getFactoryMethodReference(Method method, + SelectedMethod matchingFactoryMethod, MappingBuilderContext ctx) { Parameter providingParameter = method.getContextProvidedMethods().getParameterForProvidedMethod( matchingFactoryMethod.getMethod() ); @@ -115,6 +116,22 @@ public class ObjectFactoryMethodResolver { } } + public static List> getMatchingFactoryMethods( Method method, + Type alternativeTarget, + SelectionParameters selectionParameters, + MappingBuilderContext ctx) { + + MethodSelectors selectors = + new MethodSelectors( ctx.getTypeUtils(), ctx.getElementUtils(), ctx.getTypeFactory(), ctx.getMessager() ); + + return selectors.getMatchingMethods( + method, + getAllAvailableMethods( method, ctx.getSourceModel() ), + java.util.Collections.emptyList(), + alternativeTarget, + SelectionCriteria.forFactoryMethods( selectionParameters ) ); + } + public static MethodReference getBuilderFactoryMethod(Method method, BuilderType builder ) { if ( builder == null ) { return null; diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/PropertyMapping.java b/processor/src/main/java/org/mapstruct/ap/internal/model/PropertyMapping.java index 1558fc7f8..571c3530f 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/PropertyMapping.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/PropertyMapping.java @@ -70,6 +70,7 @@ public class PropertyMapping extends ModelElement { private final Assignment assignment; private final Set dependsOn; private final Assignment defaultValueAssignment; + private final boolean constructorMapping; @SuppressWarnings("unchecked") private static class MappingBuilderBase> extends AbstractBaseBuilder { @@ -126,7 +127,7 @@ public class PropertyMapping extends ModelElement { } protected boolean isFieldAssignment() { - return targetWriteAccessorType == AccessorType.FIELD; + return targetWriteAccessorType.isFieldAssignment(); } } @@ -278,7 +279,8 @@ public class PropertyMapping extends ModelElement { targetType, assignment, dependsOn, - getDefaultValueAssignment( assignment ) + getDefaultValueAssignment( assignment ), + targetWriteAccessorType == AccessorType.PARAMETER ); } @@ -376,7 +378,7 @@ public class PropertyMapping extends ModelElement { Assignment result; - if ( targetAccessorType == AccessorType.SETTER || targetAccessorType == AccessorType.FIELD ) { + if ( targetAccessorType == AccessorType.SETTER || targetAccessorType.isFieldAssignment() ) { result = assignToPlainViaSetter( targetType, rightHandSide ); } else { @@ -473,6 +475,7 @@ public class PropertyMapping extends ModelElement { private Assignment assignToArray(Type targetType, Assignment rightHandSide) { Type arrayType = ctx.getTypeFactory().getType( Arrays.class ); + //TODO init default value return new ArrayCopyWrapper( rightHandSide, targetPropertyName, @@ -803,7 +806,7 @@ public class PropertyMapping extends ModelElement { if ( assignment != null ) { if ( targetWriteAccessor.getAccessorType() == AccessorType.SETTER || - targetWriteAccessor.getAccessorType() == AccessorType.FIELD ) { + targetWriteAccessor.getAccessorType().isFieldAssignment() ) { // target accessor is setter, so decorate assignment as setter if ( assignment.isCallingUpdateMethod() ) { @@ -874,7 +877,8 @@ public class PropertyMapping extends ModelElement { targetType, assignment, dependsOn, - null + null, + targetWriteAccessorType == AccessorType.PARAMETER ); } @@ -919,7 +923,7 @@ public class PropertyMapping extends ModelElement { Assignment assignment = new SourceRHS( javaExpression, null, existingVariableNames, "" ); if ( targetWriteAccessor.getAccessorType() == AccessorType.SETTER || - targetWriteAccessor.getAccessorType() == AccessorType.FIELD ) { + targetWriteAccessor.getAccessorType().isFieldAssignment() ) { // setter, so wrap in setter assignment = new SetterWrapper( assignment, method.getThrownTypes(), isFieldAssignment() ); } @@ -939,7 +943,8 @@ public class PropertyMapping extends ModelElement { targetType, assignment, dependsOn, - null + null, + targetWriteAccessorType == AccessorType.PARAMETER ); } @@ -947,18 +952,19 @@ public class PropertyMapping extends ModelElement { // Constructor for creating mappings of constant expressions. private PropertyMapping(String name, String targetWriteAccessorName, - ValueProvider targetReadAccessorProvider, - Type targetType, Assignment propertyAssignment, - Set dependsOn, Assignment defaultValueAssignment ) { + ValueProvider targetReadAccessorProvider, + Type targetType, Assignment propertyAssignment, + Set dependsOn, Assignment defaultValueAssignment, boolean constructorMapping) { this( name, null, targetWriteAccessorName, targetReadAccessorProvider, - targetType, propertyAssignment, dependsOn, defaultValueAssignment + targetType, propertyAssignment, dependsOn, defaultValueAssignment, + constructorMapping ); } private PropertyMapping(String name, String sourceBeanName, String targetWriteAccessorName, - ValueProvider targetReadAccessorProvider, Type targetType, - Assignment assignment, - Set dependsOn, Assignment defaultValueAssignment) { + ValueProvider targetReadAccessorProvider, Type targetType, + Assignment assignment, + Set dependsOn, Assignment defaultValueAssignment, boolean constructorMapping) { this.name = name; this.sourceBeanName = sourceBeanName; this.targetWriteAccessorName = targetWriteAccessorName; @@ -968,6 +974,7 @@ public class PropertyMapping extends ModelElement { this.assignment = assignment; this.dependsOn = dependsOn != null ? dependsOn : Collections.emptySet(); this.defaultValueAssignment = defaultValueAssignment; + this.constructorMapping = constructorMapping; } /** @@ -1001,6 +1008,10 @@ public class PropertyMapping extends ModelElement { return defaultValueAssignment; } + public boolean isConstructorMapping() { + return constructorMapping; + } + @Override public Set getImportTypes() { if ( defaultValueAssignment == null ) { 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 958725c96..2fd2b69ee 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 @@ -9,6 +9,7 @@ import java.util.List; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; +import javax.lang.model.element.Element; import javax.lang.model.element.VariableElement; import org.mapstruct.ap.internal.gem.ContextGem; @@ -23,6 +24,7 @@ import org.mapstruct.ap.internal.util.Collections; */ public class Parameter extends ModelElement { + private final Element element; private final String name; private final String originalName; private final Type type; @@ -32,8 +34,20 @@ public class Parameter extends ModelElement { private final boolean varArgs; + private Parameter(Element element, Type type, boolean varArgs) { + this.element = element; + this.name = element.getSimpleName().toString(); + this.originalName = name; + this.type = type; + this.mappingTarget = MappingTargetGem.instanceOn( element ) != null; + this.targetType = TargetTypeGem.instanceOn( element ) != null; + this.mappingContext = ContextGem.instanceOn( element ) != null; + this.varArgs = varArgs; + } + private Parameter(String name, Type type, boolean mappingTarget, boolean targetType, boolean mappingContext, boolean varArgs) { + this.element = null; this.name = name; this.originalName = name; this.type = type; @@ -47,6 +61,10 @@ public class Parameter extends ModelElement { this( name, type, false, false, false, false ); } + public Element getElement() { + return element; + } + public String getName() { return name; } @@ -115,11 +133,8 @@ public class Parameter extends ModelElement { public static Parameter forElementAndType(VariableElement element, Type parameterType, boolean isVarArgs) { return new Parameter( - element.getSimpleName().toString(), + element, parameterType, - MappingTargetGem.instanceOn( element ) != null, - TargetTypeGem.instanceOn( element ) != null, - ContextGem.instanceOn( element ) != null, isVarArgs ); } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/common/ParameterBinding.java b/processor/src/main/java/org/mapstruct/ap/internal/model/common/ParameterBinding.java index 5f807578e..36e4d3e86 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/common/ParameterBinding.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/common/ParameterBinding.java @@ -111,6 +111,17 @@ public class ParameterBinding { return result; } + public static ParameterBinding fromTypeAndName(Type parameterType, String parameterName) { + return new ParameterBinding( + parameterType, + parameterName, + false, + false, + false, + null + ); + } + /** * @param classTypeOf the type representing {@code Class} for the target type {@code X} * @return a parameter binding representing a target type parameter diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/common/Type.java b/processor/src/main/java/org/mapstruct/ap/internal/model/common/Type.java index 52a076270..0974fe347 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/common/Type.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/common/Type.java @@ -90,14 +90,16 @@ public class Type extends ModelElement implements Comparable { private List allMethods = null; private List allFields = null; + private List recordComponents = null; private List setters = null; private List adders = null; private List alternativeTargetAccessors = null; + private Map constructorAccessors = null; private Type boundingBase = null; - private Boolean hasEmptyAccessibleConstructor; + private Boolean hasAccessibleConstructor; private final Filters filters; @@ -290,6 +292,10 @@ public class Type extends ModelElement implements Comparable { return packageName != null && packageName.startsWith( "java." ); } + public boolean isRecord() { + return typeElement.getKind().name().equals( "RECORD" ); + } + /** * Whether this type is a sub-type of {@link java.util.stream.Stream}. * @@ -498,7 +504,7 @@ public class Type extends ModelElement implements Comparable { } } - Map recordAccessors = filters.recordsIn( typeElement ); + Map recordAccessors = filters.recordAccessorsIn( getRecordComponents() ); for ( Map.Entry recordEntry : recordAccessors.entrySet() ) { modifiableGetters.putIfAbsent( recordEntry.getKey(), recordEntry.getValue() ); } @@ -600,6 +606,14 @@ public class Type extends ModelElement implements Comparable { return result; } + public List getRecordComponents() { + if ( recordComponents == null ) { + recordComponents = filters.recordComponentsIn( typeElement ); + } + + return recordComponents; + } + private Type determinePreferredType(Accessor readAccessor) { if ( readAccessor != null ) { return typeFactory.getReturnType( (DeclaredType) typeMirror, readAccessor ); @@ -613,7 +627,7 @@ public class Type extends ModelElement implements Comparable { return parameter.getType(); } else if ( candidate.getAccessorType() == AccessorType.GETTER - || candidate.getAccessorType() == AccessorType.FIELD ) { + || candidate.getAccessorType().isFieldAssignment() ) { return typeFactory.getReturnType( (DeclaredType) typeMirror, candidate ); } return null; @@ -636,11 +650,12 @@ public class Type extends ModelElement implements Comparable { } private String getPropertyName(Accessor accessor ) { - if ( accessor.getAccessorType() == AccessorType.FIELD ) { - return accessorNaming.getPropertyName( (VariableElement) accessor.getElement() ); + Element accessorElement = accessor.getElement(); + if ( accessorElement instanceof ExecutableElement ) { + return accessorNaming.getPropertyName( (ExecutableElement) accessorElement ); } else { - return accessorNaming.getPropertyName( (ExecutableElement) accessor.getElement() ); + return accessor.getSimpleName(); } } @@ -1009,20 +1024,18 @@ public class Type extends ModelElement implements Comparable { return boundingBase; } - public boolean hasEmptyAccessibleConstructor() { - - if ( this.hasEmptyAccessibleConstructor == null ) { - hasEmptyAccessibleConstructor = false; + public boolean hasAccessibleConstructor() { + if ( hasAccessibleConstructor == null ) { + hasAccessibleConstructor = false; List constructors = ElementFilter.constructorsIn( typeElement.getEnclosedElements() ); for ( ExecutableElement constructor : constructors ) { - if ( !constructor.getModifiers().contains( Modifier.PRIVATE ) - && constructor.getParameters().isEmpty() ) { - hasEmptyAccessibleConstructor = true; + if ( !constructor.getModifiers().contains( Modifier.PRIVATE ) ) { + hasAccessibleConstructor = true; break; } } } - return hasEmptyAccessibleConstructor; + return hasAccessibleConstructor; } /** diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/common/TypeFactory.java b/processor/src/main/java/org/mapstruct/ap/internal/model/common/TypeFactory.java index 594c2c7f7..a74c02fd7 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/common/TypeFactory.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/common/TypeFactory.java @@ -50,7 +50,6 @@ import org.mapstruct.ap.internal.util.NativeTypes; import org.mapstruct.ap.internal.util.RoundContext; import org.mapstruct.ap.internal.util.Strings; import org.mapstruct.ap.internal.util.accessor.Accessor; -import org.mapstruct.ap.internal.util.accessor.AccessorType; import org.mapstruct.ap.spi.AstModifyingAnnotationProcessor; import org.mapstruct.ap.spi.BuilderInfo; import org.mapstruct.ap.spi.MoreThanOneBuilderCreationMethodException; @@ -360,7 +359,7 @@ public class TypeFactory { } public Parameter getSingleParameter(DeclaredType includingType, Accessor method) { - if ( method.getAccessorType() == AccessorType.FIELD ) { + if ( method.getAccessorType().isFieldAssignment() ) { return null; } ExecutableElement executable = (ExecutableElement) method.getElement(); @@ -376,11 +375,15 @@ public class TypeFactory { public List getParameters(DeclaredType includingType, Accessor accessor) { ExecutableElement method = (ExecutableElement) accessor.getElement(); - TypeMirror methodType = getMethodType( includingType, accessor.getElement() ); + return getParameters( includingType, method ); + } + + public List getParameters(DeclaredType includingType, ExecutableElement method) { + ExecutableType methodType = getMethodType( includingType, method ); if ( method == null || methodType.getKind() != TypeKind.EXECUTABLE ) { return new ArrayList<>(); } - return getParameters( (ExecutableType) methodType, method ); + return getParameters( methodType, method ); } public List getParameters(ExecutableType methodType, ExecutableElement method) { @@ -433,7 +436,7 @@ public class TypeFactory { } public List getThrownTypes(Accessor accessor) { - if (accessor.getAccessorType() == AccessorType.FIELD) { + if (accessor.getAccessorType().isFieldAssignment()) { return new ArrayList<>(); } return extractTypes( ( (ExecutableElement) accessor.getElement() ).getThrownTypes() ); diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/AccessorNamingUtils.java b/processor/src/main/java/org/mapstruct/ap/internal/util/AccessorNamingUtils.java index f060385a0..0f17946f7 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/AccessorNamingUtils.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/AccessorNamingUtils.java @@ -7,7 +7,6 @@ package org.mapstruct.ap.internal.util; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; -import javax.lang.model.element.VariableElement; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; @@ -68,10 +67,6 @@ public final class AccessorNamingUtils { return accessorNamingStrategy.getPropertyName( executable ); } - public String getPropertyName(VariableElement variable) { - return variable.getSimpleName().toString(); - } - /** * @param adderMethod the adder method * diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/Filters.java b/processor/src/main/java/org/mapstruct/ap/internal/util/Filters.java index 6981a1ebb..f4d52d709 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/Filters.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/Filters.java @@ -7,6 +7,7 @@ package org.mapstruct.ap.internal.util; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.Collection; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; @@ -23,7 +24,7 @@ import javax.lang.model.util.Types; import org.mapstruct.ap.internal.util.accessor.Accessor; import org.mapstruct.ap.internal.util.accessor.ExecutableElementAccessor; -import org.mapstruct.ap.internal.util.accessor.VariableElementAccessor; +import org.mapstruct.ap.internal.util.accessor.FieldElementAccessor; import static org.mapstruct.ap.internal.util.Collections.first; import static org.mapstruct.ap.internal.util.accessor.AccessorType.ADDER; @@ -75,13 +76,25 @@ public class Filters { .collect( Collectors.toCollection( LinkedList::new ) ); } - public Map recordsIn(TypeElement typeElement) { - if ( RECORD_COMPONENTS_METHOD == null || RECORD_COMPONENT_ACCESSOR_METHOD == null ) { + @SuppressWarnings("unchecked") + public List recordComponentsIn(TypeElement typeElement) { + if ( RECORD_COMPONENTS_METHOD == null ) { + return java.util.Collections.emptyList(); + } + + try { + return (List) RECORD_COMPONENTS_METHOD.invoke( typeElement ); + } + catch ( IllegalAccessException | InvocationTargetException e ) { + return java.util.Collections.emptyList(); + } + } + + public Map recordAccessorsIn(Collection recordComponents) { + if ( RECORD_COMPONENT_ACCESSOR_METHOD == null ) { return java.util.Collections.emptyMap(); } try { - @SuppressWarnings("unchecked") - List recordComponents = (List) RECORD_COMPONENTS_METHOD.invoke( typeElement ); Map recordAccessors = new LinkedHashMap<>(); for ( Element recordComponent : recordComponents ) { ExecutableElement recordExecutableElement = @@ -109,7 +122,7 @@ public class Filters { public List fieldsIn(List accessors) { return accessors.stream() .filter( Fields::isFieldAccessor ) - .map( VariableElementAccessor::new ) + .map( FieldElementAccessor::new ) .collect( Collectors.toCollection( LinkedList::new ) ); } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/Message.java b/processor/src/main/java/org/mapstruct/ap/internal/util/Message.java index e4b199adf..d2cb9857e 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/Message.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/Message.java @@ -110,12 +110,14 @@ public enum Message { GENERAL_ABSTRACT_RETURN_TYPE( "The return type %s is an abstract class or interface. Provide a non abstract / non interface result type or a factory method." ), GENERAL_AMBIGIOUS_MAPPING_METHOD( "Ambiguous mapping methods found for mapping %s to %s: %s." ), GENERAL_AMBIGIOUS_FACTORY_METHOD( "Ambiguous factory methods found for creating %s: %s." ), + GENERAL_AMBIGIOUS_CONSTRUCTORS( "Ambiguous constructors found for creating %s. Either declare parameterless constructor or annotate the default constructor with an annotation named @Default." ), + GENERAL_CONSTRUCTOR_PROPERTIES_NOT_MATCHING_PARAMETERS( "Incorrect @ConstructorProperties for %s. The size of the @ConstructorProperties does not match the number of constructor parameters" ), GENERAL_UNSUPPORTED_DATE_FORMAT_CHECK( "No dateFormat check is supported for types %s, %s" ), GENERAL_VALID_DATE( "Given date format \"%s\" is valid.", Diagnostic.Kind.NOTE ), GENERAL_INVALID_DATE( "Given date format \"%s\" is invalid. Message: \"%s\"." ), GENERAL_JODA_NOT_ON_CLASSPATH( "Cannot validate Joda dateformat, no Joda on classpath. Consider adding Joda to the annotation processorpath.", Diagnostic.Kind.WARNING ), GENERAL_NOT_ALL_FORGED_CREATED( "Internal Error in creation of Forged Methods, it was expected all Forged Methods to finished with creation, but %s did not" ), - GENERAL_NO_SUITABLE_CONSTRUCTOR( "%s does not have an accessible parameterless constructor." ), + GENERAL_NO_SUITABLE_CONSTRUCTOR( "%s does not have an accessible constructor." ), GENERAL_NO_QUALIFYING_METHOD( "No qualifying method found for qualifiers: %s and / or qualifying names: %s" ), BUILDER_MORE_THAN_ONE_BUILDER_CREATION_METHOD( "More than one builder creation method for \"%s\". Found methods: \"%s\". Builder will not be used. Consider implementing a custom BuilderProvider SPI.", Diagnostic.Kind.WARNING ), diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/ValueProvider.java b/processor/src/main/java/org/mapstruct/ap/internal/util/ValueProvider.java index 0f64c2b37..bbd73943d 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/ValueProvider.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/ValueProvider.java @@ -6,7 +6,6 @@ package org.mapstruct.ap.internal.util; import org.mapstruct.ap.internal.util.accessor.Accessor; -import org.mapstruct.ap.internal.util.accessor.AccessorType; /** * This a wrapper class which provides the value that needs to be used in the models. @@ -46,7 +45,7 @@ public class ValueProvider { return null; } String value = accessor.getSimpleName(); - if ( accessor.getAccessorType() != AccessorType.FIELD ) { + if ( !accessor.getAccessorType().isFieldAssignment() ) { value += "()"; } return new ValueProvider( value ); diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/Accessor.java b/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/Accessor.java index 917d1a216..314302a14 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/Accessor.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/Accessor.java @@ -8,9 +8,8 @@ package org.mapstruct.ap.internal.util.accessor; import java.util.Set; import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.VariableElement; import javax.lang.model.element.Modifier; -import javax.lang.model.element.Name; +import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeMirror; /** diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/AccessorType.java b/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/AccessorType.java index 9348de878..23448a9e9 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/AccessorType.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/AccessorType.java @@ -7,9 +7,14 @@ package org.mapstruct.ap.internal.util.accessor; public enum AccessorType { + PARAMETER, FIELD, GETTER, SETTER, ADDER, PRESENCE_CHECKER; + + public boolean isFieldAssignment() { + return this == FIELD || this == PARAMETER; + } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/VariableElementAccessor.java b/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/FieldElementAccessor.java similarity index 83% rename from processor/src/main/java/org/mapstruct/ap/internal/util/accessor/VariableElementAccessor.java rename to processor/src/main/java/org/mapstruct/ap/internal/util/accessor/FieldElementAccessor.java index 8b1b01dbd..517490053 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/VariableElementAccessor.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/FieldElementAccessor.java @@ -13,9 +13,9 @@ import javax.lang.model.type.TypeMirror; * * @author Filip Hrisafov */ -public class VariableElementAccessor extends AbstractAccessor { +public class FieldElementAccessor extends AbstractAccessor { - public VariableElementAccessor(VariableElement element) { + public FieldElementAccessor(VariableElement element) { super( element ); } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/ParameterElementAccessor.java b/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/ParameterElementAccessor.java new file mode 100644 index 000000000..e1877cb0b --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/ParameterElementAccessor.java @@ -0,0 +1,46 @@ +/* + * 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.internal.util.accessor; + +import javax.lang.model.element.Element; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeMirror; + +/** + * An {@link Accessor} that wraps a {@link VariableElement}. + * + * @author Filip Hrisafov + */ +public class ParameterElementAccessor extends AbstractAccessor { + + protected final String name; + + public ParameterElementAccessor(Element element, String name) { + super( element ); + this.name = name; + } + + @Override + public String getSimpleName() { + return name != null ? name : super.getSimpleName(); + } + + @Override + public TypeMirror getAccessedType() { + return element.asType(); + } + + @Override + public String toString() { + return element.toString(); + } + + @Override + public AccessorType getAccessorType() { + return AccessorType.PARAMETER; + } + +} diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/BeanMappingMethod.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/BeanMappingMethod.ftl index b8ffd0a17..45825b4fd 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/BeanMappingMethod.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/BeanMappingMethod.ftl @@ -25,7 +25,62 @@ <#if !existingInstanceMapping> - <@includeModel object=returnTypeToConstruct/> ${resultName} = <#if factoryMethod??><@includeModel object=factoryMethod targetType=returnTypeToConstruct/><#else>new <@includeModel object=returnTypeToConstruct/>(); + <#if hasConstructorMappings()> + <#if (sourceParameters?size > 1)> + <#list sourceParametersExcludingPrimitives as sourceParam> + <#if (constructorPropertyMappingsByParameter(sourceParam)?size > 0)> + <#list constructorPropertyMappingsByParameter(sourceParam) as propertyMapping> + <@includeModel object=propertyMapping.targetType /> ${propertyMapping.targetWriteAccessorName}; + + if ( ${sourceParam.name} != null ) { + <#list constructorPropertyMappingsByParameter(sourceParam) as propertyMapping> + <@includeModel object=propertyMapping existingInstanceMapping=existingInstanceMapping defaultValueAssignment=propertyMapping.defaultValueAssignment/> + + } + else { + <#list constructorPropertyMappingsByParameter(sourceParam) as propertyMapping> + ${propertyMapping.targetWriteAccessorName} = ${propertyMapping.targetType.null}; + + } + + + <#list sourcePrimitiveParameters as sourceParam> + <#if (constructorPropertyMappingsByParameter(sourceParam)?size > 0)> + <#list constructorPropertyMappingsByParameter(sourceParam) as propertyMapping> + <@includeModel object=propertyMapping.targetType /> ${propertyMapping.targetWriteAccessorName}; + <@includeModel object=propertyMapping existingInstanceMapping=existingInstanceMapping defaultValueAssignment=propertyMapping.defaultValueAssignment/> + + + + <#else> + <#list constructorPropertyMappingsByParameter(sourceParameters[0]) as propertyMapping> + <@includeModel object=propertyMapping.targetType /> ${propertyMapping.targetWriteAccessorName}; + + <#if mapNullToDefault>if ( ${sourceParameters[0].name} != null ) { + <#list constructorPropertyMappingsByParameter(sourceParameters[0]) as propertyMapping> + <@includeModel object=propertyMapping existingInstanceMapping=existingInstanceMapping defaultValueAssignment=propertyMapping.defaultValueAssignment/> + + <#if mapNullToDefault> + } + else { + <#list constructorPropertyMappingsByParameter(sourceParameters[0]) as propertyMapping> + ${propertyMapping.targetWriteAccessorName} = ${propertyMapping.targetType.null}; + + } + + + <#list constructorConstantMappings as constantMapping> + + <@compress single_line=true> + <@includeModel object=constantMapping.targetType /> <@includeModel object=constantMapping existingInstanceMapping=existingInstanceMapping/> + + + + + <@includeModel object=returnTypeToConstruct/> ${resultName} = <@includeModel object=factoryMethod targetType=returnTypeToConstruct/>; + <#else > + <@includeModel object=returnTypeToConstruct/> ${resultName} = <#if factoryMethod??><@includeModel object=factoryMethod targetType=returnTypeToConstruct/><#else>new <@includeModel object=returnTypeToConstruct/>(); + <#list beforeMappingReferencesWithMappingTarget as callback> diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/MethodReference.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/MethodReference.ftl index 30a830bc5..d0ee7bef3 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/MethodReference.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/MethodReference.ftl @@ -16,6 +16,8 @@ <#-- method is referenced java8 static method in the mapper to implement (interface) --> <#elseif static> <@includeModel object=definingType/>.<@methodCall/> + <#elseif constructor> + new <@includeModel object=definingType/><#if (parameterBindings?size > 0)>( <@arguments/> )<#else>() <#else> <@methodCall/> diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/AdderWrapper.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/AdderWrapper.ftl index 361fedaa5..9160a62d9 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/AdderWrapper.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/AdderWrapper.ftl @@ -11,7 +11,7 @@ <@lib.sourceLocalVarAssignment/> <@lib.handleSourceReferenceNullCheck> for ( <@includeModel object=adderType.typeBound/> ${sourceLoopVarName} : <#if sourceLocalVarName??>${sourceLocalVarName}<#else>${sourceReference} ) { - ${ext.targetBeanName}.${ext.targetWriteAccessorName}<@lib.handleWrite><@lib.handleAssignment/>; + <#if ext.targetBeanName?has_content>${ext.targetBeanName}.${ext.targetWriteAccessorName}<@lib.handleWrite><@lib.handleAssignment/>; } \ No newline at end of file diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/ArrayCopyWrapper.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/ArrayCopyWrapper.ftl index edb0d3cad..2ce8736f9 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/ArrayCopyWrapper.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/ArrayCopyWrapper.ftl @@ -10,6 +10,6 @@ <@lib.handleExceptions> <@lib.sourceLocalVarAssignment/> <@lib.handleSourceReferenceNullCheck> - ${ext.targetBeanName}.${ext.targetWriteAccessorName}<@lib.handleWrite>Arrays.copyOf( ${sourceLocalVarName}, ${sourceLocalVarName}.length ); + <#if ext.targetBeanName?has_content>${ext.targetBeanName}.${ext.targetWriteAccessorName}<@lib.handleWrite>Arrays.copyOf( ${sourceLocalVarName}, ${sourceLocalVarName}.length ); \ No newline at end of file diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/SetterWrapper.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/SetterWrapper.ftl index 613e8f6ab..c8f18b1c7 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/SetterWrapper.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/SetterWrapper.ftl @@ -10,6 +10,6 @@ <@lib.handleExceptions> <@lib.sourceLocalVarAssignment/> <@lib.handleSourceReferenceNullCheck> - ${ext.targetBeanName}.${ext.targetWriteAccessorName}<@lib.handleWrite><@lib.handleAssignment/>; + <#if ext.targetBeanName?has_content>${ext.targetBeanName}.${ext.targetWriteAccessorName}<@lib.handleWrite><@lib.handleAssignment/>; \ No newline at end of file diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/SetterWrapperForCollectionsAndMaps.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/SetterWrapperForCollectionsAndMaps.ftl index 122c76bc6..beb6bb1f3 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/SetterWrapperForCollectionsAndMaps.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/SetterWrapperForCollectionsAndMaps.ftl @@ -9,5 +9,5 @@ <#import "../macro/CommonMacros.ftl" as lib> <@lib.sourceLocalVarAssignment/> <@lib.handleExceptions> - ${ext.targetBeanName}.${ext.targetWriteAccessorName}<@lib.handleWrite><@lib.handleAssignment/>; + <#if ext.targetBeanName?has_content>${ext.targetBeanName}.${ext.targetWriteAccessorName}<@lib.handleWrite><@lib.handleAssignment/>; \ No newline at end of file diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/SetterWrapperForCollectionsAndMapsWithNullCheck.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/SetterWrapperForCollectionsAndMapsWithNullCheck.ftl index c26557af9..92e2e9d09 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/SetterWrapperForCollectionsAndMapsWithNullCheck.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/assignment/SetterWrapperForCollectionsAndMapsWithNullCheck.ftl @@ -16,8 +16,12 @@ --> <#macro callTargetWriteAccessor> <@lib.handleLocalVarNullCheck needs_explicit_local_var=directAssignment> - ${ext.targetBeanName}.${ext.targetWriteAccessorName}<@lib.handleWrite><#if directAssignment><@wrapLocalVarInCollectionInitializer/><#else><@lib.handleWithAssignmentOrNullCheckVar/>; + <#if ext.targetBeanName?has_content>${ext.targetBeanName}.${ext.targetWriteAccessorName}<@lib.handleWrite><#if directAssignment><@wrapLocalVarInCollectionInitializer/><#else><@lib.handleWithAssignmentOrNullCheckVar/>; + <#if !ext.defaultValueAssignment?? && !sourcePresenceCheckerReference?? && !ext.targetBeanName?has_content>else {<#-- the opposite (defaultValueAssignment) case is handeld inside lib.handleLocalVarNullCheck --> + ${ext.targetWriteAccessorName}<@lib.handleWrite>null; + } + <#-- wraps the local variable in a collection initializer (new collection, or EnumSet.copyOf) diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/macro/CommonMacros.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/macro/CommonMacros.ftl index 96ecd953b..de456ca77 100644 --- a/processor/src/main/resources/org/mapstruct/ap/internal/model/macro/CommonMacros.ftl +++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/macro/CommonMacros.ftl @@ -40,7 +40,7 @@ } <#elseif setExplicitlyToDefault || setExplicitlyToNull> else { - ${ext.targetBeanName}.${ext.targetWriteAccessorName}<@lib.handleWrite><#if setExplicitlyToDefault><@lib.initTargetObject/><#else>null; + <#if ext.targetBeanName?has_content>${ext.targetBeanName}.${ext.targetWriteAccessorName}<@lib.handleWrite><#if setExplicitlyToDefault><@lib.initTargetObject/><#else>null; } diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1242/Issue1242Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1242/Issue1242Test.java index 6cf0ba401..0cf59e726 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1242/Issue1242Test.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1242/Issue1242Test.java @@ -65,12 +65,7 @@ public class Issue1242Test { ".lang.Class clazz), org.mapstruct.ap.test.bugs._1242" + ".TargetB org.mapstruct.ap.test.bugs._1242.TargetFactories.createTargetB(@TargetType java.lang" + ".Class clazz), org.mapstruct.ap.test.bugs._1242" + - ".TargetB org.mapstruct.ap.test.bugs._1242.TargetFactories.createTargetB()."), - @Diagnostic(type = ErroneousIssue1242MapperMultipleSources.class, - kind = javax.tools.Diagnostic.Kind.ERROR, - line = 20, - message = "org.mapstruct.ap.test.bugs._1242.TargetB does not have an accessible parameterless " + - "constructor.") + ".TargetB org.mapstruct.ap.test.bugs._1242.TargetFactories.createTargetB().") }) public void ambiguousMethodErrorForTwoFactoryMethodsWithSourceParam() { } diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1283/Issue1283Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1283/Issue1283Test.java index 4d875017f..b4ce1447f 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1283/Issue1283Test.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1283/Issue1283Test.java @@ -33,8 +33,7 @@ public class Issue1283Test { @Diagnostic(type = ErroneousInverseTargetHasNoSuitableConstructorMapper.class, kind = javax.tools.Diagnostic.Kind.ERROR, line = 22L, - message = "org.mapstruct.ap.test.bugs._1283.Source does not have an accessible parameterless " + - "constructor." + message = "org.mapstruct.ap.test.bugs._1283.Source does not have an accessible constructor." ) } ) @@ -49,8 +48,7 @@ public class Issue1283Test { @Diagnostic(type = ErroneousTargetHasNoSuitableConstructorMapper.class, kind = javax.tools.Diagnostic.Kind.ERROR, line = 18L, - message = "org.mapstruct.ap.test.bugs._1283.Source does not have an accessible parameterless " + - "constructor." + message = "org.mapstruct.ap.test.bugs._1283.Source does not have an accessible constructor." ) } ) diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1283/Source.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1283/Source.java index a34c8d4a5..227296ad2 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1283/Source.java +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1283/Source.java @@ -12,7 +12,7 @@ public class Source { private String source; - public Source(String source) { + private Source(String source) { this.source = source; } diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/ConstructorProperties.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/ConstructorProperties.java new file mode 100644 index 000000000..affbded7f --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/ConstructorProperties.java @@ -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.constructor; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Filip Hrisafov + */ +@Documented +@Target(ElementType.CONSTRUCTOR) +@Retention(RetentionPolicy.SOURCE) +public @interface ConstructorProperties { + + String[] value(); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/Default.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/Default.java new file mode 100644 index 000000000..96b30c02e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/Default.java @@ -0,0 +1,21 @@ +/* + * 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.constructor; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Filip Hrisafov + */ +@Documented +@Target(ElementType.CONSTRUCTOR) +@Retention(RetentionPolicy.SOURCE) +public @interface Default { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/Person.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/Person.java new file mode 100644 index 000000000..8fc9e7ce3 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/Person.java @@ -0,0 +1,55 @@ +/* + * 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.constructor; + +import java.util.List; + +/** + * @author Filip Hrisafov + */ +public class Person { + + private final String name; + private final int age; + private final String job; + private final String city; + private final String address; + private final List children; + + public Person(String name, int age, String job, String city, String address, + List children) { + this.name = name; + this.age = age; + this.job = job; + this.city = city; + this.address = address; + this.children = children; + } + + public String getName() { + return name; + } + + public int getAge() { + return age; + } + + public String getJob() { + return job; + } + + public String getCity() { + return city; + } + + public String getAddress() { + return address; + } + + public List getChildren() { + return children; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/PersonDto.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/PersonDto.java new file mode 100644 index 000000000..0b2cba09c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/PersonDto.java @@ -0,0 +1,69 @@ +/* + * 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.constructor; + +import java.util.List; + +/** + * @author Filip Hrisafov + */ +public class PersonDto { + + private String name; + private int age; + private String job; + private String city; + private String address; + private List children; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public String getJob() { + return job; + } + + public void setJob(String job) { + this.job = job; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/constructorproperties/PersonWithConstructorProperties.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/constructorproperties/PersonWithConstructorProperties.java new file mode 100644 index 000000000..8419442cf --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/constructorproperties/PersonWithConstructorProperties.java @@ -0,0 +1,58 @@ +/* + * 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.constructor.constructorproperties; + +import java.util.List; + +import org.mapstruct.ap.test.constructor.ConstructorProperties; + +/** + * @author Filip Hrisafov + */ +public class PersonWithConstructorProperties { + + private final String name; + private final int age; + private final String job; + private final String city; + private final String address; + private final List children; + + @ConstructorProperties({"name", "age", "job", "city", "address", "children"}) + public PersonWithConstructorProperties(String var1, int var2, String var3, String var4, String var5, + List var6) { + this.name = var1; + this.age = var2; + this.job = var3; + this.city = var4; + this.address = var5; + this.children = var6; + } + + public String getName() { + return name; + } + + public int getAge() { + return age; + } + + public String getJob() { + return job; + } + + public String getCity() { + return city; + } + + public String getAddress() { + return address; + } + + public List getChildren() { + return children; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/constructorproperties/SimpleConstructorPropertiesMapper.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/constructorproperties/SimpleConstructorPropertiesMapper.java new file mode 100644 index 000000000..3d32d8b31 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/constructorproperties/SimpleConstructorPropertiesMapper.java @@ -0,0 +1,30 @@ +/* + * 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.constructor.constructorproperties; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ap.test.constructor.PersonDto; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface SimpleConstructorPropertiesMapper { + + SimpleConstructorPropertiesMapper INSTANCE = Mappers.getMapper( SimpleConstructorPropertiesMapper.class ); + + PersonWithConstructorProperties map(PersonDto dto); + + @Mapping(target = "age", constant = "25") + @Mapping(target = "job", constant = "Software Developer") + PersonWithConstructorProperties mapWithConstants(PersonDto dto); + + @Mapping(target = "age", expression = "java(25 - 5)") + @Mapping(target = "job", expression = "java(\"Software Developer\".toLowerCase())") + PersonWithConstructorProperties mapWithExpression(PersonDto dto); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/constructorproperties/SimpleConstructorPropertiesTest.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/constructorproperties/SimpleConstructorPropertiesTest.java new file mode 100644 index 000000000..b58d55f1e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/constructorproperties/SimpleConstructorPropertiesTest.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.constructor.constructorproperties; + +import java.util.Arrays; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mapstruct.ap.test.constructor.ConstructorProperties; +import org.mapstruct.ap.test.constructor.PersonDto; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.runner.AnnotationProcessorTestRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@WithClasses({ + ConstructorProperties.class, + PersonWithConstructorProperties.class, + PersonDto.class, + SimpleConstructorPropertiesMapper.class +}) +@RunWith(AnnotationProcessorTestRunner.class) +public class SimpleConstructorPropertiesTest { + + @Test + public void mapDefault() { + PersonDto source = new PersonDto(); + source.setName( "Bob" ); + source.setAge( 30 ); + source.setJob( "Software Engineer" ); + source.setCity( "Zurich" ); + source.setAddress( "Plaza 1" ); + source.setChildren( Arrays.asList( "Alice", "Tom" ) ); + + PersonWithConstructorProperties target = SimpleConstructorPropertiesMapper.INSTANCE.map( source ); + + assertThat( target.getName() ).isEqualTo( "Bob" ); + assertThat( target.getAge() ).isEqualTo( 30 ); + assertThat( target.getJob() ).isEqualTo( "Software Engineer" ); + assertThat( target.getCity() ).isEqualTo( "Zurich" ); + assertThat( target.getAddress() ).isEqualTo( "Plaza 1" ); + assertThat( target.getChildren() ).containsExactly( "Alice", "Tom" ); + } + + @Test + public void mapWithConstants() { + PersonDto source = new PersonDto(); + source.setName( "Bob" ); + source.setAge( 30 ); + source.setJob( "Software Engineer" ); + source.setCity( "Zurich" ); + source.setAddress( "Plaza 1" ); + source.setChildren( Arrays.asList( "Alice", "Tom" ) ); + + PersonWithConstructorProperties target = SimpleConstructorPropertiesMapper.INSTANCE.mapWithConstants( source ); + + assertThat( target.getName() ).isEqualTo( "Bob" ); + assertThat( target.getAge() ).isEqualTo( 25 ); + assertThat( target.getJob() ).isEqualTo( "Software Developer" ); + assertThat( target.getCity() ).isEqualTo( "Zurich" ); + assertThat( target.getAddress() ).isEqualTo( "Plaza 1" ); + assertThat( target.getChildren() ).containsExactly( "Alice", "Tom" ); + } + + @Test + public void mapWithExpressions() { + PersonDto source = new PersonDto(); + source.setName( "Bob" ); + source.setAge( 30 ); + source.setJob( "Software Engineer" ); + source.setCity( "Zurich" ); + source.setAddress( "Plaza 1" ); + source.setChildren( Arrays.asList( "Alice", "Tom" ) ); + + PersonWithConstructorProperties target = SimpleConstructorPropertiesMapper.INSTANCE.mapWithExpression( source ); + + assertThat( target.getName() ).isEqualTo( "Bob" ); + assertThat( target.getAge() ).isEqualTo( 20 ); + assertThat( target.getJob() ).isEqualTo( "software developer" ); + assertThat( target.getCity() ).isEqualTo( "Zurich" ); + assertThat( target.getAddress() ).isEqualTo( "Plaza 1" ); + assertThat( target.getChildren() ).containsExactly( "Alice", "Tom" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/defaultannotated/PersonWithDefaultAnnotatedConstructor.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/defaultannotated/PersonWithDefaultAnnotatedConstructor.java new file mode 100644 index 000000000..190ab818e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/defaultannotated/PersonWithDefaultAnnotatedConstructor.java @@ -0,0 +1,35 @@ +/* + * 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.constructor.defaultannotated; + +import org.mapstruct.ap.test.constructor.Default; + +/** + * @author Filip Hrisafov + */ +public class PersonWithDefaultAnnotatedConstructor { + + private final String name; + private final int age; + + public PersonWithDefaultAnnotatedConstructor(String name) { + this( name, -1 ); + } + + @Default + public PersonWithDefaultAnnotatedConstructor(String name, int age) { + this.name = name; + this.age = age; + } + + public String getName() { + return name; + } + + public int getAge() { + return age; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/defaultannotated/SimpleDefaultAnnotatedConstructorMapper.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/defaultannotated/SimpleDefaultAnnotatedConstructorMapper.java new file mode 100644 index 000000000..6934c34fe --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/defaultannotated/SimpleDefaultAnnotatedConstructorMapper.java @@ -0,0 +1,29 @@ +/* + * 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.constructor.defaultannotated; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ap.test.constructor.PersonDto; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface SimpleDefaultAnnotatedConstructorMapper { + + SimpleDefaultAnnotatedConstructorMapper INSTANCE = + Mappers.getMapper( SimpleDefaultAnnotatedConstructorMapper.class ); + + PersonWithDefaultAnnotatedConstructor map(PersonDto dto); + + @Mapping(target = "age", constant = "25") + PersonWithDefaultAnnotatedConstructor mapWithConstants(PersonDto dto); + + @Mapping(target = "age", expression = "java(25 - 5)") + PersonWithDefaultAnnotatedConstructor mapWithExpression(PersonDto dto); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/defaultannotated/SimpleDefaultAnnotatedConstructorTest.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/defaultannotated/SimpleDefaultAnnotatedConstructorTest.java new file mode 100644 index 000000000..ac447d787 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/defaultannotated/SimpleDefaultAnnotatedConstructorTest.java @@ -0,0 +1,80 @@ +/* + * 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.constructor.defaultannotated; + +import java.util.Arrays; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mapstruct.ap.test.constructor.Default; +import org.mapstruct.ap.test.constructor.PersonDto; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.runner.AnnotationProcessorTestRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@WithClasses({ + Default.class, + PersonWithDefaultAnnotatedConstructor.class, + PersonDto.class, + SimpleDefaultAnnotatedConstructorMapper.class +}) +@RunWith(AnnotationProcessorTestRunner.class) +public class SimpleDefaultAnnotatedConstructorTest { + + @Test + public void mapDefault() { + PersonDto source = new PersonDto(); + source.setName( "Bob" ); + source.setAge( 30 ); + source.setJob( "Software Engineer" ); + source.setCity( "Zurich" ); + source.setAddress( "Plaza 1" ); + source.setChildren( Arrays.asList( "Alice", "Tom" ) ); + + PersonWithDefaultAnnotatedConstructor target = SimpleDefaultAnnotatedConstructorMapper.INSTANCE.map( source ); + + assertThat( target.getName() ).isEqualTo( "Bob" ); + assertThat( target.getAge() ).isEqualTo( 30 ); + } + + @Test + public void mapWithConstants() { + PersonDto source = new PersonDto(); + source.setName( "Bob" ); + source.setAge( 30 ); + source.setJob( "Software Engineer" ); + source.setCity( "Zurich" ); + source.setAddress( "Plaza 1" ); + source.setChildren( Arrays.asList( "Alice", "Tom" ) ); + + PersonWithDefaultAnnotatedConstructor target = + SimpleDefaultAnnotatedConstructorMapper.INSTANCE.mapWithConstants( source ); + + assertThat( target.getName() ).isEqualTo( "Bob" ); + assertThat( target.getAge() ).isEqualTo( 25 ); + } + + @Test + public void mapWithExpressions() { + PersonDto source = new PersonDto(); + source.setName( "Bob" ); + source.setAge( 30 ); + source.setJob( "Software Engineer" ); + source.setCity( "Zurich" ); + source.setAddress( "Plaza 1" ); + source.setChildren( Arrays.asList( "Alice", "Tom" ) ); + + PersonWithDefaultAnnotatedConstructor target = + SimpleDefaultAnnotatedConstructorMapper.INSTANCE.mapWithExpression( source ); + + assertThat( target.getName() ).isEqualTo( "Bob" ); + assertThat( target.getAge() ).isEqualTo( 20 ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/erroneous/ErroneousAmbiguousConstructorsMapper.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/erroneous/ErroneousAmbiguousConstructorsMapper.java new file mode 100644 index 000000000..afa37ea77 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/erroneous/ErroneousAmbiguousConstructorsMapper.java @@ -0,0 +1,33 @@ +/* + * 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.constructor.erroneous; + +import org.mapstruct.Mapper; +import org.mapstruct.ap.test.constructor.PersonDto; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface ErroneousAmbiguousConstructorsMapper { + + PersonWithMultipleConstructors map(PersonDto dto); + + class PersonWithMultipleConstructors { + + private final String name; + private final int age; + + public PersonWithMultipleConstructors(String name) { + this( name, -1 ); + } + + public PersonWithMultipleConstructors(String name, int age) { + this.name = name; + this.age = age; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/erroneous/ErroneousConstructorPropertiesMapper.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/erroneous/ErroneousConstructorPropertiesMapper.java new file mode 100644 index 000000000..705421724 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/erroneous/ErroneousConstructorPropertiesMapper.java @@ -0,0 +1,31 @@ +/* + * 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.constructor.erroneous; + +import org.mapstruct.Mapper; +import org.mapstruct.ap.test.constructor.PersonDto; +import org.mapstruct.ap.test.constructor.ConstructorProperties; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface ErroneousConstructorPropertiesMapper { + + PersonWithIncorrectConstructorProperties map(PersonDto dto); + + class PersonWithIncorrectConstructorProperties { + + private final String name; + private final int age; + + @ConstructorProperties({ "name" }) + public PersonWithIncorrectConstructorProperties(String name, int age) { + this.name = name; + this.age = age; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/erroneous/ErroneousConstructorTest.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/erroneous/ErroneousConstructorTest.java new file mode 100644 index 000000000..eac6188ab --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/erroneous/ErroneousConstructorTest.java @@ -0,0 +1,59 @@ +/* + * 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.constructor.erroneous; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mapstruct.ap.test.constructor.ConstructorProperties; +import org.mapstruct.ap.test.constructor.PersonDto; +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.AnnotationProcessorTestRunner; + +/** + * @author Filip Hrisafov + */ +@RunWith(AnnotationProcessorTestRunner.class) +public class ErroneousConstructorTest { + + @Test + @WithClasses({ + ErroneousAmbiguousConstructorsMapper.class, + PersonDto.class + }) + @ExpectedCompilationOutcome(value = CompilationResult.FAILED, diagnostics = { + @Diagnostic( + type = ErroneousAmbiguousConstructorsMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 17, + message = "Ambiguous constructors found for creating org.mapstruct.ap.test.constructor.erroneous" + + ".ErroneousAmbiguousConstructorsMapper.PersonWithMultipleConstructors. Either declare parameterless " + + "constructor or annotate the default constructor with an annotation named @Default." + ) + }) + public void shouldUseMultipleConstructors() { + } + + @Test + @WithClasses({ + ConstructorProperties.class, + ErroneousConstructorPropertiesMapper.class, + PersonDto.class + }) + @ExpectedCompilationOutcome(value = CompilationResult.FAILED, diagnostics = { + @Diagnostic( + type = ErroneousConstructorPropertiesMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 18, + message = "Incorrect @ConstructorProperties for org.mapstruct.ap.test.constructor.erroneous" + + ".ErroneousConstructorPropertiesMapper.PersonWithIncorrectConstructorProperties. The size of the " + + "@ConstructorProperties does not match the number of constructor parameters") + }) + public void shouldNotCompileIfConstructorPropertiesDoesNotMatch() { + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/mixed/ConstructorMixedWithSettersMapper.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/mixed/ConstructorMixedWithSettersMapper.java new file mode 100644 index 000000000..02a6d9af6 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/mixed/ConstructorMixedWithSettersMapper.java @@ -0,0 +1,21 @@ +/* + * 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.constructor.mixed; + +import org.mapstruct.Mapper; +import org.mapstruct.ap.test.constructor.PersonDto; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface ConstructorMixedWithSettersMapper { + + ConstructorMixedWithSettersMapper INSTANCE = Mappers.getMapper( ConstructorMixedWithSettersMapper.class ); + + PersonMixed map(PersonDto dto); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/mixed/ConstructorMixedWithSettersTest.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/mixed/ConstructorMixedWithSettersTest.java new file mode 100644 index 000000000..2c24e33cb --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/mixed/ConstructorMixedWithSettersTest.java @@ -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.constructor.mixed; + +import java.util.Arrays; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mapstruct.ap.test.constructor.PersonDto; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.runner.AnnotationProcessorTestRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@WithClasses({ + PersonMixed.class, + PersonDto.class, + ConstructorMixedWithSettersMapper.class +}) +@RunWith(AnnotationProcessorTestRunner.class) +public class ConstructorMixedWithSettersTest { + + @Test + public void mapDefault() { + PersonDto source = new PersonDto(); + source.setName( "Bob" ); + source.setAge( 30 ); + source.setJob( "Software Engineer" ); + source.setCity( "Zurich" ); + source.setAddress( "Plaza 1" ); + source.setChildren( Arrays.asList( "Alice", "Tom" ) ); + + PersonMixed target = ConstructorMixedWithSettersMapper.INSTANCE.map( source ); + + assertThat( target.getName() ).isEqualTo( "Bob" ); + assertThat( target.getAge() ).isEqualTo( 30 ); + assertThat( target.getJob() ).isEqualTo( "Software Engineer" ); + assertThat( target.getCity() ).isEqualTo( "Zurich" ); + assertThat( target.getAddress() ).isEqualTo( "Plaza 1" ); + assertThat( target.getChildren() ).containsExactly( "Alice", "Tom" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/mixed/PersonMixed.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/mixed/PersonMixed.java new file mode 100644 index 000000000..deb79e84f --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/mixed/PersonMixed.java @@ -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.ap.test.constructor.mixed; + +import java.util.List; + +/** + * @author Filip Hrisafov + */ +public class PersonMixed { + + private final String name; + private final int age; + private final String job; + private String city; + private String address; + private List children; + + public PersonMixed(String name, int age, String job) { + this.name = name; + this.age = age; + this.job = job; + } + + public String getName() { + return name; + } + + public void setName(String name) { + throw new RuntimeException( "Method is here only to verify that MapStruct won't use it" ); + } + + public int getAge() { + return age; + } + + public String getJob() { + return job; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/ArtistToChartEntry.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/ArtistToChartEntry.java new file mode 100644 index 000000000..02a82ba8d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/ArtistToChartEntry.java @@ -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.ap.test.constructor.nestedsource; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import org.mapstruct.ap.test.constructor.nestedsource._target.ChartEntry; +import org.mapstruct.ap.test.constructor.nestedsource.source.Chart; +import org.mapstruct.ap.test.constructor.nestedsource.source.Song; +import org.mapstruct.factory.Mappers; + +/** + * @author Sjaak Derksen + */ +@Mapper +public interface ArtistToChartEntry { + + ArtistToChartEntry MAPPER = Mappers.getMapper( ArtistToChartEntry.class ); + + @Mappings({ + @Mapping(target = "chartName", source = "chart.name"), + @Mapping(target = "songTitle", source = "song.title"), + @Mapping(target = "artistName", source = "song.artist.name"), + @Mapping(target = "recordedAt", source = "song.artist.label.studio.name"), + @Mapping(target = "city", source = "song.artist.label.studio.city"), + @Mapping(target = "position", source = "position") + }) + ChartEntry map(Chart chart, Song song, Integer position); + + @Mappings({ + @Mapping(target = "chartName", ignore = true), + @Mapping(target = "songTitle", source = "title"), + @Mapping(target = "artistName", source = "artist.name"), + @Mapping(target = "recordedAt", source = "artist.label.studio.name"), + @Mapping(target = "city", source = "artist.label.studio.city"), + @Mapping(target = "position", ignore = true) + }) + ChartEntry map(Song song); + + @Mappings({ + @Mapping(target = "chartName", source = "name"), + @Mapping(target = "songTitle", ignore = true), + @Mapping(target = "artistName", ignore = true), + @Mapping(target = "recordedAt", ignore = true), + @Mapping(target = "city", ignore = true), + @Mapping(target = "position", ignore = true) + }) + ChartEntry map(Chart name); + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/ArtistToChartEntryComposedReverse.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/ArtistToChartEntryComposedReverse.java new file mode 100644 index 000000000..7fd9fbdb5 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/ArtistToChartEntryComposedReverse.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.constructor.nestedsource; + +import org.mapstruct.InheritInverseConfiguration; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import org.mapstruct.ap.test.constructor.nestedsource._target.ChartEntryComposed; +import org.mapstruct.ap.test.constructor.nestedsource._target.ChartEntryLabel; +import org.mapstruct.ap.test.constructor.nestedsource.source.Label; +import org.mapstruct.ap.test.constructor.nestedsource.source.Song; +import org.mapstruct.factory.Mappers; + +/** + * @author Sjaak Derksen + */ +@Mapper +public abstract class ArtistToChartEntryComposedReverse { + + public static final ArtistToChartEntryComposedReverse MAPPER = + Mappers.getMapper( ArtistToChartEntryComposedReverse.class ); + + @Mappings({ + @Mapping(target = "songTitle", source = "title"), + @Mapping(target = "artistName", source = "artist.name"), + @Mapping(target = "label", source = "artist.label"), + @Mapping(target = "position", ignore = true), + @Mapping(target = "chartName", ignore = true ) + }) + abstract ChartEntryComposed mapForward(Song song); + + @Mappings({ + @Mapping(target = "name", source = "name"), + @Mapping(target = "recordedAt", source = "studio.name"), + @Mapping(target = "city", source = "studio.city") + }) + abstract ChartEntryLabel mapForward(Label label); + + @InheritInverseConfiguration + @Mapping(target = "positions", ignore = true) + abstract Song mapReverse(ChartEntryComposed ce); + + @InheritInverseConfiguration + abstract Label mapReverse(ChartEntryLabel label); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/ArtistToChartEntryConfig.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/ArtistToChartEntryConfig.java new file mode 100644 index 000000000..c1d0d0522 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/ArtistToChartEntryConfig.java @@ -0,0 +1,28 @@ +/* + * 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.constructor.nestedsource; + +import org.mapstruct.MapperConfig; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.ap.test.constructor.nestedsource._target.BaseChartEntry; +import org.mapstruct.ap.test.constructor.nestedsource.source.Song; + +/** + * + * @author Sjaak Derksen + */ +@MapperConfig( unmappedTargetPolicy = ReportingPolicy.ERROR ) +public interface ArtistToChartEntryConfig { + + @Mappings({ + @Mapping(target = "songTitle", source = "title"), + @Mapping(target = "artistName", source = "artist.name"), + @Mapping(target = "chartName", ignore = true ) + }) + BaseChartEntry mapForwardConfig( Song song ); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/ArtistToChartEntryReverse.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/ArtistToChartEntryReverse.java new file mode 100644 index 000000000..193d4ea6e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/ArtistToChartEntryReverse.java @@ -0,0 +1,38 @@ +/* + * 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.constructor.nestedsource; + +import org.mapstruct.InheritInverseConfiguration; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import org.mapstruct.ap.test.constructor.nestedsource._target.ChartEntry; +import org.mapstruct.ap.test.constructor.nestedsource.source.Song; +import org.mapstruct.factory.Mappers; + +/** + * @author Sjaak Derksen + */ +@Mapper +public abstract class ArtistToChartEntryReverse { + + public static final ArtistToChartEntryReverse MAPPER = Mappers.getMapper( ArtistToChartEntryReverse.class ); + + @Mappings({ + + @Mapping(target = "songTitle", source = "title"), + @Mapping(target = "artistName", source = "artist.name"), + @Mapping(target = "recordedAt", source = "artist.label.studio.name"), + @Mapping(target = "city", source = "artist.label.studio.city"), + @Mapping(target = "position", ignore = true), + @Mapping(target = "chartName", ignore = true ) + }) + abstract ChartEntry mapForward(Song song); + + @InheritInverseConfiguration + @Mapping(target = "positions", ignore = true) + abstract Song mapReverse(ChartEntry ce); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/ArtistToChartEntryWithConfigReverse.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/ArtistToChartEntryWithConfigReverse.java new file mode 100644 index 000000000..7c21104a6 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/ArtistToChartEntryWithConfigReverse.java @@ -0,0 +1,37 @@ +/* + * 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.constructor.nestedsource; + +import org.mapstruct.InheritConfiguration; +import org.mapstruct.InheritInverseConfiguration; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import org.mapstruct.ap.test.constructor.nestedsource._target.ChartEntryWithBase; +import org.mapstruct.ap.test.constructor.nestedsource.source.Song; +import org.mapstruct.factory.Mappers; + +/** + * @author Sjaak Derksen + */ +@Mapper( config = ArtistToChartEntryConfig.class ) +public abstract class ArtistToChartEntryWithConfigReverse { + + public static final ArtistToChartEntryWithConfigReverse MAPPER = + Mappers.getMapper( ArtistToChartEntryWithConfigReverse.class ); + + @InheritConfiguration + @Mappings({ + @Mapping(target = "recordedAt", source = "artist.label.studio.name"), + @Mapping(target = "city", source = "artist.label.studio.city"), + @Mapping(target = "position", ignore = true) + }) + abstract ChartEntryWithBase mapForward(Song song); + + @InheritInverseConfiguration( name = "mapForward" ) + @Mapping(target = "positions", ignore = true) + abstract Song mapReverse(ChartEntryWithBase ce); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/ArtistToChartEntryWithFactoryReverse.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/ArtistToChartEntryWithFactoryReverse.java new file mode 100644 index 000000000..658383867 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/ArtistToChartEntryWithFactoryReverse.java @@ -0,0 +1,39 @@ +/* + * 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.constructor.nestedsource; + +import org.mapstruct.InheritInverseConfiguration; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import org.mapstruct.ap.test.constructor.nestedsource._target.ChartEntry; +import org.mapstruct.ap.test.constructor.nestedsource.source.Song; +import org.mapstruct.factory.Mappers; + +/** + * @author Sjaak Derksen + */ +@Mapper +public abstract class ArtistToChartEntryWithFactoryReverse { + + public static final ArtistToChartEntryWithFactoryReverse MAPPER + = Mappers.getMapper( ArtistToChartEntryWithFactoryReverse.class ); + + @Mappings({ + + @Mapping(target = "songTitle", source = "title"), + @Mapping(target = "artistName", source = "artist.name"), + @Mapping(target = "recordedAt", source = "artist.label.studio.name"), + @Mapping(target = "city", source = "artist.label.studio.city"), + @Mapping(target = "position", ignore = true), + @Mapping(target = "chartName", ignore = true) + }) + abstract ChartEntry mapForward(Song song); + + @InheritInverseConfiguration + @Mapping(target = "positions", ignore = true) + abstract Song mapReverse(ChartEntry ce); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/ArtistToChartEntryWithIgnoresReverse.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/ArtistToChartEntryWithIgnoresReverse.java new file mode 100644 index 000000000..4d1d35fbe --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/ArtistToChartEntryWithIgnoresReverse.java @@ -0,0 +1,41 @@ +/* + * 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.constructor.nestedsource; + +import org.mapstruct.InheritInverseConfiguration; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import org.mapstruct.ap.test.constructor.nestedsource._target.ChartEntry; +import org.mapstruct.ap.test.constructor.nestedsource.source.Song; +import org.mapstruct.factory.Mappers; + +/** + * @author Sjaak Derksen + */ +@Mapper +public abstract class ArtistToChartEntryWithIgnoresReverse { + + public static final ArtistToChartEntryWithIgnoresReverse MAPPER = + Mappers.getMapper( ArtistToChartEntryWithIgnoresReverse.class ); + + @Mappings({ + @Mapping(target = "songTitle", source = "title"), + @Mapping(target = "artistName", source = "artist.name"), + @Mapping(target = "recordedAt", source = "artist.label.studio.name"), + @Mapping(target = "city", source = "artist.label.studio.city"), + @Mapping(target = "position", ignore = true), + @Mapping(target = "chartName", ignore = true) + }) + abstract ChartEntry mapForward(Song song); + + @InheritInverseConfiguration + @Mappings({ + @Mapping(target = "positions", ignore = true), + @Mapping(target = "artist", ignore = true) + }) + abstract Song mapReverse(ChartEntry ce); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/ArtistToChartEntryWithMappingReverse.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/ArtistToChartEntryWithMappingReverse.java new file mode 100644 index 000000000..b6b4cac80 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/ArtistToChartEntryWithMappingReverse.java @@ -0,0 +1,59 @@ +/* + * 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.constructor.nestedsource; + +import org.mapstruct.InheritInverseConfiguration; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import org.mapstruct.ap.test.constructor.nestedsource._target.ChartEntryWithMapping; +import org.mapstruct.ap.test.constructor.nestedsource.source.Song; +import org.mapstruct.factory.Mappers; + +/** + * @author Sjaak Derksen + */ +@Mapper +public abstract class ArtistToChartEntryWithMappingReverse { + + public static final ArtistToChartEntryWithMappingReverse MAPPER = + Mappers.getMapper( ArtistToChartEntryWithMappingReverse.class ); + + @Mappings({ + @Mapping(target = "songTitle", source = "title"), + @Mapping(target = "artistId", source = "artist.name"), + @Mapping(target = "recordedAt", source = "artist.label.studio.name"), + @Mapping(target = "city", source = "artist.label.studio.city"), + @Mapping(target = "position", ignore = true), + @Mapping(target = "chartName", ignore = true) + }) + abstract ChartEntryWithMapping mapForward(Song song); + + @InheritInverseConfiguration + @Mapping(target = "positions", ignore = true) + abstract Song mapReverse(ChartEntryWithMapping ce); + + int mapArtistToArtistId(String in) { + + if ( "The Beatles".equals( in ) ) { + return 1; + } + else { + return -1; + } + } + + String mapArtistIdToArtist(int in) { + + if ( in == 1 ) { + return "The Beatles"; + } + else { + return "Unknown"; + } + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/NestedSourcePropertiesConstructorTest.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/NestedSourcePropertiesConstructorTest.java new file mode 100644 index 000000000..4fe43506a --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/NestedSourcePropertiesConstructorTest.java @@ -0,0 +1,132 @@ +/* + * 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.constructor.nestedsource; + +import java.util.Collections; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mapstruct.ap.test.constructor.nestedsource._target.ChartEntry; +import org.mapstruct.ap.test.constructor.nestedsource.source.Artist; +import org.mapstruct.ap.test.constructor.nestedsource.source.Chart; +import org.mapstruct.ap.test.constructor.nestedsource.source.Label; +import org.mapstruct.ap.test.constructor.nestedsource.source.Song; +import org.mapstruct.ap.test.constructor.nestedsource.source.Studio; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.runner.AnnotationProcessorTestRunner; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Sjaak Derksen + */ +@WithClasses({ Song.class, Artist.class, Chart.class, Label.class, Studio.class, ChartEntry.class }) +@IssueKey("73") +@RunWith(AnnotationProcessorTestRunner.class) +public class NestedSourcePropertiesConstructorTest { + + @Rule + public final GeneratedSource generatedSource = new GeneratedSource(); + + @Test + @WithClasses({ ArtistToChartEntry.class }) + public void shouldGenerateImplementationForPropertyNamesOnly() { + generatedSource.addComparisonToFixtureFor( ArtistToChartEntry.class ); + + Studio studio = new Studio( "Abbey Road", "London" ); + + Label label = new Label( "EMY", studio ); + + Artist artist = new Artist( "The Beatles", label ); + + Song song = new Song( artist, "A Hard Day's Night", Collections.emptyList() ); + + ChartEntry chartEntry = ArtistToChartEntry.MAPPER.map( song ); + + assertThat( chartEntry ).isNotNull(); + assertThat( chartEntry.getArtistName() ).isEqualTo( "The Beatles" ); + assertThat( chartEntry.getChartName() ).isNull(); + assertThat( chartEntry.getCity() ).isEqualTo( "London" ); + assertThat( chartEntry.getPosition() ).isEqualTo( 0 ); + assertThat( chartEntry.getRecordedAt() ).isEqualTo( "Abbey Road" ); + assertThat( chartEntry.getSongTitle() ).isEqualTo( "A Hard Day's Night" ); + } + + @Test + @WithClasses({ ArtistToChartEntry.class }) + public void shouldGenerateImplementationForMultipleParam() { + + Studio studio = new Studio( "Abbey Road", "London" ); + + Label label = new Label( "EMY", studio ); + + Artist artist = new Artist( "The Beatles", label ); + + Song song = new Song( artist, "A Hard Day's Night", Collections.emptyList() ); + + Chart chart = new Chart( "record-sales", "Billboard", null ); + + ChartEntry chartEntry = ArtistToChartEntry.MAPPER.map( chart, song, 1 ); + + assertThat( chartEntry ).isNotNull(); + assertThat( chartEntry.getArtistName() ).isEqualTo( "The Beatles" ); + assertThat( chartEntry.getChartName() ).isEqualTo( "Billboard" ); + assertThat( chartEntry.getCity() ).isEqualTo( "London" ); + assertThat( chartEntry.getPosition() ).isEqualTo( 1 ); + assertThat( chartEntry.getRecordedAt() ).isEqualTo( "Abbey Road" ); + assertThat( chartEntry.getSongTitle() ).isEqualTo( "A Hard Day's Night" ); + + chartEntry = ArtistToChartEntry.MAPPER.map( null, song, 10 ); + + assertThat( chartEntry ).isNotNull(); + assertThat( chartEntry.getArtistName() ).isEqualTo( "The Beatles" ); + assertThat( chartEntry.getChartName() ).isNull(); + assertThat( chartEntry.getCity() ).isEqualTo( "London" ); + assertThat( chartEntry.getPosition() ).isEqualTo( 10 ); + assertThat( chartEntry.getRecordedAt() ).isEqualTo( "Abbey Road" ); + assertThat( chartEntry.getSongTitle() ).isEqualTo( "A Hard Day's Night" ); + + chartEntry = ArtistToChartEntry.MAPPER.map( chart, null, 5 ); + + assertThat( chartEntry ).isNotNull(); + assertThat( chartEntry.getArtistName() ).isNull(); + assertThat( chartEntry.getChartName() ).isEqualTo( "Billboard" ); + assertThat( chartEntry.getCity() ).isNull(); + assertThat( chartEntry.getPosition() ).isEqualTo( 5 ); + assertThat( chartEntry.getRecordedAt() ).isNull(); + assertThat( chartEntry.getSongTitle() ).isNull(); + + chartEntry = ArtistToChartEntry.MAPPER.map( chart, song, null ); + + assertThat( chartEntry ).isNotNull(); + assertThat( chartEntry.getArtistName() ).isEqualTo( "The Beatles" ); + assertThat( chartEntry.getChartName() ).isEqualTo( "Billboard" ); + assertThat( chartEntry.getCity() ).isEqualTo( "London" ); + assertThat( chartEntry.getPosition() ).isEqualTo( 0 ); + assertThat( chartEntry.getRecordedAt() ).isEqualTo( "Abbey Road" ); + assertThat( chartEntry.getSongTitle() ).isEqualTo( "A Hard Day's Night" ); + } + + @Test + @WithClasses({ ArtistToChartEntry.class }) + public void shouldPickPropertyNameOverParameterName() { + + Chart chart = new Chart( "record-sales", "Billboard", null ); + + ChartEntry chartEntry = ArtistToChartEntry.MAPPER.map( chart ); + + assertThat( chartEntry ).isNotNull(); + assertThat( chartEntry.getArtistName() ).isNull(); + assertThat( chartEntry.getChartName() ).isEqualTo( "Billboard" ); + assertThat( chartEntry.getCity() ).isNull(); + assertThat( chartEntry.getPosition() ).isEqualTo( 0 ); + assertThat( chartEntry.getRecordedAt() ).isNull(); + assertThat( chartEntry.getSongTitle() ).isNull(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/ReversingNestedSourcePropertiesConstructorTest.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/ReversingNestedSourcePropertiesConstructorTest.java new file mode 100644 index 000000000..231a4fcad --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/ReversingNestedSourcePropertiesConstructorTest.java @@ -0,0 +1,222 @@ +/* + * 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.constructor.nestedsource; + +import java.util.Collections; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mapstruct.ap.test.constructor.nestedsource._target.BaseChartEntry; +import org.mapstruct.ap.test.constructor.nestedsource._target.ChartEntry; +import org.mapstruct.ap.test.constructor.nestedsource._target.ChartEntryComposed; +import org.mapstruct.ap.test.constructor.nestedsource._target.ChartEntryLabel; +import org.mapstruct.ap.test.constructor.nestedsource._target.ChartEntryWithBase; +import org.mapstruct.ap.test.constructor.nestedsource._target.ChartEntryWithMapping; +import org.mapstruct.ap.test.constructor.nestedsource.source.Artist; +import org.mapstruct.ap.test.constructor.nestedsource.source.Chart; +import org.mapstruct.ap.test.constructor.nestedsource.source.Label; +import org.mapstruct.ap.test.constructor.nestedsource.source.Song; +import org.mapstruct.ap.test.constructor.nestedsource.source.Studio; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.runner.AnnotationProcessorTestRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@IssueKey("73") +@WithClasses({ Song.class, Artist.class, Chart.class, Label.class, Studio.class, ChartEntry.class }) +@RunWith(AnnotationProcessorTestRunner.class) +public class ReversingNestedSourcePropertiesConstructorTest { + + @Test + @WithClasses({ ArtistToChartEntryReverse.class }) + public void shouldGenerateNestedReverse() { + + Song song1 = prepareSong(); + + ChartEntry chartEntry = ArtistToChartEntryReverse.MAPPER.mapForward( song1 ); + + assertThat( chartEntry ).isNotNull(); + assertThat( chartEntry.getArtistName() ).isEqualTo( "The Beatles" ); + assertThat( chartEntry.getChartName() ).isNull(); + assertThat( chartEntry.getCity() ).isEqualTo( "London" ); + assertThat( chartEntry.getPosition() ).isEqualTo( 0 ); + assertThat( chartEntry.getRecordedAt() ).isEqualTo( "Abbey Road" ); + assertThat( chartEntry.getSongTitle() ).isEqualTo( "A Hard Day's Night" ); + + // and now in reverse + Song song2 = ArtistToChartEntryReverse.MAPPER.mapReverse( chartEntry ); + + assertThat( song2 ).isNotNull(); + assertThat( song2.getArtist() ).isNotNull(); + assertThat( song2.getArtist().getName() ).isEqualTo( "The Beatles" ); + assertThat( song2.getArtist().getLabel() ).isNotNull(); + assertThat( song2.getArtist().getLabel().getName() ).isNull(); + assertThat( song2.getArtist().getLabel().getStudio() ).isNotNull(); + assertThat( song2.getArtist().getLabel().getStudio().getCity() ).isEqualTo( "London" ); + assertThat( song2.getArtist().getLabel().getStudio().getName() ).isEqualTo( "Abbey Road" ); + } + + @Test + @WithClasses({ ArtistToChartEntryWithIgnoresReverse.class }) + public void shouldIgnoreEverytingBelowArtist() { + + Song song1 = prepareSong(); + + ChartEntry chartEntry = ArtistToChartEntryWithIgnoresReverse.MAPPER.mapForward( song1 ); + + assertThat( chartEntry ).isNotNull(); + assertThat( chartEntry.getArtistName() ).isEqualTo( "The Beatles" ); + assertThat( chartEntry.getChartName() ).isNull(); + assertThat( chartEntry.getCity() ).isEqualTo( "London" ); + assertThat( chartEntry.getPosition() ).isEqualTo( 0 ); + assertThat( chartEntry.getRecordedAt() ).isEqualTo( "Abbey Road" ); + assertThat( chartEntry.getSongTitle() ).isEqualTo( "A Hard Day's Night" ); + + // and now in reverse + Song song2 = ArtistToChartEntryWithIgnoresReverse.MAPPER.mapReverse( chartEntry ); + + assertThat( song2 ).isNotNull(); + assertThat( song2.getArtist() ).isNull(); + } + + @Test + @WithClasses({ ArtistToChartEntryWithFactoryReverse.class }) + public void shouldGenerateNestedReverseWithFactory() { + + Song song1 = prepareSong(); + + ChartEntry chartEntry = ArtistToChartEntryWithFactoryReverse.MAPPER.mapForward( song1 ); + + assertThat( chartEntry ).isNotNull(); + assertThat( chartEntry.getArtistName() ).isEqualTo( "The Beatles" ); + assertThat( chartEntry.getChartName() ).isNull(); + assertThat( chartEntry.getCity() ).isEqualTo( "London" ); + assertThat( chartEntry.getPosition() ).isEqualTo( 0 ); + assertThat( chartEntry.getRecordedAt() ).isEqualTo( "Abbey Road" ); + assertThat( chartEntry.getSongTitle() ).isEqualTo( "A Hard Day's Night" ); + + // and now in reverse + Song song2 = ArtistToChartEntryWithFactoryReverse.MAPPER.mapReverse( chartEntry ); + + assertThat( song2 ).isNotNull(); + assertThat( song2.getArtist() ).isNotNull(); + assertThat( song2.getArtist().getName() ).isEqualTo( "The Beatles" ); + assertThat( song2.getArtist().getLabel() ).isNotNull(); + assertThat( song2.getArtist().getLabel().getName() ).isNull(); + assertThat( song2.getArtist().getLabel().getStudio() ).isNotNull(); + assertThat( song2.getArtist().getLabel().getStudio().getCity() ).isEqualTo( "London" ); + assertThat( song2.getArtist().getLabel().getStudio().getName() ).isEqualTo( "Abbey Road" ); + + } + + @Test + @WithClasses({ ArtistToChartEntryComposedReverse.class, ChartEntryComposed.class, ChartEntryLabel.class }) + public void shouldGenerateNestedComposedReverse() { + + Song song1 = prepareSong(); + + ChartEntryComposed chartEntry = ArtistToChartEntryComposedReverse.MAPPER.mapForward( song1 ); + + assertThat( chartEntry ).isNotNull(); + assertThat( chartEntry.getArtistName() ).isEqualTo( "The Beatles" ); + assertThat( chartEntry.getChartName() ).isNull(); + assertThat( chartEntry.getLabel().getName() ).isEqualTo( "EMY" ); + assertThat( chartEntry.getLabel().getCity() ).isEqualTo( "London" ); + assertThat( chartEntry.getLabel().getRecordedAt() ).isEqualTo( "Abbey Road" ); + assertThat( chartEntry.getPosition() ).isEqualTo( 0 ); + assertThat( chartEntry.getSongTitle() ).isEqualTo( "A Hard Day's Night" ); + + // and now in reverse + Song song2 = ArtistToChartEntryComposedReverse.MAPPER.mapReverse( chartEntry ); + + assertThat( song2 ).isNotNull(); + assertThat( song2.getArtist() ).isNotNull(); + assertThat( song2.getArtist().getName() ).isEqualTo( "The Beatles" ); + assertThat( song2.getArtist().getLabel() ).isNotNull(); + assertThat( song2.getArtist().getLabel().getName() ).isEqualTo( "EMY" ); + assertThat( song2.getArtist().getLabel().getStudio() ).isNotNull(); + assertThat( song2.getArtist().getLabel().getStudio().getCity() ).isEqualTo( "London" ); + assertThat( song2.getArtist().getLabel().getStudio().getName() ).isEqualTo( "Abbey Road" ); + } + + @Test + @WithClasses({ ArtistToChartEntryWithMappingReverse.class, ChartEntryWithMapping.class }) + public void shouldGenerateNestedWithMappingReverse() { + + Song song1 = prepareSong(); + + ChartEntryWithMapping chartEntry = ArtistToChartEntryWithMappingReverse.MAPPER.mapForward( song1 ); + + assertThat( chartEntry ).isNotNull(); + assertThat( chartEntry.getArtistId() ).isEqualTo( 1 ); + assertThat( chartEntry.getChartName() ).isNull(); + assertThat( chartEntry.getCity() ).isEqualTo( "London" ); + assertThat( chartEntry.getPosition() ).isEqualTo( 0 ); + assertThat( chartEntry.getRecordedAt() ).isEqualTo( "Abbey Road" ); + assertThat( chartEntry.getSongTitle() ).isEqualTo( "A Hard Day's Night" ); + + // and now in reverse + Song song2 = ArtistToChartEntryWithMappingReverse.MAPPER.mapReverse( chartEntry ); + + assertThat( song2 ).isNotNull(); + assertThat( song2.getArtist() ).isNotNull(); + assertThat( song2.getArtist().getName() ).isEqualTo( "The Beatles" ); + assertThat( song2.getArtist().getLabel() ).isNotNull(); + assertThat( song2.getArtist().getLabel().getName() ).isNull(); + assertThat( song2.getArtist().getLabel().getStudio() ).isNotNull(); + assertThat( song2.getArtist().getLabel().getStudio().getCity() ).isEqualTo( "London" ); + assertThat( song2.getArtist().getLabel().getStudio().getName() ).isEqualTo( "Abbey Road" ); + } + + @Test + @WithClasses({ + ArtistToChartEntryWithConfigReverse.class, + ArtistToChartEntryConfig.class, + BaseChartEntry.class, + ChartEntryWithBase.class + }) + public void shouldGenerateNestedWithConfigReverse() { + + Song song1 = prepareSong(); + + ChartEntryWithBase chartEntry = ArtistToChartEntryWithConfigReverse.MAPPER.mapForward( song1 ); + + assertThat( chartEntry ).isNotNull(); + assertThat( chartEntry.getArtistName() ).isEqualTo( "The Beatles" ); + assertThat( chartEntry.getChartName() ).isNull(); + assertThat( chartEntry.getCity() ).isEqualTo( "London" ); + assertThat( chartEntry.getPosition() ).isEqualTo( 0 ); + assertThat( chartEntry.getRecordedAt() ).isEqualTo( "Abbey Road" ); + assertThat( chartEntry.getSongTitle() ).isEqualTo( "A Hard Day's Night" ); + + // and now in reverse + Song song2 = ArtistToChartEntryWithConfigReverse.MAPPER.mapReverse( chartEntry ); + + assertThat( song2 ).isNotNull(); + assertThat( song2.getArtist() ).isNotNull(); + assertThat( song2.getArtist().getName() ).isEqualTo( "The Beatles" ); + assertThat( song2.getArtist().getLabel() ).isNotNull(); + assertThat( song2.getArtist().getLabel().getName() ).isNull(); + assertThat( song2.getArtist().getLabel().getStudio() ).isNotNull(); + assertThat( song2.getArtist().getLabel().getStudio().getCity() ).isEqualTo( "London" ); + assertThat( song2.getArtist().getLabel().getStudio().getName() ).isEqualTo( "Abbey Road" ); + } + + private Song prepareSong() { + Studio studio = new Studio( "Abbey Road", "London" ); + + Label label = new Label( "EMY", studio ); + + Artist artist = new Artist( "The Beatles", label ); + + return new Song( artist, "A Hard Day's Night", Collections.emptyList() ); + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/_target/AdderUsageObserver.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/_target/AdderUsageObserver.java new file mode 100644 index 000000000..ff21870b7 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/_target/AdderUsageObserver.java @@ -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.constructor.nestedsource._target; + +/** + * @author Sjaak Derksen + */ +public class AdderUsageObserver { + + private AdderUsageObserver() { + } + + private static boolean used = false; + + public static boolean isUsed() { + return used; + } + + public static void setUsed(boolean used) { + AdderUsageObserver.used = used; + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/_target/BaseChartEntry.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/_target/BaseChartEntry.java new file mode 100644 index 000000000..7b82c77b3 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/_target/BaseChartEntry.java @@ -0,0 +1,34 @@ +/* + * 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.constructor.nestedsource._target; + +/** + * @author Filip Hrisafov + */ +public class BaseChartEntry { + + private final String chartName; + private final String songTitle; + private final String artistName; + + public BaseChartEntry(String chartName, String songTitle, String artistName) { + this.chartName = chartName; + this.songTitle = songTitle; + this.artistName = artistName; + } + + public String getChartName() { + return chartName; + } + + public String getSongTitle() { + return songTitle; + } + + public String getArtistName() { + return artistName; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/_target/ChartEntry.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/_target/ChartEntry.java new file mode 100644 index 000000000..36d7e9560 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/_target/ChartEntry.java @@ -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.ap.test.constructor.nestedsource._target; + +/** + * @author Filip Hrisafov + */ +public class ChartEntry { + + private final String chartName; + private final String songTitle; + private final String artistName; + private final String recordedAt; + private final String city; + private final int position; + + public ChartEntry(String chartName, String songTitle, String artistName, String recordedAt, String city, + int position) { + this.chartName = chartName; + this.songTitle = songTitle; + this.artistName = artistName; + this.recordedAt = recordedAt; + this.city = city; + this.position = position; + } + + public String getChartName() { + return chartName; + } + + public String getSongTitle() { + return songTitle; + } + + public String getArtistName() { + return artistName; + } + + public String getRecordedAt() { + return recordedAt; + } + + public String getCity() { + return city; + } + + public int getPosition() { + return position; + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/_target/ChartEntryComposed.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/_target/ChartEntryComposed.java new file mode 100644 index 000000000..2543c7068 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/_target/ChartEntryComposed.java @@ -0,0 +1,58 @@ +/* + * 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.constructor.nestedsource._target; + +/** + * @author Filip Hrisafov + */ +public class ChartEntryComposed { + + private String chartName; + private String songTitle; + private String artistName; + private ChartEntryLabel label; + private int position; + + public String getChartName() { + return chartName; + } + + public void setChartName(String chartName) { + this.chartName = chartName; + } + + public String getSongTitle() { + return songTitle; + } + + public void setSongTitle(String songTitle) { + this.songTitle = songTitle; + } + + public String getArtistName() { + return artistName; + } + + public void setArtistName(String artistName) { + this.artistName = artistName; + } + + public ChartEntryLabel getLabel() { + return label; + } + + public void setLabel(ChartEntryLabel label) { + this.label = label; + } + + public int getPosition() { + return position; + } + + public void setPosition(int position) { + this.position = position; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/_target/ChartEntryLabel.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/_target/ChartEntryLabel.java new file mode 100644 index 000000000..b639b97b3 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/_target/ChartEntryLabel.java @@ -0,0 +1,34 @@ +/* + * 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.constructor.nestedsource._target; + +/** + * @author Filip Hrisafov + */ +public class ChartEntryLabel { + + private final String name; + private final String city; + private final String recordedAt; + + public ChartEntryLabel(String name, String city, String recordedAt) { + this.name = name; + this.city = city; + this.recordedAt = recordedAt; + } + + public String getName() { + return name; + } + + public String getCity() { + return city; + } + + public String getRecordedAt() { + return recordedAt; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/_target/ChartEntryWithBase.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/_target/ChartEntryWithBase.java new file mode 100644 index 000000000..4855080fb --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/_target/ChartEntryWithBase.java @@ -0,0 +1,36 @@ +/* + * 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.constructor.nestedsource._target; + +/** + * @author Filip Hrisafov + */ +public class ChartEntryWithBase extends BaseChartEntry { + + private final String recordedAt; + private final String city; + private final int position; + + public ChartEntryWithBase(String chartName, String songTitle, String artistName, String recordedAt, + String city, int position) { + super( chartName, songTitle, artistName ); + this.recordedAt = recordedAt; + this.city = city; + this.position = position; + } + + public String getRecordedAt() { + return recordedAt; + } + + public String getCity() { + return city; + } + + public int getPosition() { + return position; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/_target/ChartEntryWithMapping.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/_target/ChartEntryWithMapping.java new file mode 100644 index 000000000..f7ba366e1 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/_target/ChartEntryWithMapping.java @@ -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.ap.test.constructor.nestedsource._target; + +/** + * @author Filip Hrisafov + */ +public class ChartEntryWithMapping { + + private final String chartName; + private final String songTitle; + private final int artistId; + private final String recordedAt; + private final String city; + private final int position; + + public ChartEntryWithMapping(String chartName, String songTitle, int artistId, String recordedAt, + String city, int position) { + this.chartName = chartName; + this.songTitle = songTitle; + this.artistId = artistId; + this.recordedAt = recordedAt; + this.city = city; + this.position = position; + } + + public String getChartName() { + return chartName; + } + + public String getSongTitle() { + return songTitle; + } + + public int getArtistId() { + return artistId; + } + + public String getRecordedAt() { + return recordedAt; + } + + public String getCity() { + return city; + } + + public int getPosition() { + return position; + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/_target/ChartPositions.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/_target/ChartPositions.java new file mode 100644 index 000000000..a14e04ac7 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/_target/ChartPositions.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.ap.test.constructor.nestedsource._target; + +import java.util.Collections; +import java.util.List; + +/** + * @author Filip Hrisafov + */ +public class ChartPositions { + + private final List positions; + + public ChartPositions(List positions) { + this.positions = positions; + } + + public List getPositions() { + return Collections.unmodifiableList( positions ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/source/Artist.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/source/Artist.java new file mode 100644 index 000000000..def4ac697 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/source/Artist.java @@ -0,0 +1,29 @@ +/* + * 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.constructor.nestedsource.source; + +/** + * @author Filip Hrisafov + */ +public class Artist { + + private final String name; + private final Label label; + + public Artist(String name, Label label) { + this.name = name; + this.label = label; + } + + public String getName() { + return name; + } + + public Label getLabel() { + return label; + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/source/Chart.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/source/Chart.java new file mode 100644 index 000000000..064129fc0 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/source/Chart.java @@ -0,0 +1,35 @@ +/* + * 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.constructor.nestedsource.source; + +/** + * @author Filip Hrisafov + */ +public class Chart { + + private final String type; + private final String name; + private final Song song; + + public Chart(String type, String name, Song song) { + this.type = type; + this.name = name; + this.song = song; + } + + public String getType() { + return type; + } + + public String getName() { + return name; + } + + public Song getSong() { + return song; + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/source/Label.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/source/Label.java new file mode 100644 index 000000000..6c2a15bbf --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/source/Label.java @@ -0,0 +1,29 @@ +/* + * 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.constructor.nestedsource.source; + +/** + * @author Filip Hrisafov + */ +public class Label { + + private final String name; + private final Studio studio; + + public Label(String name, Studio studio) { + this.name = name; + this.studio = studio; + } + + public String getName() { + return name; + } + + public Studio getStudio() { + return studio; + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/source/Song.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/source/Song.java new file mode 100644 index 000000000..ac6ce2010 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/source/Song.java @@ -0,0 +1,37 @@ +/* + * 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.constructor.nestedsource.source; + +import java.util.List; + +/** + * @author Filip Hrisafov + */ +public class Song { + + private final Artist artist; + private final String title; + private final List positions; + + public Song(Artist artist, String title, List positions) { + this.artist = artist; + this.title = title; + this.positions = positions; + } + + public Artist getArtist() { + return artist; + } + + public String getTitle() { + return title; + } + + public List getPositions() { + return positions; + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/source/Studio.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/source/Studio.java new file mode 100644 index 000000000..db7bfd738 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedsource/source/Studio.java @@ -0,0 +1,29 @@ +/* + * 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.constructor.nestedsource.source; + +/** + * @author Filip Hrisafov + */ +public class Studio { + + private final String name; + private final String city; + + public Studio(String name, String city) { + this.name = name; + this.city = city; + } + + public String getName() { + return name; + } + + public String getCity() { + return city; + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedtarget/ChartEntryToArtist.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedtarget/ChartEntryToArtist.java new file mode 100644 index 000000000..9c1946fa2 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedtarget/ChartEntryToArtist.java @@ -0,0 +1,59 @@ +/* + * 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.constructor.nestedtarget; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.mapstruct.InheritInverseConfiguration; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import org.mapstruct.ap.test.constructor.nestedsource._target.ChartEntry; +import org.mapstruct.ap.test.constructor.nestedsource.source.Chart; +import org.mapstruct.factory.Mappers; + +/** + * @author Sjaak Derksen + */ +@Mapper +public abstract class ChartEntryToArtist { + + public static final ChartEntryToArtist MAPPER = Mappers.getMapper( ChartEntryToArtist.class ); + + @Mappings({ + @Mapping(target = "type", ignore = true), + @Mapping(target = "name", source = "chartName"), + @Mapping(target = "song.title", source = "songTitle"), + @Mapping(target = "song.artist.name", source = "artistName"), + @Mapping(target = "song.artist.label.studio.name", source = "recordedAt"), + @Mapping(target = "song.artist.label.studio.city", source = "city"), + @Mapping(target = "song.positions", source = "position") + }) + public abstract Chart map(ChartEntry chartEntry); + + @InheritInverseConfiguration + public abstract ChartEntry map(Chart chart); + + protected List mapPosition(Integer in) { + if ( in != null ) { + return new ArrayList<>( Arrays.asList( in ) ); + } + else { + return new ArrayList<>(); + } + } + + protected Integer mapPosition(List in) { + if ( in != null && !in.isEmpty() ) { + return in.get( 0 ); + } + else { + return null; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedtarget/NestedProductPropertiesConstructorTest.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedtarget/NestedProductPropertiesConstructorTest.java new file mode 100644 index 000000000..78d113a8c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/nestedtarget/NestedProductPropertiesConstructorTest.java @@ -0,0 +1,98 @@ +/* + * 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.constructor.nestedtarget; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mapstruct.ap.test.constructor.nestedsource._target.ChartEntry; +import org.mapstruct.ap.test.constructor.nestedsource.source.Artist; +import org.mapstruct.ap.test.constructor.nestedsource.source.Chart; +import org.mapstruct.ap.test.constructor.nestedsource.source.Label; +import org.mapstruct.ap.test.constructor.nestedsource.source.Song; +import org.mapstruct.ap.test.constructor.nestedsource.source.Studio; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.runner.AnnotationProcessorTestRunner; +import org.mapstruct.ap.testutil.runner.GeneratedSource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Sjaak Derksen + */ +@WithClasses({ + Song.class, + Artist.class, + Chart.class, + Label.class, + Studio.class, + ChartEntry.class, + ChartEntryToArtist.class, +}) +@IssueKey("73") +@RunWith(AnnotationProcessorTestRunner.class) +public class NestedProductPropertiesConstructorTest { + + @Rule + public GeneratedSource generatedSource = new GeneratedSource().addComparisonToFixtureFor( + ChartEntryToArtist.class + ); + + @Test + public void shouldMapNestedTarget() { + + ChartEntry chartEntry = new ChartEntry( + "US Billboard Hot Rock Songs", + "Purple Rain", + "Prince", + "Live, First Avenue, Minneapolis", + "Minneapolis", + 1 + ); + + Chart result = ChartEntryToArtist.MAPPER.map( chartEntry ); + + assertThat( result.getName() ).isEqualTo( "US Billboard Hot Rock Songs" ); + assertThat( result.getSong() ).isNotNull(); + assertThat( result.getSong().getArtist() ).isNotNull(); + assertThat( result.getSong().getTitle() ).isEqualTo( "Purple Rain" ); + assertThat( result.getSong().getArtist().getName() ).isEqualTo( "Prince" ); + assertThat( result.getSong().getArtist().getLabel() ).isNotNull(); + assertThat( result.getSong().getArtist().getLabel().getStudio() ).isNotNull(); + assertThat( result.getSong().getArtist().getLabel().getStudio().getName() ) + .isEqualTo( "Live, First Avenue, Minneapolis" ); + assertThat( result.getSong().getArtist().getLabel().getStudio().getCity() ) + .isEqualTo( "Minneapolis" ); + assertThat( result.getSong().getPositions() ).hasSize( 1 ); + assertThat( result.getSong().getPositions().get( 0 ) ).isEqualTo( 1 ); + + } + + @Test + public void shouldReverseNestedTarget() { + + ChartEntry chartEntry = new ChartEntry( + "US Billboard Hot Rock Songs", + "Purple Rain", + "Prince", + "Live, First Avenue, Minneapolis", + "Minneapolis", + 1 + ); + + Chart chart = ChartEntryToArtist.MAPPER.map( chartEntry ); + ChartEntry result = ChartEntryToArtist.MAPPER.map( chart ); + + assertThat( result ).isNotNull(); + assertThat( result.getArtistName() ).isEqualTo( "Prince" ); + assertThat( result.getChartName() ).isEqualTo( "US Billboard Hot Rock Songs" ); + assertThat( result.getCity() ).isEqualTo( "Minneapolis" ); + assertThat( result.getPosition() ).isEqualTo( 1 ); + assertThat( result.getRecordedAt() ).isEqualTo( "Live, First Avenue, Minneapolis" ); + assertThat( result.getSongTitle() ).isEqualTo( "Purple Rain" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/simple/SimpleConstructorMapper.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/simple/SimpleConstructorMapper.java new file mode 100644 index 000000000..2e162d2f2 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/simple/SimpleConstructorMapper.java @@ -0,0 +1,31 @@ +/* + * 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.constructor.simple; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ap.test.constructor.Person; +import org.mapstruct.ap.test.constructor.PersonDto; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface SimpleConstructorMapper { + + SimpleConstructorMapper INSTANCE = Mappers.getMapper( SimpleConstructorMapper.class ); + + Person map(PersonDto dto); + + @Mapping(target = "age", constant = "25") + @Mapping(target = "job", constant = "Software Developer") + Person mapWithConstants(PersonDto dto); + + @Mapping(target = "age", expression = "java(25 - 5)") + @Mapping(target = "job", expression = "java(\"Software Developer\".toLowerCase())") + Person mapWithExpression(PersonDto dto); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/simple/SimpleConstructorTest.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/simple/SimpleConstructorTest.java new file mode 100644 index 000000000..e24abb84b --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/simple/SimpleConstructorTest.java @@ -0,0 +1,89 @@ +/* + * 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.constructor.simple; + +import java.util.Arrays; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mapstruct.ap.test.constructor.Person; +import org.mapstruct.ap.test.constructor.PersonDto; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.runner.AnnotationProcessorTestRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@WithClasses({ + Person.class, + PersonDto.class, + SimpleConstructorMapper.class +}) +@RunWith(AnnotationProcessorTestRunner.class) +public class SimpleConstructorTest { + + @Test + public void mapDefault() { + PersonDto source = new PersonDto(); + source.setName( "Bob" ); + source.setAge( 30 ); + source.setJob( "Software Engineer" ); + source.setCity( "Zurich" ); + source.setAddress( "Plaza 1" ); + source.setChildren( Arrays.asList( "Alice", "Tom" ) ); + + Person target = SimpleConstructorMapper.INSTANCE.map( source ); + + assertThat( target.getName() ).isEqualTo( "Bob" ); + assertThat( target.getAge() ).isEqualTo( 30 ); + assertThat( target.getJob() ).isEqualTo( "Software Engineer" ); + assertThat( target.getCity() ).isEqualTo( "Zurich" ); + assertThat( target.getAddress() ).isEqualTo( "Plaza 1" ); + assertThat( target.getChildren() ).containsExactly( "Alice", "Tom" ); + } + + @Test + public void mapWithConstants() { + PersonDto source = new PersonDto(); + source.setName( "Bob" ); + source.setAge( 30 ); + source.setJob( "Software Engineer" ); + source.setCity( "Zurich" ); + source.setAddress( "Plaza 1" ); + source.setChildren( Arrays.asList( "Alice", "Tom" ) ); + + Person target = SimpleConstructorMapper.INSTANCE.mapWithConstants( source ); + + assertThat( target.getName() ).isEqualTo( "Bob" ); + assertThat( target.getAge() ).isEqualTo( 25 ); + assertThat( target.getJob() ).isEqualTo( "Software Developer" ); + assertThat( target.getCity() ).isEqualTo( "Zurich" ); + assertThat( target.getAddress() ).isEqualTo( "Plaza 1" ); + assertThat( target.getChildren() ).containsExactly( "Alice", "Tom" ); + } + + @Test + public void mapWithExpressions() { + PersonDto source = new PersonDto(); + source.setName( "Bob" ); + source.setAge( 30 ); + source.setJob( "Software Engineer" ); + source.setCity( "Zurich" ); + source.setAddress( "Plaza 1" ); + source.setChildren( Arrays.asList( "Alice", "Tom" ) ); + + Person target = SimpleConstructorMapper.INSTANCE.mapWithExpression( source ); + + assertThat( target.getName() ).isEqualTo( "Bob" ); + assertThat( target.getAge() ).isEqualTo( 20 ); + assertThat( target.getJob() ).isEqualTo( "software developer" ); + assertThat( target.getCity() ).isEqualTo( "Zurich" ); + assertThat( target.getAddress() ).isEqualTo( "Plaza 1" ); + assertThat( target.getChildren() ).containsExactly( "Alice", "Tom" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/unmapped/Order.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/unmapped/Order.java new file mode 100644 index 000000000..18fe9b101 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/unmapped/Order.java @@ -0,0 +1,30 @@ +/* + * 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.constructor.unmapped; + +import java.util.Date; + +/** + * @author Filip Hrisafov + */ +public class Order { + + private final String name; + private final Date time; + + public Order(String name, Date time) { + this.name = name; + this.time = time; + } + + public String getName() { + return name; + } + + public Date getTime() { + return time; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/unmapped/OrderDto.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/unmapped/OrderDto.java new file mode 100644 index 000000000..259cf8973 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/unmapped/OrderDto.java @@ -0,0 +1,22 @@ +/* + * 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.constructor.unmapped; + +/** + * @author Filip Hrisafov + */ +public class OrderDto { + + private final String name; + + public OrderDto(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/unmapped/UnmappedConstructorMapper.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/unmapped/UnmappedConstructorMapper.java new file mode 100644 index 000000000..8fcede06f --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/unmapped/UnmappedConstructorMapper.java @@ -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.constructor.unmapped; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface UnmappedConstructorMapper { + + UnmappedConstructorMapper INSTANCE = Mappers.getMapper( UnmappedConstructorMapper.class ); + + Order map(OrderDto dto); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/constructor/unmapped/UnmappedConstructorTest.java b/processor/src/test/java/org/mapstruct/ap/test/constructor/unmapped/UnmappedConstructorTest.java new file mode 100644 index 000000000..7a7f85904 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/constructor/unmapped/UnmappedConstructorTest.java @@ -0,0 +1,45 @@ +/* + * 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.constructor.unmapped; + +import org.junit.Test; +import org.junit.runner.RunWith; +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.AnnotationProcessorTestRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@WithClasses({ + Order.class, + OrderDto.class, + UnmappedConstructorMapper.class +}) +@RunWith(AnnotationProcessorTestRunner.class) +public class UnmappedConstructorTest { + + @Test + @ExpectedCompilationOutcome(value = CompilationResult.SUCCEEDED, + diagnostics = { + @Diagnostic(type = UnmappedConstructorMapper.class, + kind = javax.tools.Diagnostic.Kind.WARNING, + line = 19, + message = "Unmapped target property: \"time\".") + }) + public void shouldGenerateCompilableCodeForUnmappedConstructorProperties() { + OrderDto source = new OrderDto( "truck" ); + + Order target = UnmappedConstructorMapper.INSTANCE.map( source ); + + assertThat( target.getName() ).isEqualTo( "truck" ); + assertThat( target.getTime() ).isNull(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/erroneous/ambiguousannotatedfactorymethod/AmbiguousAnnotatedFactoryTest.java b/processor/src/test/java/org/mapstruct/ap/test/erroneous/ambiguousannotatedfactorymethod/AmbiguousAnnotatedFactoryTest.java index 561b53eff..94993e4a6 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/erroneous/ambiguousannotatedfactorymethod/AmbiguousAnnotatedFactoryTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/erroneous/ambiguousannotatedfactorymethod/AmbiguousAnnotatedFactoryTest.java @@ -36,12 +36,7 @@ public class AmbiguousAnnotatedFactoryTest { ".ambiguousannotatedfactorymethod.Foo foo), org.mapstruct.ap.test.erroneous" + ".ambiguousannotatedfactorymethod.Bar org.mapstruct.ap.test.erroneous" + ".ambiguousannotatedfactorymethod.AmbiguousBarFactory.createBar(org.mapstruct.ap.test.erroneous" + - ".ambiguousannotatedfactorymethod.Foo foo)."), - @Diagnostic(type = SourceTargetMapperAndBarFactory.class, - kind = javax.tools.Diagnostic.Kind.ERROR, - line = 22, - message = "org.mapstruct.ap.test.erroneous.ambiguousannotatedfactorymethod.Bar does not have an " + - "accessible parameterless constructor.") + ".ambiguousannotatedfactorymethod.Foo foo).") } ) diff --git a/processor/src/test/java/org/mapstruct/ap/test/erroneous/ambiguousfactorymethod/FactoryTest.java b/processor/src/test/java/org/mapstruct/ap/test/erroneous/ambiguousfactorymethod/FactoryTest.java index 82886eb03..e4e9ff81c 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/erroneous/ambiguousfactorymethod/FactoryTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/erroneous/ambiguousfactorymethod/FactoryTest.java @@ -37,12 +37,7 @@ public class FactoryTest { message = "Ambiguous factory methods found for creating org.mapstruct.ap.test.erroneous" + ".ambiguousfactorymethod.Bar: org.mapstruct.ap.test.erroneous.ambiguousfactorymethod.Bar " + "createBar(), org.mapstruct.ap.test.erroneous.ambiguousfactorymethod.Bar org.mapstruct.ap.test" + - ".erroneous.ambiguousfactorymethod.a.BarFactory.createBar()."), - @Diagnostic(type = SourceTargetMapperAndBarFactory.class, - kind = javax.tools.Diagnostic.Kind.ERROR, - line = 22, - message = "org.mapstruct.ap.test.erroneous.ambiguousfactorymethod.Bar does not have an accessible " + - "parameterless constructor.") + ".erroneous.ambiguousfactorymethod.a.BarFactory.createBar().") } ) diff --git a/processor/src/test/java/org/mapstruct/ap/test/nestedsourceproperties/ArtistToChartEntryErroneous.java b/processor/src/test/java/org/mapstruct/ap/test/nestedsourceproperties/ArtistToChartEntryErroneous.java index 5ed5ee57d..5f8be2876 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/nestedsourceproperties/ArtistToChartEntryErroneous.java +++ b/processor/src/test/java/org/mapstruct/ap/test/nestedsourceproperties/ArtistToChartEntryErroneous.java @@ -28,9 +28,22 @@ public interface ArtistToChartEntryErroneous { @Mapping(target = "city", ignore = true), @Mapping(target = "position", source = "position") } ) - ChartEntry forward(Integer position); + ChartEntry forward(ChartPosition position); @InheritInverseConfiguration - Integer reverse(ChartEntry position); + ChartPosition reverse(ChartEntry position); + + class ChartPosition { + + private final int position; + + private ChartPosition(int position) { + this.position = position; + } + + public int getPosition() { + return position; + } + } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/nestedsourceproperties/NestedSourcePropertiesTest.java b/processor/src/test/java/org/mapstruct/ap/test/nestedsourceproperties/NestedSourcePropertiesTest.java index 1d61b6e69..f9d5b11ee 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/nestedsourceproperties/NestedSourcePropertiesTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/nestedsourceproperties/NestedSourcePropertiesTest.java @@ -164,17 +164,18 @@ public class NestedSourcePropertiesTest { } @Test - @IssueKey( "838" ) + @IssueKey("838") @ExpectedCompilationOutcome( - value = CompilationResult.FAILED, - diagnostics = { - @Diagnostic( type = ArtistToChartEntryErroneous.class, - kind = javax.tools.Diagnostic.Kind.ERROR, - line = 34, - message = "java.lang.Integer does not have an accessible parameterless constructor." ) - } + value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic(type = ArtistToChartEntryErroneous.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 34, + message = "org.mapstruct.ap.test.nestedsourceproperties.ArtistToChartEntryErroneous.ChartPosition " + + "does not have an accessible constructor.") + } ) @WithClasses({ ArtistToChartEntryErroneous.class }) - public void inverseShouldRaiseErrorForEmptyConstructor() { + public void inverseShouldRaiseErrorForNotAccessibleConstructor() { } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/selection/resulttype/Citrus.java b/processor/src/test/java/org/mapstruct/ap/test/selection/resulttype/Citrus.java new file mode 100644 index 000000000..5dd69a83f --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/selection/resulttype/Citrus.java @@ -0,0 +1,21 @@ +/* + * 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.selection.resulttype; + +/** + * @author Filip Hrisafov + */ +public class Citrus extends Fruit implements IsFruit { + + public Citrus(String type) { + super( type ); + } + + @Override + public void setType(String type) { + throw new RuntimeException( "Not allowed to change citrus type" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/selection/resulttype/ErroneousResultTypeNoEmptyConstructorMapper.java b/processor/src/test/java/org/mapstruct/ap/test/selection/resulttype/ErroneousResultTypeNoAccessibleConstructorMapper.java similarity index 71% rename from processor/src/test/java/org/mapstruct/ap/test/selection/resulttype/ErroneousResultTypeNoEmptyConstructorMapper.java rename to processor/src/test/java/org/mapstruct/ap/test/selection/resulttype/ErroneousResultTypeNoAccessibleConstructorMapper.java index 0e8cb1138..40612fcf3 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/selection/resulttype/ErroneousResultTypeNoEmptyConstructorMapper.java +++ b/processor/src/test/java/org/mapstruct/ap/test/selection/resulttype/ErroneousResultTypeNoAccessibleConstructorMapper.java @@ -13,9 +13,16 @@ import org.mapstruct.Mapping; * @author Filip Hrisafov */ @Mapper -public interface ErroneousResultTypeNoEmptyConstructorMapper { +public interface ErroneousResultTypeNoAccessibleConstructorMapper { @BeanMapping(resultType = Banana.class) @Mapping(target = "type", ignore = true) Fruit map(FruitDto source); + + class Banana extends Fruit { + + private Banana(String type) { + super( type ); + } + } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/selection/resulttype/InheritanceSelectionTest.java b/processor/src/test/java/org/mapstruct/ap/test/selection/resulttype/InheritanceSelectionTest.java index efdea3c0e..5cd415e56 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/selection/resulttype/InheritanceSelectionTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/selection/resulttype/InheritanceSelectionTest.java @@ -21,9 +21,9 @@ import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutco import org.mapstruct.ap.testutil.runner.AnnotationProcessorTestRunner; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** - * * @author Sjaak Derksen */ @IssueKey("385") @@ -48,12 +48,7 @@ public class InheritanceSelectionTest { message = "Ambiguous factory methods found for creating org.mapstruct.ap.test.selection.resulttype" + ".Fruit: org.mapstruct.ap.test.selection.resulttype.Apple org.mapstruct.ap.test.selection" + ".resulttype.ConflictingFruitFactory.createApple(), org.mapstruct.ap.test.selection.resulttype" + - ".Banana org.mapstruct.ap.test.selection.resulttype.ConflictingFruitFactory.createBanana()."), - @Diagnostic(type = ErroneousFruitMapper.class, - kind = Kind.ERROR, - line = 23, - message = "org.mapstruct.ap.test.selection.resulttype.Fruit does not have an accessible parameterless" + - " constructor.") + ".Banana org.mapstruct.ap.test.selection.resulttype.ConflictingFruitFactory.createBanana().") } ) public void testForkedInheritanceHierarchyShouldResultInAmbigousMappingMethod() { @@ -61,22 +56,22 @@ public class InheritanceSelectionTest { @IssueKey("1283") @Test - @WithClasses({ ErroneousResultTypeNoEmptyConstructorMapper.class, Banana.class }) + @WithClasses({ ErroneousResultTypeNoAccessibleConstructorMapper.class }) @ExpectedCompilationOutcome( value = CompilationResult.FAILED, diagnostics = { - @Diagnostic(type = ErroneousResultTypeNoEmptyConstructorMapper.class, + @Diagnostic(type = ErroneousResultTypeNoAccessibleConstructorMapper.class, kind = Kind.ERROR, line = 18, - message = "org.mapstruct.ap.test.selection.resulttype.Banana does not have an accessible " + - "parameterless constructor.") + message = "org.mapstruct.ap.test.selection.resulttype" + + ".ErroneousResultTypeNoAccessibleConstructorMapper.Banana does not have an accessible constructor.") } ) - public void testResultTypeHasNoSuitableEmptyConstructor() { + public void testResultTypeHasNoSuitableAccessibleConstructor() { } @Test - @WithClasses( { ConflictingFruitFactory.class, ResultTypeSelectingFruitMapper.class, Banana.class } ) + @WithClasses({ ConflictingFruitFactory.class, ResultTypeSelectingFruitMapper.class, Banana.class }) public void testResultTypeBasedFactoryMethodSelection() { FruitDto fruitDto = new FruitDto( null ); @@ -88,7 +83,7 @@ public class InheritanceSelectionTest { @Test @IssueKey("434") - @WithClasses( { ResultTypeConstructingFruitMapper.class } ) + @WithClasses({ ResultTypeConstructingFruitMapper.class }) public void testResultTypeBasedConstructionOfResult() { FruitDto fruitDto = new FruitDto( null ); @@ -99,7 +94,7 @@ public class InheritanceSelectionTest { @Test @IssueKey("657") - @WithClasses( { ResultTypeConstructingFruitInterfaceMapper.class } ) + @WithClasses({ ResultTypeConstructingFruitInterfaceMapper.class }) public void testResultTypeBasedConstructionOfResultForInterface() { FruitDto fruitDto = new FruitDto( null ); @@ -143,7 +138,7 @@ public class InheritanceSelectionTest { @Test @IssueKey("433") - @WithClasses( { + @WithClasses({ FruitFamilyMapper.class, GoldenDeliciousDto.class, GoldenDelicious.class, @@ -151,11 +146,11 @@ public class InheritanceSelectionTest { AppleFamilyDto.class, AppleFactory.class, Banana.class - } ) + }) public void testShouldSelectResultTypeInCaseOfAmbiguity() { AppleFamilyDto appleFamilyDto = new AppleFamilyDto(); - appleFamilyDto.setApple( new AppleDto("AppleDto") ); + appleFamilyDto.setApple( new AppleDto( "AppleDto" ) ); AppleFamily result = FruitFamilyMapper.INSTANCE.map( appleFamilyDto ); assertThat( result ).isNotNull(); @@ -167,7 +162,7 @@ public class InheritanceSelectionTest { @Test @IssueKey("433") - @WithClasses( { + @WithClasses({ FruitFamilyMapper.class, GoldenDeliciousDto.class, GoldenDelicious.class, @@ -175,7 +170,7 @@ public class InheritanceSelectionTest { AppleFamilyDto.class, AppleFactory.class, Banana.class - } ) + }) public void testShouldSelectResultTypeInCaseOfAmbiguityForIterable() { List source = Arrays.asList( new AppleDto( "AppleDto" ) ); @@ -189,7 +184,7 @@ public class InheritanceSelectionTest { @Test @IssueKey("433") - @WithClasses( { + @WithClasses({ FruitFamilyMapper.class, GoldenDeliciousDto.class, GoldenDelicious.class, @@ -197,7 +192,7 @@ public class InheritanceSelectionTest { AppleFamilyDto.class, AppleFactory.class, Banana.class - } ) + }) public void testShouldSelectResultTypeInCaseOfAmbiguityForMap() { Map source = new HashMap(); @@ -214,4 +209,38 @@ public class InheritanceSelectionTest { assertThat( entry.getValue().getType() ).isEqualTo( "AppleDto" ); } + + @Test + @IssueKey("73") + @WithClasses({ + Citrus.class, + ResultTypeWithConstructorConstructingFruitInterfaceMapper.class + }) + public void testShouldUseConstructorFromResultTypeForInterface() { + FruitDto orange = new FruitDto( "orange" ); + IsFruit citrus = ResultTypeWithConstructorConstructingFruitInterfaceMapper.INSTANCE.map( orange ); + + assertThat( citrus ).isInstanceOf( Citrus.class ); + assertThat( citrus.getType() ).isEqualTo( "orange" ); + assertThatThrownBy( () -> citrus.setType( "lemon" ) ) + .hasMessage( "Not allowed to change citrus type" ); + assertThat( citrus.getType() ).isEqualTo( "orange" ); + } + + @Test + @IssueKey("73") + @WithClasses({ + Citrus.class, + ResultTypeWithConstructorConstructingFruitInterfaceMapper.class + }) + public void testShouldUseConstructorFromResultType() { + FruitDto lemon = new FruitDto( "lemon" ); + IsFruit citrus = ResultTypeWithConstructorConstructingFruitInterfaceMapper.INSTANCE.map( lemon ); + + assertThat( citrus ).isInstanceOf( Citrus.class ); + assertThat( citrus.getType() ).isEqualTo( "lemon" ); + assertThatThrownBy( () -> citrus.setType( "orange" ) ) + .hasMessage( "Not allowed to change citrus type" ); + assertThat( citrus.getType() ).isEqualTo( "lemon" ); + } } diff --git a/processor/src/test/java/org/mapstruct/ap/test/selection/resulttype/ResultTypeWithConstructorConstructingFruitInterfaceMapper.java b/processor/src/test/java/org/mapstruct/ap/test/selection/resulttype/ResultTypeWithConstructorConstructingFruitInterfaceMapper.java new file mode 100644 index 000000000..b1670772e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/selection/resulttype/ResultTypeWithConstructorConstructingFruitInterfaceMapper.java @@ -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.selection.resulttype; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface ResultTypeWithConstructorConstructingFruitInterfaceMapper { + + ResultTypeWithConstructorConstructingFruitInterfaceMapper INSTANCE = Mappers.getMapper( + ResultTypeWithConstructorConstructingFruitInterfaceMapper.class ); + + @BeanMapping(resultType = Citrus.class) + IsFruit map(FruitDto source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/selection/resulttype/ResultTypeWithConstructorConstructingFruitMapper.java b/processor/src/test/java/org/mapstruct/ap/test/selection/resulttype/ResultTypeWithConstructorConstructingFruitMapper.java new file mode 100644 index 000000000..7448cbb7a --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/selection/resulttype/ResultTypeWithConstructorConstructingFruitMapper.java @@ -0,0 +1,24 @@ +/* + * 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.selection.resulttype; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface ResultTypeWithConstructorConstructingFruitMapper { + + ResultTypeWithConstructorConstructingFruitMapper INSTANCE = Mappers.getMapper( + ResultTypeWithConstructorConstructingFruitMapper.class ); + + @BeanMapping(resultType = Citrus.class) + Fruit map(FruitDto source); + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/updatemethods/ErroneousOrganizationMapper2.java b/processor/src/test/java/org/mapstruct/ap/test/updatemethods/ErroneousOrganizationMapper2.java index 81eecea98..f57e4c95a 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/updatemethods/ErroneousOrganizationMapper2.java +++ b/processor/src/test/java/org/mapstruct/ap/test/updatemethods/ErroneousOrganizationMapper2.java @@ -34,4 +34,11 @@ public interface ErroneousOrganizationMapper2 { }) DepartmentEntity toDepartmentEntity(DepartmentDto dto); + class DepartmentEntity extends org.mapstruct.ap.test.updatemethods.DepartmentEntity { + + private DepartmentEntity() { + super( null ); + } + } + } diff --git a/processor/src/test/java/org/mapstruct/ap/test/updatemethods/UpdateMethodsTest.java b/processor/src/test/java/org/mapstruct/ap/test/updatemethods/UpdateMethodsTest.java index f04ddf338..1b93d04a5 100644 --- a/processor/src/test/java/org/mapstruct/ap/test/updatemethods/UpdateMethodsTest.java +++ b/processor/src/test/java/org/mapstruct/ap/test/updatemethods/UpdateMethodsTest.java @@ -175,8 +175,8 @@ public class UpdateMethodsTest { @Diagnostic(type = ErroneousOrganizationMapper2.class, kind = javax.tools.Diagnostic.Kind.ERROR, line = 35, - message = "org.mapstruct.ap.test.updatemethods.DepartmentEntity does not have an accessible " + - "parameterless constructor.") + message = "org.mapstruct.ap.test.updatemethods.ErroneousOrganizationMapper2.DepartmentEntity does not" + + " have an accessible constructor.") }) public void testShouldFailOnConstantMappingNoPropertyGetter() { diff --git a/processor/src/test/java/org/mapstruct/ap/testutil/runner/CompilingStatement.java b/processor/src/test/java/org/mapstruct/ap/testutil/runner/CompilingStatement.java index e8c09bec9..59916e981 100644 --- a/processor/src/test/java/org/mapstruct/ap/testutil/runner/CompilingStatement.java +++ b/processor/src/test/java/org/mapstruct/ap/testutil/runner/CompilingStatement.java @@ -308,9 +308,11 @@ abstract class CompilingStatement extends Statement { assertThat( actual.getLine() ).isIn( expected.getLine(), expected.getAlternativeLine() ); } else if ( expected.getLine() != null ) { - assertThat( actual.getLine() ).isEqualTo( expected.getLine() ); + assertThat( actual.getLine() ).as( actual.getMessage() ).isEqualTo( expected.getLine() ); } - assertThat( actual.getKind() ).isEqualTo( expected.getKind() ); + assertThat( actual.getKind() ) + .as( actual.getMessage() ) + .isEqualTo( expected.getKind() ); if ( expected.getMessage() != null && !expected.getMessage().isEmpty() ) { assertThat( actual.getMessage() ).describedAs( String.format( diff --git a/processor/src/test/resources/fixtures/org/mapstruct/ap/test/constructor/nestedsource/ArtistToChartEntryImpl.java b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/constructor/nestedsource/ArtistToChartEntryImpl.java new file mode 100644 index 000000000..bd8c81a5d --- /dev/null +++ b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/constructor/nestedsource/ArtistToChartEntryImpl.java @@ -0,0 +1,170 @@ +/* + * 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.constructor.nestedsource; + +import javax.annotation.processing.Generated; +import org.mapstruct.ap.test.constructor.nestedsource._target.ChartEntry; +import org.mapstruct.ap.test.constructor.nestedsource.source.Artist; +import org.mapstruct.ap.test.constructor.nestedsource.source.Chart; +import org.mapstruct.ap.test.constructor.nestedsource.source.Label; +import org.mapstruct.ap.test.constructor.nestedsource.source.Song; +import org.mapstruct.ap.test.constructor.nestedsource.source.Studio; + +@Generated( + value = "org.mapstruct.ap.MappingProcessor", + date = "2020-04-19T11:28:54+0200", + comments = "version: , compiler: javac, environment: Java 14.0.1 (Oracle Corporation)" +) +public class ArtistToChartEntryImpl implements ArtistToChartEntry { + + @Override + public ChartEntry map(Chart chart, Song song, Integer position) { + if ( chart == null && song == null && position == null ) { + return null; + } + + String chartName; + if ( chart != null ) { + chartName = chart.getName(); + } + else { + chartName = null; + } + String songTitle; + String artistName; + String recordedAt; + String city; + if ( song != null ) { + songTitle = song.getTitle(); + artistName = songArtistName( song ); + recordedAt = songArtistLabelStudioName( song ); + city = songArtistLabelStudioCity( song ); + } + else { + songTitle = null; + artistName = null; + recordedAt = null; + city = null; + } + int position1; + if ( position != null ) { + position1 = position; + } + else { + position1 = 0; + } + + ChartEntry chartEntry = new ChartEntry( chartName, songTitle, artistName, recordedAt, city, position1 ); + + return chartEntry; + } + + @Override + public ChartEntry map(Song song) { + if ( song == null ) { + return null; + } + + String songTitle; + String artistName; + String recordedAt; + String city; + + songTitle = song.getTitle(); + artistName = songArtistName( song ); + recordedAt = songArtistLabelStudioName( song ); + city = songArtistLabelStudioCity( song ); + + String chartName = null; + int position = 0; + + ChartEntry chartEntry = new ChartEntry( chartName, songTitle, artistName, recordedAt, city, position ); + + return chartEntry; + } + + @Override + public ChartEntry map(Chart name) { + if ( name == null ) { + return null; + } + + String chartName; + + chartName = name.getName(); + + String songTitle = null; + String artistName = null; + String recordedAt = null; + String city = null; + int position = 0; + + ChartEntry chartEntry = new ChartEntry( chartName, songTitle, artistName, recordedAt, city, position ); + + return chartEntry; + } + + private String songArtistName(Song song) { + if ( song == null ) { + return null; + } + Artist artist = song.getArtist(); + if ( artist == null ) { + return null; + } + String name = artist.getName(); + if ( name == null ) { + return null; + } + return name; + } + + private String songArtistLabelStudioName(Song song) { + if ( song == null ) { + return null; + } + Artist artist = song.getArtist(); + if ( artist == null ) { + return null; + } + Label label = artist.getLabel(); + if ( label == null ) { + return null; + } + Studio studio = label.getStudio(); + if ( studio == null ) { + return null; + } + String name = studio.getName(); + if ( name == null ) { + return null; + } + return name; + } + + private String songArtistLabelStudioCity(Song song) { + if ( song == null ) { + return null; + } + Artist artist = song.getArtist(); + if ( artist == null ) { + return null; + } + Label label = artist.getLabel(); + if ( label == null ) { + return null; + } + Studio studio = label.getStudio(); + if ( studio == null ) { + return null; + } + String city = studio.getCity(); + if ( city == null ) { + return null; + } + return city; + } +} diff --git a/processor/src/test/resources/fixtures/org/mapstruct/ap/test/constructor/nestedtarget/ChartEntryToArtistImpl.java b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/constructor/nestedtarget/ChartEntryToArtistImpl.java new file mode 100644 index 000000000..f5deb649b --- /dev/null +++ b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/constructor/nestedtarget/ChartEntryToArtistImpl.java @@ -0,0 +1,236 @@ +/* + * 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.constructor.nestedtarget; + +import java.util.List; +import javax.annotation.processing.Generated; +import org.mapstruct.ap.test.constructor.nestedsource._target.ChartEntry; +import org.mapstruct.ap.test.constructor.nestedsource.source.Artist; +import org.mapstruct.ap.test.constructor.nestedsource.source.Chart; +import org.mapstruct.ap.test.constructor.nestedsource.source.Label; +import org.mapstruct.ap.test.constructor.nestedsource.source.Song; +import org.mapstruct.ap.test.constructor.nestedsource.source.Studio; + +@Generated( + value = "org.mapstruct.ap.MappingProcessor", + date = "2020-04-19T14:54:28+0200", + comments = "version: , compiler: javac, environment: Java 14.0.1 (Oracle Corporation)" +) +public class ChartEntryToArtistImpl extends ChartEntryToArtist { + + @Override + public Chart map(ChartEntry chartEntry) { + if ( chartEntry == null ) { + return null; + } + + Song song; + String name; + + song = chartEntryToSong( chartEntry ); + name = chartEntry.getChartName(); + + String type = null; + + Chart chart = new Chart( type, name, song ); + + return chart; + } + + @Override + public ChartEntry map(Chart chart) { + if ( chart == null ) { + return null; + } + + String chartName; + String songTitle; + String artistName; + String recordedAt; + String city; + int position; + + chartName = chart.getName(); + songTitle = chartSongTitle( chart ); + artistName = chartSongArtistName( chart ); + recordedAt = chartSongArtistLabelStudioName( chart ); + city = chartSongArtistLabelStudioCity( chart ); + position = mapPosition( chartSongPositions( chart ) ); + + ChartEntry chartEntry = new ChartEntry( chartName, songTitle, artistName, recordedAt, city, position ); + + return chartEntry; + } + + protected Studio chartEntryToStudio(ChartEntry chartEntry) { + if ( chartEntry == null ) { + return null; + } + + String name; + String city; + + name = chartEntry.getRecordedAt(); + city = chartEntry.getCity(); + + Studio studio = new Studio( name, city ); + + return studio; + } + + protected Label chartEntryToLabel(ChartEntry chartEntry) { + if ( chartEntry == null ) { + return null; + } + + Studio studio; + + studio = chartEntryToStudio( chartEntry ); + + String name = null; + + Label label = new Label( name, studio ); + + return label; + } + + protected Artist chartEntryToArtist(ChartEntry chartEntry) { + if ( chartEntry == null ) { + return null; + } + + Label label; + String name; + + label = chartEntryToLabel( chartEntry ); + name = chartEntry.getArtistName(); + + Artist artist = new Artist( name, label ); + + return artist; + } + + protected Song chartEntryToSong(ChartEntry chartEntry) { + if ( chartEntry == null ) { + return null; + } + + Artist artist; + String title; + List positions; + + artist = chartEntryToArtist( chartEntry ); + title = chartEntry.getSongTitle(); + positions = mapPosition( chartEntry.getPosition() ); + + Song song = new Song( artist, title, positions ); + + return song; + } + + private String chartSongTitle(Chart chart) { + if ( chart == null ) { + return null; + } + Song song = chart.getSong(); + if ( song == null ) { + return null; + } + String title = song.getTitle(); + if ( title == null ) { + return null; + } + return title; + } + + private String chartSongArtistName(Chart chart) { + if ( chart == null ) { + return null; + } + Song song = chart.getSong(); + if ( song == null ) { + return null; + } + Artist artist = song.getArtist(); + if ( artist == null ) { + return null; + } + String name = artist.getName(); + if ( name == null ) { + return null; + } + return name; + } + + private String chartSongArtistLabelStudioName(Chart chart) { + if ( chart == null ) { + return null; + } + Song song = chart.getSong(); + if ( song == null ) { + return null; + } + Artist artist = song.getArtist(); + if ( artist == null ) { + return null; + } + Label label = artist.getLabel(); + if ( label == null ) { + return null; + } + Studio studio = label.getStudio(); + if ( studio == null ) { + return null; + } + String name = studio.getName(); + if ( name == null ) { + return null; + } + return name; + } + + private String chartSongArtistLabelStudioCity(Chart chart) { + if ( chart == null ) { + return null; + } + Song song = chart.getSong(); + if ( song == null ) { + return null; + } + Artist artist = song.getArtist(); + if ( artist == null ) { + return null; + } + Label label = artist.getLabel(); + if ( label == null ) { + return null; + } + Studio studio = label.getStudio(); + if ( studio == null ) { + return null; + } + String city = studio.getCity(); + if ( city == null ) { + return null; + } + return city; + } + + private List chartSongPositions(Chart chart) { + if ( chart == null ) { + return null; + } + Song song = chart.getSong(); + if ( song == null ) { + return null; + } + List positions = song.getPositions(); + if ( positions == null ) { + return null; + } + return positions; + } +}