Skip to content

Commit 2b2299a

Browse files
committed
mapstruct#73 Add support for using constructor arguments when instantiating mapping targets
By default the constructor argument names are used to extract the target properties. If a constructor is annotated with an annotation named `@ConstructorProperties` (from any package) then it would be used to extract the target properties. If a mapping target has a parameterless empty constructor it would be used to instantiate the target. When there are multiple constructors then an annotation named `@Default` (from any package) can be used to mark a constructor that should be used by default when instantiating the target. Supports mapping into Java 14 Records and Kotlin data classes out of the box
1 parent d6ff520 commit 2b2299a

File tree

105 files changed

+3905
-173
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

105 files changed

+3905
-173
lines changed

build-config/src/main/resources/build-config/checkstyle.xml

-3
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,6 @@
122122

123123
<!-- Checks for Size Violations. -->
124124
<!-- See http://checkstyle.sf.net/config_sizes.html -->
125-
<module name="MethodLength">
126-
<property name="max" value="200"/>
127-
</module>
128125
<module name="ParameterNumber">
129126
<property name="max" value="10"/>
130127
</module>

documentation/src/main/asciidoc/chapter-3-defining-a-mapper.asciidoc

+77
Original file line numberDiff line numberDiff line change
@@ -528,3 +528,80 @@ Otherwise, you would need to write a custom `BuilderProvider`
528528
====
529529
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.
530530
====
531+
532+
[[mapping-with-constructors]]
533+
=== Using Constructors
534+
535+
MapStruct supports using constructors for mapping target types.
536+
When doing a mapping MapStruct checks if there is a builder for the type being mapped.
537+
If there is no builder, then MapStruct looks for a single accessible constructor.
538+
When there are multiple constructors then the following is done to pick the one which should be used:
539+
540+
* If a parameterless constructor exists then it would be used to construct the object, and the other constructors will be ignored
541+
* If there are multiple constructors then the one annotated with annotation named `@Default` (from any package) will be used
542+
543+
When using a constructor then the names of the parameters of the constructor will be used and matched to the target properties.
544+
When the constructor has an annotation named `@ConstructorProperties` (from any package) then this annotation will be used to get the names of the parameters.
545+
546+
[NOTE]
547+
====
548+
When an object factory method or a method annotated with `@ObjectFactory` exists, it will take precedence over any constructor defined in the target.
549+
The target object constructor will not be used in that case.
550+
====
551+
552+
553+
.Person with constructor parameters
554+
====
555+
[source, java, linenums]
556+
[subs="verbatim,attributes"]
557+
----
558+
public class Person {
559+
560+
private final String name;
561+
private final String surname;
562+
563+
public Person(String name, String surname) {
564+
this.name = name;
565+
this.surname = surname;
566+
}
567+
}
568+
----
569+
====
570+
571+
.Person With Constructor Mapper definition
572+
====
573+
[source, java, linenums]
574+
[subs="verbatim,attributes"]
575+
----
576+
public interface PersonMapper {
577+
578+
Person map(PersonDto dto);
579+
}
580+
----
581+
====
582+
583+
.Generated mapper with constructor
584+
====
585+
[source, java, linenums]
586+
[subs="verbatim,attributes"]
587+
----
588+
// GENERATED CODE
589+
public class PersonMapperImpl implements PersonMapper {
590+
591+
public Person map(PersonDto dto) {
592+
if (dto == null) {
593+
return null;
594+
}
595+
596+
String name;
597+
String surname;
598+
name = dto.getName();
599+
surname = dto.getSurname();
600+
601+
Person person = new Person( name, surname );
602+
603+
return person;
604+
}
605+
}
606+
----
607+
====

documentation/src/main/asciidoc/chapter-5-data-type-conversions.asciidoc

+6
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,12 @@ During the generation of automatic sub-mapping methods <<shared-configurations>>
174174
Follow issue https://github.com/mapstruct/mapstruct/issues/1086[#1086] for more information.
175175
====
176176

177+
[NOTE]
178+
====
179+
Constructor properties of the target object are also considered as target properties.
180+
You can read more about that in <<mapping-with-constructors>>
181+
====
182+
177183
[[controlling-nested-bean-mappings]]
178184
=== Controlling nested bean mappings
179185

integrationtest/src/test/java/org/mapstruct/itest/tests/MavenIntegrationTest.java

