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 ab43cc4cba..f3faf85e20 100644 --- a/integrationtest/src/test/java/org/mapstruct/itest/tests/MavenIntegrationTest.java +++ b/integrationtest/src/test/java/org/mapstruct/itest/tests/MavenIntegrationTest.java @@ -222,4 +222,12 @@ void usesTypeGenerationTestEclipse() { void faultyAstModifyingProcessor() { } + /** + * For sealed interface, create an impl class with the switch case. + */ + @ProcessorTest(baseDir = "sealedInterfaceTest") + @EnabledForJreRange(min = JRE.JAVA_21) + void sealedInterfaceSubclassMappingTest() { + } + } diff --git a/integrationtest/src/test/resources/sealedInterfaceTest/pom.xml b/integrationtest/src/test/resources/sealedInterfaceTest/pom.xml new file mode 100644 index 0000000000..655c188b3d --- /dev/null +++ b/integrationtest/src/test/resources/sealedInterfaceTest/pom.xml @@ -0,0 +1,103 @@ + + + + 4.0.0 + + + org.mapstruct + mapstruct-it-parent + 1.0.0 + ../pom.xml + + + sealedInterfaceTest + 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/sealedInterfaceTest/src/main/java/org/mapstruct/itest/sealedinterface/From.java b/integrationtest/src/test/resources/sealedInterfaceTest/src/main/java/org/mapstruct/itest/sealedinterface/From.java new file mode 100644 index 0000000000..4e8f39984a --- /dev/null +++ b/integrationtest/src/test/resources/sealedInterfaceTest/src/main/java/org/mapstruct/itest/sealedinterface/From.java @@ -0,0 +1,8 @@ +/* + * 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.sealedinterface; + +public sealed interface From permits FromA, FromB {} \ No newline at end of file diff --git a/integrationtest/src/test/resources/sealedInterfaceTest/src/main/java/org/mapstruct/itest/sealedinterface/FromA.java b/integrationtest/src/test/resources/sealedInterfaceTest/src/main/java/org/mapstruct/itest/sealedinterface/FromA.java new file mode 100644 index 0000000000..d0f92d816f --- /dev/null +++ b/integrationtest/src/test/resources/sealedInterfaceTest/src/main/java/org/mapstruct/itest/sealedinterface/FromA.java @@ -0,0 +1,8 @@ +/* + * 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.sealedinterface; + +public record FromA(int a) implements From {} \ No newline at end of file diff --git a/integrationtest/src/test/resources/sealedInterfaceTest/src/main/java/org/mapstruct/itest/sealedinterface/FromB.java b/integrationtest/src/test/resources/sealedInterfaceTest/src/main/java/org/mapstruct/itest/sealedinterface/FromB.java new file mode 100644 index 0000000000..2c1ab4b299 --- /dev/null +++ b/integrationtest/src/test/resources/sealedInterfaceTest/src/main/java/org/mapstruct/itest/sealedinterface/FromB.java @@ -0,0 +1,8 @@ +/* + * 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.sealedinterface; + +public record FromB(int b) implements From {} \ No newline at end of file diff --git a/integrationtest/src/test/resources/sealedInterfaceTest/src/main/java/org/mapstruct/itest/sealedinterface/Issue3836Mapper.java b/integrationtest/src/test/resources/sealedInterfaceTest/src/main/java/org/mapstruct/itest/sealedinterface/Issue3836Mapper.java new file mode 100644 index 0000000000..4e9d983488 --- /dev/null +++ b/integrationtest/src/test/resources/sealedInterfaceTest/src/main/java/org/mapstruct/itest/sealedinterface/Issue3836Mapper.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.itest.sealedinterface; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.SubclassMapping; +import org.mapstruct.factory.Mappers; + +@Mapper(unmappedTargetPolicy = ReportingPolicy.WARN) +public interface Issue3836Mapper { + + Issue3836Mapper INSTANCE = Mappers.getMapper( Issue3836Mapper.class ); + + @Mapping(source = "a", target = "to") + To map(FromA from); + + @Mapping(source = "b", target = "to") + To map(FromB fromB); + + @SubclassMapping(source = FromA.class, target = To.class) + @SubclassMapping(source = FromB.class, target = To.class) + To map(From from); +} \ No newline at end of file diff --git a/integrationtest/src/test/resources/sealedInterfaceTest/src/main/java/org/mapstruct/itest/sealedinterface/To.java b/integrationtest/src/test/resources/sealedInterfaceTest/src/main/java/org/mapstruct/itest/sealedinterface/To.java new file mode 100644 index 0000000000..c61cae9d59 --- /dev/null +++ b/integrationtest/src/test/resources/sealedInterfaceTest/src/main/java/org/mapstruct/itest/sealedinterface/To.java @@ -0,0 +1,8 @@ +/* + * 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.sealedinterface; + +public record To(int to) {} \ No newline at end of file diff --git a/integrationtest/src/test/resources/sealedInterfaceTest/src/test/java/org/mapstruct/itest/sealedinterface/SealedInterfaceTest.java b/integrationtest/src/test/resources/sealedInterfaceTest/src/test/java/org/mapstruct/itest/sealedinterface/SealedInterfaceTest.java new file mode 100644 index 0000000000..c3eb45b3cd --- /dev/null +++ b/integrationtest/src/test/resources/sealedInterfaceTest/src/test/java/org/mapstruct/itest/sealedinterface/SealedInterfaceTest.java @@ -0,0 +1,78 @@ +/* + * 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.sealedinterface; + +import org.junit.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SealedInterfaceTest { + + private final Issue3836Mapper mapper = Issue3836Mapper.INSTANCE; + + @Test + public void shouldGeneratePatternSwitchInPolymorphicMapMethod() throws IOException { + Path generatedFile = Path.of("target/generated-sources/annotations/org/mapstruct/itest/sealedinterface/Issue3836MapperImpl.java"); + String content = Files.readString(generatedFile); + assertThat(content.contains("switch (from) {")).isTrue(); + assertThat(content.contains("case FromA fromA ->")).isTrue(); + assertThat(content.contains("return map( (FromA) from );")).isTrue(); + assertThat(content.contains("case FromB fromB ->")).isTrue(); + assertThat(content.contains("return map( (FromB) from );")).isTrue(); + assertThat(content.contains("if (from instanceof")).isFalse(); + } + + @Test + public void shouldMapFromAToTo() { + FromA fromA = new FromA(100); + + To result = mapper.map(fromA); + + assertThat(result).isNotNull(); + assertThat(result.to()).isEqualTo(100); + } + + @Test + public void shouldMapFromBToTo() { + FromB fromB = new FromB(200); + + To result = mapper.map(fromB); + + assertThat(result).isNotNull(); + assertThat(result.to()).isEqualTo(200); + } + + @Test + public void shouldMapFromInterfaceWithFromAInstance() { + From from = new FromA(300); + + To result = mapper.map(from); + + assertThat(result).isNotNull(); + assertThat(result.to()).isEqualTo(300); + } + + @Test + public void shouldMapFromInterfaceWithFromBInstance() { + From from = new FromB(400); + + To result = mapper.map(from); + + assertThat(result).isNotNull(); + assertThat(result.to()).isEqualTo(400); + } + + @Test + public void shouldReturnNullWhenInputIsNull() { + assertThat(mapper.map((FromA) null)).isNull(); + assertThat(mapper.map((FromB) null)).isNull(); + assertThat(mapper.map((From) null)).isNull(); + } +} \ No newline at end of file diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java index b5208d302b..f5c0b5a3e9 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 @@ -532,18 +532,20 @@ private SubclassMapping createSubclassMapping(SubclassMappingOptions subclassMap targetType, mappingReferences ) ); String sourceArgument = null; + Type sourceArgumentType = null; for ( Parameter parameter : method.getSourceParameters() ) { if ( ctx .getTypeUtils() .isAssignable( sourceType.getTypeMirror(), parameter.getType().getTypeMirror() ) ) { sourceArgument = parameter.getName(); + sourceArgumentType = parameter.getType(); if ( assignment != null ) { assignment.setSourceLocalVarName( "(" + sourceType.createReferenceName() + ") " + sourceArgument ); } } } - return new SubclassMapping( sourceType, sourceArgument, targetType, assignment ); + return new SubclassMapping( sourceType, sourceArgument, sourceArgumentType, targetType, assignment ); } private boolean isAbstractReturnTypeAllowed() { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/SubclassMapping.java b/processor/src/main/java/org/mapstruct/ap/internal/model/SubclassMapping.java index 3ce42f68e5..bfb3c6cfde 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/SubclassMapping.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/SubclassMapping.java @@ -28,10 +28,13 @@ public class SubclassMapping extends ModelElement { private final Type targetType; private final Assignment assignment; private final String sourceArgument; + private final Type sourceArgumentType; - public SubclassMapping(Type sourceType, String sourceArgument, Type targetType, Assignment assignment) { + public SubclassMapping(Type sourceType, String sourceArgument, Type sourceArgumentType, + Type targetType, Assignment assignment) { this.sourceType = sourceType; this.sourceArgument = sourceArgument; + this.sourceArgumentType = sourceArgumentType; this.targetType = targetType; this.assignment = assignment; } @@ -40,6 +43,10 @@ public Type getSourceType() { return sourceType; } + public Type getSourceArgumentType() { + return sourceArgumentType; + } + @Override public Set getImportTypes() { return Collections.singleton( sourceType ); 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 da590bb506..60d8b59d75 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 @@ -34,127 +34,140 @@ <#if hasSubclassMappings()> - <#list subclassMappings as subclass> - <#if subclass_index > 0>else if (${subclass.sourceArgument} instanceof <@includeModel object=subclass.sourceType/>) { + <#if subclassMappings[0].sourceArgumentType.isSealed()> + <#assign switchVar = subclassMappings[0].sourceArgument> + switch (${switchVar}) { + <#list subclassMappings as subclass> + case <@includeModel object=subclass.sourceType/> ${subclass.sourceType.name?uncap_first} -> { <@includeModel object=subclass.assignment existingInstanceMapping=existingInstanceMapping/> + } + } - - else { + <#else> + <#list subclassMappings as subclass> + <#if subclass_index > 0>else if (${subclass.sourceArgument} instanceof <@includeModel object=subclass.sourceType/>) { + <@includeModel object=subclass.assignment existingInstanceMapping=existingInstanceMapping/> + } + + else { + - <#if isAbstractReturnType()> - throw new <@includeModel object=subclassExhaustiveException />("Not all subclasses are supported for this mapping. Missing for " + ${subclassMappings[0].sourceArgument}.getClass()); - <#else> - <#if !existingInstanceMapping> - <#if hasConstructorMappings()> - <#if (sourceParameters?size > 1)> - <#list sourceParametersNeedingPresenceCheck as sourceParam> - <#if (constructorPropertyMappingsByParameter(sourceParam)?size > 0)> - <#list constructorPropertyMappingsByParameter(sourceParam) as propertyMapping> - <@includeModel object=propertyMapping.targetType /> ${propertyMapping.targetWriteAccessorName} = ${propertyMapping.targetType.null}; - - if ( <@includeModel object=getPresenceCheckByParameter(sourceParam) /> ) { - <#list constructorPropertyMappingsByParameter(sourceParam) as propertyMapping> - <@includeModel object=propertyMapping existingInstanceMapping=existingInstanceMapping defaultValueAssignment=propertyMapping.defaultValueAssignment/> - + <#if (!hasSubclassMappings() || !subclassMappings[0].sourceArgumentType.sealed)> + <#if isAbstractReturnType()> + throw new <@includeModel object=subclassExhaustiveException />("Not all subclasses are supported for this mapping. Missing for " + ${subclassMappings[0].sourceArgument}.getClass()); + <#else> + <#if !existingInstanceMapping> + <#if hasConstructorMappings()> + <#if (sourceParameters?size > 1)> + <#list sourceParametersNeedingPresenceCheck as sourceParam> + <#if (constructorPropertyMappingsByParameter(sourceParam)?size > 0)> + <#list constructorPropertyMappingsByParameter(sourceParam) as propertyMapping> + <@includeModel object=propertyMapping.targetType /> ${propertyMapping.targetWriteAccessorName} = ${propertyMapping.targetType.null}; + + if ( <@includeModel object=getPresenceCheckByParameter(sourceParam) /> ) { + <#list constructorPropertyMappingsByParameter(sourceParam) as propertyMapping> + <@includeModel object=propertyMapping existingInstanceMapping=existingInstanceMapping defaultValueAssignment=propertyMapping.defaultValueAssignment/> + + } + + + <#list sourceParametersNotNeedingPresenceCheck as sourceParam> + <#if (constructorPropertyMappingsByParameter(sourceParam)?size > 0)> + <#list constructorPropertyMappingsByParameter(sourceParam) as propertyMapping> + <@includeModel object=propertyMapping.targetType /> ${propertyMapping.targetWriteAccessorName} = ${propertyMapping.targetType.null}; + <@includeModel object=propertyMapping existingInstanceMapping=existingInstanceMapping defaultValueAssignment=propertyMapping.defaultValueAssignment/> + + + + <#else> + <#list constructorPropertyMappingsByParameter(sourceParameters[0]) as propertyMapping> + <@includeModel object=propertyMapping.targetType /> ${propertyMapping.targetWriteAccessorName} = ${propertyMapping.targetType.null}; + + <#if mapNullToDefault>if ( <@includeModel object=getPresenceCheckByParameter(sourceParameters[0]) /> ) { + <#list constructorPropertyMappingsByParameter(sourceParameters[0]) as propertyMapping> + <@includeModel object=propertyMapping existingInstanceMapping=existingInstanceMapping defaultValueAssignment=propertyMapping.defaultValueAssignment/> + + <#if mapNullToDefault> } - - <#list sourceParametersNotNeedingPresenceCheck as sourceParam> - <#if (constructorPropertyMappingsByParameter(sourceParam)?size > 0)> - <#list constructorPropertyMappingsByParameter(sourceParam) as propertyMapping> - <@includeModel object=propertyMapping.targetType /> ${propertyMapping.targetWriteAccessorName} = ${propertyMapping.targetType.null}; - <@includeModel object=propertyMapping existingInstanceMapping=existingInstanceMapping defaultValueAssignment=propertyMapping.defaultValueAssignment/> - - - - <#else> - <#list constructorPropertyMappingsByParameter(sourceParameters[0]) as propertyMapping> - <@includeModel object=propertyMapping.targetType /> ${propertyMapping.targetWriteAccessorName} = ${propertyMapping.targetType.null}; - - <#if mapNullToDefault>if ( <@includeModel object=getPresenceCheckByParameter(sourceParameters[0]) /> ) { - <#list constructorPropertyMappingsByParameter(sourceParameters[0]) as propertyMapping> - <@includeModel object=propertyMapping existingInstanceMapping=existingInstanceMapping defaultValueAssignment=propertyMapping.defaultValueAssignment/> - - <#if mapNullToDefault> - } - - <#list constructorConstantMappings as constantMapping> + <#list constructorConstantMappings as constantMapping> - <@compress single_line=true> - <@includeModel object=constantMapping.targetType /> <@includeModel object=constantMapping existingInstanceMapping=existingInstanceMapping/> - - + <@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/>(); - + <@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> - <@includeModel object=callback targetBeanName=resultName targetType=targetType/> - <#if !callback_has_next> + + <#list beforeMappingReferencesWithMappingTarget as callback> + <@includeModel object=callback targetBeanName=resultName targetType=targetType/> + <#if !callback_has_next> - - - <#if (sourceParameters?size > 1)> - <#list sourceParametersNeedingPresenceCheck as sourceParam> - <#if (propertyMappingsByParameter(sourceParam)?size > 0)> - if ( <@includeModel object=getPresenceCheckByParameter(sourceParam) /> ) { + + + <#if (sourceParameters?size > 1)> + <#list sourceParametersNeedingPresenceCheck as sourceParam> + <#if (propertyMappingsByParameter(sourceParam)?size > 0)> + if ( <@includeModel object=getPresenceCheckByParameter(sourceParam) /> ) { + <#list propertyMappingsByParameter(sourceParam) as propertyMapping> + <@includeModel object=propertyMapping targetBeanName=resultName existingInstanceMapping=existingInstanceMapping defaultValueAssignment=propertyMapping.defaultValueAssignment/> + + } + + + <#list sourceParametersNotNeedingPresenceCheck as sourceParam> + <#if (propertyMappingsByParameter(sourceParam)?size > 0)> <#list propertyMappingsByParameter(sourceParam) as propertyMapping> <@includeModel object=propertyMapping targetBeanName=resultName existingInstanceMapping=existingInstanceMapping defaultValueAssignment=propertyMapping.defaultValueAssignment/> - } - + + + <#elseif !propertyMappingsByParameter(sourceParameters[0]).empty> + <#if mapNullToDefault>if ( <@includeModel object=getPresenceCheckByParameter(sourceParameters[0]) /> ) { + <#list propertyMappingsByParameter(sourceParameters[0]) as propertyMapping> + <@includeModel object=propertyMapping targetBeanName=resultName existingInstanceMapping=existingInstanceMapping defaultValueAssignment=propertyMapping.defaultValueAssignment/> + + <#if mapNullToDefault>} + + <#list constantMappings as constantMapping> + <@includeModel object=constantMapping targetBeanName=resultName existingInstanceMapping=existingInstanceMapping/> - <#list sourceParametersNotNeedingPresenceCheck as sourceParam> - <#if (propertyMappingsByParameter(sourceParam)?size > 0)> - <#list propertyMappingsByParameter(sourceParam) as propertyMapping> - <@includeModel object=propertyMapping targetBeanName=resultName existingInstanceMapping=existingInstanceMapping defaultValueAssignment=propertyMapping.defaultValueAssignment/> - + <#list afterMappingReferences as callback> + <#if callback_index = 0> + + <@includeModel object=callback targetBeanName=resultName targetType=targetType/> - <#elseif !propertyMappingsByParameter(sourceParameters[0]).empty> - <#if mapNullToDefault>if ( <@includeModel object=getPresenceCheckByParameter(sourceParameters[0]) /> ) { - <#list propertyMappingsByParameter(sourceParameters[0]) as propertyMapping> - <@includeModel object=propertyMapping targetBeanName=resultName existingInstanceMapping=existingInstanceMapping defaultValueAssignment=propertyMapping.defaultValueAssignment/> - - <#if mapNullToDefault>} - - <#list constantMappings as constantMapping> - <@includeModel object=constantMapping targetBeanName=resultName existingInstanceMapping=existingInstanceMapping/> - - <#list afterMappingReferences as callback> - <#if callback_index = 0> - - - <@includeModel object=callback targetBeanName=resultName targetType=targetType/> - - <#if returnType.name != "void"> + <#if returnType.name != "void"> - <#if finalizerMethod??> - <#if (afterMappingReferencesWithFinalizedReturnType?size > 0)> - <@includeModel object=returnType /> ${finalizedResultName} = ${resultName}.<@includeModel object=finalizerMethod />; + <#if finalizerMethod??> + <#if (afterMappingReferencesWithFinalizedReturnType?size > 0)> + <@includeModel object=returnType /> ${finalizedResultName} = ${resultName}.<@includeModel object=finalizerMethod />; - <#list afterMappingReferencesWithFinalizedReturnType as callback> - <#if callback_index = 0> + <#list afterMappingReferencesWithFinalizedReturnType as callback> + <#if callback_index = 0> - - <@includeModel object=callback targetBeanName=finalizedResultName targetType=returnType/> - + + <@includeModel object=callback targetBeanName=finalizedResultName targetType=returnType/> + - return ${finalizedResultName}; + return ${finalizedResultName}; + <#else> + return ${resultName}.<@includeModel object=finalizerMethod />; + <#else> - return ${resultName}.<@includeModel object=finalizerMethod />; + return ${resultName}; + + - <#else> - return ${resultName}; - - - <#if hasSubclassMappings()> + <#if hasSubclassMappings() && !subclassMappings[0].sourceArgumentType.isSealed()> } }