diff --git a/.travis.yml b/.travis.yml index 6a370d1b4..b86d33d31 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,6 +14,9 @@ matrix: - jdk: openjdk13 # There is an issue with the documentation so skip it script: mvn -B -V clean install -DskipDistribution=true + - jdk: openjdk14 + # There is an issue with the documentation so skip it + script: mvn -B -V clean install -DskipDistribution=true # There is an issue with the documentation so skip it - jdk: openjdk-ea script: mvn -B -V clean install -DskipDistribution=true 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 16298179a..e579f031f 100644 --- a/integrationtest/src/test/java/org/mapstruct/itest/tests/MavenIntegrationTest.java +++ b/integrationtest/src/test/java/org/mapstruct/itest/tests/MavenIntegrationTest.java @@ -5,6 +5,8 @@ */ package org.mapstruct.itest.tests; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; import org.junit.jupiter.api.parallel.Execution; import org.junit.jupiter.api.parallel.ExecutionMode; import org.mapstruct.itest.testutil.extension.ProcessorTest; @@ -99,6 +101,13 @@ public class MavenIntegrationTest { void protobufBuilderTest() { } + @ProcessorTest(baseDir = "recordsTest", processorTypes = { + ProcessorTest.ProcessorType.JAVAC + }) + @EnabledForJreRange(min = JRE.JAVA_14) + void recordsTest() { + } + @ProcessorTest(baseDir = "simpleTest") void simpleTest() { } diff --git a/integrationtest/src/test/resources/recordsTest/pom.xml b/integrationtest/src/test/resources/recordsTest/pom.xml new file mode 100644 index 000000000..c74469a7b --- /dev/null +++ b/integrationtest/src/test/resources/recordsTest/pom.xml @@ -0,0 +1,102 @@ + + + + 4.0.0 + + + org.mapstruct + mapstruct-it-parent + 1.0.0 + ../pom.xml + + + recordsTest + jar + + + + generate-via-compiler-plugin + + false + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + \${compiler-id} + --enable-preview + + + + org.eclipse.tycho + tycho-compiler-jdt + ${org.eclipse.tycho.compiler-jdt.version} + + + + + + + + ${project.groupId} + mapstruct-processor + ${mapstruct.version} + provided + + + + + debug-forked-javac + + false + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + default-compile + + true + + --enable-preview + -J-Xdebug + -J-Xnoagent + -J-Djava.compiler=NONE + -J-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 + + + + + + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + 1 + --enable-preview + + + + + + diff --git a/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/CustomerDto.java b/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/CustomerDto.java new file mode 100644 index 000000000..69b2547bc --- /dev/null +++ b/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/CustomerDto.java @@ -0,0 +1,13 @@ +/* + * 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.records; + +/** + * @author Filip Hrisafov + */ +public record CustomerDto(String name, String email) { + +} diff --git a/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/CustomerEntity.java b/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/CustomerEntity.java new file mode 100644 index 000000000..1663bbac5 --- /dev/null +++ b/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/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.records; + +/** + * @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/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 new file mode 100644 index 000000000..0e8a0d256 --- /dev/null +++ b/integrationtest/src/test/resources/recordsTest/src/main/java/org/mapstruct/itest/records/CustomerMapper.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.itest.records; + +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); + +} 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 new file mode 100644 index 000000000..5a7a62f78 --- /dev/null +++ b/integrationtest/src/test/resources/recordsTest/src/test/java/org/mapstruct/itest/records/RecordsTest.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.itest.records; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; +import org.mapstruct.itest.records.CustomerDto; +import org.mapstruct.itest.records.CustomerEntity; +import org.mapstruct.itest.records.CustomerMapper; + +public class RecordsTest { + + @Test + public void shouldMapRecord() { + 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" ); + } +} diff --git a/parent/pom.xml b/parent/pom.xml index 9b920c212..83a59bf2c 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -510,7 +510,7 @@ org.jacoco jacoco-maven-plugin - 0.8.3 + 0.8.5 org.jvnet.jaxb2.maven2 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 fe9d5e7e9..6f3a7b79c 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 @@ -498,6 +498,11 @@ public class Type extends ModelElement implements Comparable { } } + Map recordAccessors = filters.recordsIn( typeElement ); + for ( Map.Entry recordEntry : recordAccessors.entrySet() ) { + modifiableGetters.putIfAbsent( recordEntry.getKey(), recordEntry.getValue() ); + } + List fieldsList = filters.fieldsIn( getAllFields() ); for ( Accessor field : fieldsList ) { String propertyName = getPropertyName( field ); 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 b673b3f16..6981a1ebb 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 @@ -5,11 +5,16 @@ */ package org.mapstruct.ap.internal.util; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import javax.lang.model.element.Element; 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.ExecutableType; @@ -30,9 +35,29 @@ import static org.mapstruct.ap.internal.util.accessor.AccessorType.SETTER; * Filter methods for working with {@link Element} collections. * * @author Gunnar Morling + * @author Filip Hrisafov */ public class Filters { + private static final Method RECORD_COMPONENTS_METHOD; + private static final Method RECORD_COMPONENT_ACCESSOR_METHOD; + + static { + Method recordComponentsMethod; + Method recordComponentAccessorMethod; + try { + recordComponentsMethod = TypeElement.class.getMethod( "getRecordComponents" ); + recordComponentAccessorMethod = Class.forName( "javax.lang.model.element.RecordComponentElement" ) + .getMethod( "getAccessor" ); + } + catch ( NoSuchMethodException | ClassNotFoundException e ) { + recordComponentsMethod = null; + recordComponentAccessorMethod = null; + } + RECORD_COMPONENTS_METHOD = recordComponentsMethod; + RECORD_COMPONENT_ACCESSOR_METHOD = recordComponentAccessorMethod; + } + private final AccessorNamingUtils accessorNaming; private final Types typeUtils; private final TypeMirror typeMirror; @@ -50,6 +75,33 @@ public class Filters { .collect( Collectors.toCollection( LinkedList::new ) ); } + public Map recordsIn(TypeElement typeElement) { + if ( RECORD_COMPONENTS_METHOD == null || 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 = + (ExecutableElement) RECORD_COMPONENT_ACCESSOR_METHOD.invoke( recordComponent ); + recordAccessors.put( + recordComponent.getSimpleName().toString(), + new ExecutableElementAccessor( recordExecutableElement, + getReturnType( recordExecutableElement ), + GETTER + ) + ); + } + + return recordAccessors; + } + catch ( IllegalAccessException | InvocationTargetException e ) { + return java.util.Collections.emptyMap(); + } + } + private TypeMirror getReturnType(ExecutableElement executableElement) { return getWithinContext( executableElement ).getReturnType(); }