+8
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,14 @@ void protobufBuilderTest() {
107107
void recordsTest() {
108108
}
109109

110+
@ProcessorTest(baseDir = "kotlinDataTest", processorTypes = {
111+
ProcessorTest.ProcessorType.JAVAC
112+
}, forkJvm = true)
113+
// We have to fork the jvm because there is an NPE in com.intellij.openapi.util.SystemInfo.getRtVersion
114+
// and the kotlin-maven-plugin uses that. See also https://youtrack.jetbrains.com/issue/IDEA-238907
115+
void kotlinDataTest() {
116+
}
117+
110118
@ProcessorTest(baseDir = "simpleTest")
111119
void simpleTest() {
112120
}

integrationtest/src/test/java/org/mapstruct/itest/testutil/extension/ProcessorInvocationInterceptor.java

+3
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ private void doExecute(ExtensionContext extensionContext) throws Exception {
7171
}
7272
else {
7373
verifier = new Verifier( destination.getCanonicalPath() );
74+
if ( processorTestContext.isForkJvm() ) {
75+
verifier.setForkJvm( true );
76+
}
7477
}
7578

7679
List<String> goals = new ArrayList<>( 3 );

integrationtest/src/test/java/org/mapstruct/itest/testutil/extension/ProcessorTest.java

+3
Original file line numberDiff line numberDiff line change
@@ -106,4 +106,7 @@ ProcessorType[] processorTypes() default {
106106
* @return the {@link CommandLineEnhancer} implementation. Must have a default constructor.
107107
*/
108108
Class<? extends CommandLineEnhancer> commandLineEnhancer() default CommandLineEnhancer.class;
109+
110+
boolean forkJvm() default false;
111+
109112
}

integrationtest/src/test/java/org/mapstruct/itest/testutil/extension/ProcessorTestContext.java

+8-1
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,16 @@ public class ProcessorTestContext {
1313
private final String baseDir;
1414
private final ProcessorTest.ProcessorType processor;
1515
private final Class<? extends ProcessorTest.CommandLineEnhancer> cliEnhancerClass;
16+
private final boolean forkJvm;
1617

1718
public ProcessorTestContext(String baseDir,
1819
ProcessorTest.ProcessorType processor,
19-
Class<? extends ProcessorTest.CommandLineEnhancer> cliEnhancerClass) {
20+
Class<? extends ProcessorTest.CommandLineEnhancer> cliEnhancerClass,
21+
boolean forkJvm) {
2022
this.baseDir = baseDir;
2123
this.processor = processor;
2224
this.cliEnhancerClass = cliEnhancerClass;
25+
this.forkJvm = forkJvm;
2326
}
2427

2528
public String getBaseDir() {
@@ -33,4 +36,8 @@ public ProcessorTest.ProcessorType getProcessor() {
3336
public Class<? extends ProcessorTest.CommandLineEnhancer> getCliEnhancerClass() {
3437
return cliEnhancerClass;
3538
}
39+
40+
public boolean isForkJvm() {
41+
return forkJvm;
42+
}
3643
}

integrationtest/src/test/java/org/mapstruct/itest/testutil/extension/ProcessorTestTemplateInvocationContextProvider.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContex
3434
.map( processorType -> new ProcessorTestTemplateInvocationContext( new ProcessorTestContext(
3535
processorTest.baseDir(),
3636
processorType,
37-
processorTest.commandLineEnhancer()
37+
processorTest.commandLineEnhancer(),
38+
processorTest.forkJvm()
3839
) ) );
3940
}
4041
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
4+
Copyright MapStruct Authors.
5+
6+
Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
7+
8+
-->
9+
<project xmlns="http://maven.apache.org/POM/4.0.0"
10+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
11+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
12+
<modelVersion>4.0.0</modelVersion>
13+
14+
<parent>
15+
<groupId>org.mapstruct</groupId>
16+
<artifactId>mapstruct-it-parent</artifactId>
17+
<version>1.0.0</version>
18+
<relativePath>../pom.xml</relativePath>
19+
</parent>
20+
21+
<artifactId>kotlinDataTest</artifactId>
22+
<packaging>jar</packaging>
23+
24+
<properties>
25+
<kotlin.version>1.3.70</kotlin.version>
26+
</properties>
27+
28+
<dependencies>
29+
<dependency>
30+
<groupId>org.jetbrains.kotlin</groupId>
31+
<artifactId>kotlin-stdlib</artifactId>
32+
<version>${kotlin.version}</version>
33+
</dependency>
34+
</dependencies>
35+
36+
<profiles>
37+
<profile>
38+
<id>generate-via-compiler-plugin</id>
39+
<activation>
40+
<activeByDefault>false</activeByDefault>
41+
</activation>
42+
<build>
43+
<plugins>
44+
<plugin>
45+
<groupId>org.jetbrains.kotlin</groupId>
46+
<artifactId>kotlin-maven-plugin</artifactId>
47+
<version>${kotlin.version}</version>
48+
<executions>
49+
<execution>
50+
<id>compile</id>
51+
<goals> <goal>compile</goal> </goals>
52+
<configuration>
53+
<sourceDirs>
54+
<sourceDir>\${project.basedir}/src/main/kotlin</sourceDir>
55+
<sourceDir>\${project.basedir}/src/main/java</sourceDir>
56+
</sourceDirs>
57+
</configuration>
58+
</execution>
59+
<execution>
60+
<id>test-compile</id>
61+
<goals> <goal>test-compile</goal> </goals>
62+
<configuration>
63+
<sourceDirs>
64+
<sourceDir>\${project.basedir}/src/test/kotlin</sourceDir>
65+
<sourceDir>\${project.basedir}/src/test/java</sourceDir>
66+
</sourceDirs>
67+
</configuration>
68+
</execution>
69+
</executions>
70+
</plugin>
71+
<plugin>
72+
<groupId>org.apache.maven.plugins</groupId>
73+
<artifactId>maven-compiler-plugin</artifactId>
74+
<executions>
75+
<!-- Replacing default-compile as it is treated specially by maven -->
76+
<execution>
77+
<id>default-compile</id>
78+
<phase>none</phase>
79+
</execution>
80+
<!-- Replacing default-testCompile as it is treated specially by maven -->
81+
<execution>
82+
<id>default-testCompile</id>
83+
<phase>none</phase>
84+
</execution>
85+
<execution>
86+
<id>java-compile</id>
87+
<phase>compile</phase>
88+
<goals> <goal>compile</goal> </goals>
89+
</execution>
90+
<execution>
91+
<id>java-test-compile</id>
92+
<phase>test-compile</phase>
93+
<goals> <goal>testCompile</goal> </goals>
94+
</execution>
95+
</executions>
96+
</plugin>
97+
</plugins>
98+
</build>
99+
</profile>
100+
</profiles>
101+
102+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright MapStruct Authors.
3+
*
4+
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
5+
*/
6+
package org.mapstruct.itest.kotlin.data;
7+
8+
/**
9+
* @author Filip Hrisafov
10+
*/
11+
public class CustomerEntity {
12+
13+
private String name;
14+
private String mail;
15+
16+
public String getName() {
17+
return name;
18+
}
19+
20+
public void setName(String name) {
21+
this.name = name;
22+
}
23+
24+
public String getMail() {
25+
return mail;
26+
}
27+
28+
public void setMail(String mail) {
29+
this.mail = mail;
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright MapStruct Authors.
3+
*
4+
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
5+
*/
6+
package org.mapstruct.itest.kotlin.data;
7+
8+
import org.mapstruct.InheritInverseConfiguration;
9+
import org.mapstruct.Mapper;
10+
import org.mapstruct.Mapping;
11+
import org.mapstruct.factory.Mappers;
12+
13+
/**
14+
* @author Filip Hrisafov
15+
*/
16+
@Mapper
17+
public interface CustomerMapper {
18+
19+
CustomerMapper INSTANCE = Mappers.getMapper( CustomerMapper.class );
20+
21+
@Mapping(target = "mail", source = "email")
22+
CustomerEntity fromRecord(CustomerDto record);
23+
24+
@InheritInverseConfiguration
25+
CustomerDto toRecord(CustomerEntity entity);
26+
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/*
2+
* Copyright MapStruct Authors.
3+
*
4+
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
5+
*/
6+
package org.mapstruct.itest.kotlin.data;
7+
8+
data class CustomerDto(var name: String?, var email: String?) {
9+
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright MapStruct Authors.
3+
*
4+
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
5+
*/
6+
package org.mapstruct.itest.kotlin.data;
7+
8+
import static org.assertj.core.api.Assertions.assertThat;
9+
10+
import org.junit.Test;
11+
import org.mapstruct.itest.kotlin.data.CustomerDto;
12+
import org.mapstruct.itest.kotlin.data.CustomerEntity;
13+
import org.mapstruct.itest.kotlin.data.CustomerMapper;
14+
15+
public class KotlinDataTest {
16+
17+
@Test
18+
public void shouldMapData() {
19+
CustomerEntity customer = CustomerMapper.INSTANCE.fromRecord( new CustomerDto( "Kermit", "[email protected]" ) );
20+
21+
assertThat( customer ).isNotNull();
22+
assertThat( customer.getName() ).isEqualTo( "Kermit" );
23+
assertThat( customer.getMail() ).isEqualTo( "[email protected]" );
24+
}
25+
26+
@Test
27+
public void shouldMapIntoData() {
28+
CustomerEntity entity = new CustomerEntity();
29+
entity.setName( "Kermit" );
30+
entity.setMail( "[email protected]" );
31+
32+
CustomerDto customer = CustomerMapper.INSTANCE.toRecord( entity );
33+
34+
assertThat( customer ).isNotNull();
35+
assertThat( customer.getName() ).isEqualTo( "Kermit" );
36+
assertThat( customer.getEmail() ).isEqualTo( "[email protected]" );
37+
}
38+
}

0 commit comments

Comments
 (0)