From 6e6fd01a2eb08177d5cc360a97e1721db511239d Mon Sep 17 00:00:00 2001 From: Yang Tang Date: Sun, 18 May 2025 00:40:51 +0800 Subject: [PATCH] #3821: Add support for custom exception for subclass exhaustive strategy for `@SubclassMapping` --------- Signed-off-by: TangYang --- .../main/java/org/mapstruct/BeanMapping.java | 12 +++ core/src/main/java/org/mapstruct/Mapper.java | 12 +++ .../main/java/org/mapstruct/MapperConfig.java | 12 +++ .../ap/internal/model/BeanMappingMethod.java | 17 ++++- .../model/source/BeanMappingOptions.java | 9 +++ .../internal/model/source/DefaultOptions.java | 4 + .../model/source/DelegatingOptions.java | 4 + .../model/source/MapperConfigOptions.java | 6 ++ .../internal/model/source/MapperOptions.java | 7 ++ .../ap/internal/model/BeanMappingMethod.ftl | 2 +- .../CustomExceptionSubclassMapper.java | 25 ++++++ .../CustomSubclassMappingException.java | 12 +++ .../CustomSubclassMappingExceptionTest.java | 76 +++++++++++++++++++ .../MapperConfigSubclassMapper.java | 29 +++++++ .../MapperSubclassMapper.java | 24 ++++++ 15 files changed, 248 insertions(+), 3 deletions(-) create mode 100644 processor/src/test/java/org/mapstruct/ap/test/subclassmapping/abstractsuperclass/CustomExceptionSubclassMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/subclassmapping/abstractsuperclass/CustomSubclassMappingException.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/subclassmapping/abstractsuperclass/CustomSubclassMappingExceptionTest.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/subclassmapping/abstractsuperclass/MapperConfigSubclassMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/subclassmapping/abstractsuperclass/MapperSubclassMapper.java diff --git a/core/src/main/java/org/mapstruct/BeanMapping.java b/core/src/main/java/org/mapstruct/BeanMapping.java index e94ff98f2..309458f86 100644 --- a/core/src/main/java/org/mapstruct/BeanMapping.java +++ b/core/src/main/java/org/mapstruct/BeanMapping.java @@ -132,6 +132,18 @@ public @interface BeanMapping { */ SubclassExhaustiveStrategy subclassExhaustiveStrategy() default COMPILE_ERROR; + /** + * Specifies the exception type to be thrown when a missing subclass implementation is detected + * in combination with {@link SubclassMappings}, based on the {@link #subclassExhaustiveStrategy()}. + *

+ * This exception will only be thrown when the {@code subclassExhaustiveStrategy} is set to + * {@link SubclassExhaustiveStrategy#RUNTIME_EXCEPTION}. + * + * @return the exception class to throw when missing implementations are found. + * Defaults to {@link IllegalArgumentException}. + */ + Class subclassExhaustiveException() default IllegalArgumentException.class; + /** * Default ignore all mappings. All mappings have to be defined manually. No automatic mapping will take place. No * warning will be issued on missing source or target properties. diff --git a/core/src/main/java/org/mapstruct/Mapper.java b/core/src/main/java/org/mapstruct/Mapper.java index 8a6f48dad..398dc1870 100644 --- a/core/src/main/java/org/mapstruct/Mapper.java +++ b/core/src/main/java/org/mapstruct/Mapper.java @@ -281,6 +281,18 @@ public @interface Mapper { */ SubclassExhaustiveStrategy subclassExhaustiveStrategy() default COMPILE_ERROR; + /** + * Specifies the exception type to be thrown when a missing subclass implementation is detected + * in combination with {@link SubclassMappings}, based on the {@link #subclassExhaustiveStrategy()}. + *

+ * This exception will only be thrown when the {@code subclassExhaustiveStrategy} is set to + * {@link SubclassExhaustiveStrategy#RUNTIME_EXCEPTION}. + * + * @return the exception class to throw when missing implementations are found. + * Defaults to {@link IllegalArgumentException}. + */ + Class subclassExhaustiveException() default IllegalArgumentException.class; + /** * Determines whether to use field or constructor injection. This is only used on annotated based component models * such as CDI, Spring and JSR 330. diff --git a/core/src/main/java/org/mapstruct/MapperConfig.java b/core/src/main/java/org/mapstruct/MapperConfig.java index 915f3dd12..8631562a5 100644 --- a/core/src/main/java/org/mapstruct/MapperConfig.java +++ b/core/src/main/java/org/mapstruct/MapperConfig.java @@ -249,6 +249,18 @@ public @interface MapperConfig { */ SubclassExhaustiveStrategy subclassExhaustiveStrategy() default COMPILE_ERROR; + /** + * Specifies the exception type to be thrown when a missing subclass implementation is detected + * in combination with {@link SubclassMappings}, based on the {@link #subclassExhaustiveStrategy()}. + *

+ * This exception will only be thrown when the {@code subclassExhaustiveStrategy} is set to + * {@link SubclassExhaustiveStrategy#RUNTIME_EXCEPTION}. + * + * @return the exception class to throw when missing implementations are found. + * Defaults to {@link IllegalArgumentException}. + */ + Class subclassExhaustiveException() default IllegalArgumentException.class; + /** * Determines whether to use field or constructor injection. This is only used on annotated based component models * such as CDI, Spring and JSR 330. 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 08c8ceda5..34c1ea3cc 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 @@ -100,6 +100,7 @@ public class BeanMappingMethod extends NormalTypeMappingMethod { private final String finalizedResultName; private final List beforeMappingReferencesWithFinalizedReturnType; private final List afterMappingReferencesWithFinalizedReturnType; + private final Type subclassExhaustiveException; private final MappingReferences mappingReferences; @@ -378,6 +379,11 @@ public class BeanMappingMethod extends NormalTypeMappingMethod { } + TypeMirror subclassExhaustiveException = method.getOptions() + .getBeanMapping() + .getSubclassExhaustiveException(); + Type subclassExhaustiveExceptionType = ctx.getTypeFactory().getType( subclassExhaustiveException ); + List subclasses = new ArrayList<>(); for ( SubclassMappingOptions subclassMappingOptions : method.getOptions().getSubclassMappings() ) { subclasses.add( createSubclassMapping( subclassMappingOptions ) ); @@ -451,7 +457,8 @@ public class BeanMappingMethod extends NormalTypeMappingMethod { finalizeMethod, mappingReferences, subclasses, - presenceChecksByParameter + presenceChecksByParameter, + subclassExhaustiveExceptionType ); } @@ -1954,7 +1961,8 @@ public class BeanMappingMethod extends NormalTypeMappingMethod { MethodReference finalizerMethod, MappingReferences mappingReferences, List subclassMappings, - Map presenceChecksByParameter) { + Map presenceChecksByParameter, + Type subclassExhaustiveException) { super( method, annotations, @@ -1969,6 +1977,7 @@ public class BeanMappingMethod extends NormalTypeMappingMethod { this.propertyMappings = propertyMappings; this.returnTypeBuilder = returnTypeBuilder; this.finalizerMethod = finalizerMethod; + this.subclassExhaustiveException = subclassExhaustiveException; if ( this.finalizerMethod != null ) { this.finalizedResultName = Strings.getSafeVariableName( getResultName() + "Result", existingVariableNames ); @@ -2017,6 +2026,10 @@ public class BeanMappingMethod extends NormalTypeMappingMethod { this.subclassMappings = subclassMappings; } + public Type getSubclassExhaustiveException() { + return subclassExhaustiveException; + } + public List getConstantMappings() { return constantMappings; } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/BeanMappingOptions.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/BeanMappingOptions.java index ac27dfff0..73a4d7c12 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/BeanMappingOptions.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/BeanMappingOptions.java @@ -11,6 +11,7 @@ import java.util.Objects; import java.util.Optional; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; +import javax.lang.model.type.TypeMirror; import org.mapstruct.ap.internal.gem.BeanMappingGem; import org.mapstruct.ap.internal.gem.BuilderGem; @@ -182,6 +183,14 @@ public class BeanMappingOptions extends DelegatingOptions { .orElse( next().getSubclassExhaustiveStrategy() ); } + @Override + public TypeMirror getSubclassExhaustiveException() { + return Optional.ofNullable( beanMapping ).map( BeanMappingGem::subclassExhaustiveException ) + .filter( GemValue::hasValue ) + .map( GemValue::getValue ) + .orElse( next().getSubclassExhaustiveException() ); + } + @Override public ReportingPolicyGem unmappedTargetPolicy() { return Optional.ofNullable( beanMapping ).map( BeanMappingGem::unmappedTargetPolicy ) diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/DefaultOptions.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/DefaultOptions.java index c754d3c39..e1f04fd94 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/DefaultOptions.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/DefaultOptions.java @@ -131,6 +131,10 @@ public class DefaultOptions extends DelegatingOptions { return SubclassExhaustiveStrategyGem.valueOf( mapper.subclassExhaustiveStrategy().getDefaultValue() ); } + public TypeMirror getSubclassExhaustiveException() { + return mapper.subclassExhaustiveException().getDefaultValue(); + } + public NullValueMappingStrategyGem getNullValueIterableMappingStrategy() { NullValueMappingStrategyGem nullValueIterableMappingStrategy = options.getNullValueIterableMappingStrategy(); if ( nullValueIterableMappingStrategy != null ) { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/DelegatingOptions.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/DelegatingOptions.java index 50c1d8454..34478969f 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/DelegatingOptions.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/DelegatingOptions.java @@ -106,6 +106,10 @@ public abstract class DelegatingOptions { return next.getSubclassExhaustiveStrategy(); } + public TypeMirror getSubclassExhaustiveException() { + return next.getSubclassExhaustiveException(); + } + public NullValueMappingStrategyGem getNullValueIterableMappingStrategy() { return next.getNullValueIterableMappingStrategy(); } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/MapperConfigOptions.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/MapperConfigOptions.java index e3ca6162a..d60665887 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/MapperConfigOptions.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/MapperConfigOptions.java @@ -141,6 +141,12 @@ public class MapperConfigOptions extends DelegatingOptions { next().getSubclassExhaustiveStrategy(); } + public TypeMirror getSubclassExhaustiveException() { + return mapperConfig.subclassExhaustiveException().hasValue() ? + mapperConfig.subclassExhaustiveException().get() : + next().getSubclassExhaustiveException(); + } + @Override public NullValueMappingStrategyGem getNullValueIterableMappingStrategy() { if ( mapperConfig.nullValueIterableMappingStrategy().hasValue() ) { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/MapperOptions.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/MapperOptions.java index ed1af34f7..9c2203efd 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/MapperOptions.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/MapperOptions.java @@ -170,6 +170,13 @@ public class MapperOptions extends DelegatingOptions { next().getSubclassExhaustiveStrategy(); } + @Override + public TypeMirror getSubclassExhaustiveException() { + return mapper.subclassExhaustiveException().hasValue() ? + mapper.subclassExhaustiveException().get() : + next().getSubclassExhaustiveException(); + } + @Override public NullValueMappingStrategyGem getNullValueIterableMappingStrategy() { if ( mapper.nullValueIterableMappingStrategy().hasValue() ) { 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 3036e4a2c..da590bb50 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 @@ -42,7 +42,7 @@ else { <#if isAbstractReturnType()> - throw new IllegalArgumentException("Not all subclasses are supported for this mapping. Missing for " + ${subclassMappings[0].sourceArgument}.getClass()); + throw new <@includeModel object=subclassExhaustiveException />("Not all subclasses are supported for this mapping. Missing for " + ${subclassMappings[0].sourceArgument}.getClass()); <#else> <#if !existingInstanceMapping> <#if hasConstructorMappings()> diff --git a/processor/src/test/java/org/mapstruct/ap/test/subclassmapping/abstractsuperclass/CustomExceptionSubclassMapper.java b/processor/src/test/java/org/mapstruct/ap/test/subclassmapping/abstractsuperclass/CustomExceptionSubclassMapper.java new file mode 100644 index 000000000..52738ef6f --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/subclassmapping/abstractsuperclass/CustomExceptionSubclassMapper.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.subclassmapping.abstractsuperclass; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.SubclassExhaustiveStrategy; +import org.mapstruct.SubclassMapping; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface CustomExceptionSubclassMapper { + CustomExceptionSubclassMapper INSTANCE = Mappers.getMapper( CustomExceptionSubclassMapper.class ); + + @BeanMapping(subclassExhaustiveStrategy = SubclassExhaustiveStrategy.RUNTIME_EXCEPTION, + subclassExhaustiveException = CustomSubclassMappingException.class) + @SubclassMapping(source = Car.class, target = CarDto.class) + @SubclassMapping(source = Bike.class, target = BikeDto.class) + VehicleDto map(AbstractVehicle vehicle); + + VehicleCollectionDto mapInverse(VehicleCollection vehicles); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/subclassmapping/abstractsuperclass/CustomSubclassMappingException.java b/processor/src/test/java/org/mapstruct/ap/test/subclassmapping/abstractsuperclass/CustomSubclassMappingException.java new file mode 100644 index 000000000..abb675bbb --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/subclassmapping/abstractsuperclass/CustomSubclassMappingException.java @@ -0,0 +1,12 @@ +/* + * 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.subclassmapping.abstractsuperclass; + +public class CustomSubclassMappingException extends RuntimeException { + public CustomSubclassMappingException(String message) { + super( message ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/subclassmapping/abstractsuperclass/CustomSubclassMappingExceptionTest.java b/processor/src/test/java/org/mapstruct/ap/test/subclassmapping/abstractsuperclass/CustomSubclassMappingExceptionTest.java new file mode 100644 index 000000000..472c00726 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/subclassmapping/abstractsuperclass/CustomSubclassMappingExceptionTest.java @@ -0,0 +1,76 @@ +/* + * 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.subclassmapping.abstractsuperclass; + +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.ProcessorTest; +import org.mapstruct.ap.testutil.WithClasses; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@IssueKey("3821") +@WithClasses({ + Bike.class, + BikeDto.class, + Car.class, + CarDto.class, + Motorcycle.class, + VehicleCollection.class, + VehicleCollectionDto.class, + AbstractVehicle.class, + VehicleDto.class, + CustomSubclassMappingException.class, + CustomExceptionSubclassMapper.class +}) +public class CustomSubclassMappingExceptionTest { + + private static final String EXPECTED_ERROR_MESSAGE = "Not all subclasses are supported for this mapping. " + + "Missing for class org.mapstruct.ap.test.subclassmapping.abstractsuperclass.Motorcycle"; + + @ProcessorTest + void customExceptionIsThrownForUnknownSubclass() { + VehicleCollection vehicles = new VehicleCollection(); + vehicles.getVehicles().add( new Car() ); + vehicles.getVehicles().add( new Motorcycle() ); // undefine subclass + + assertThatThrownBy( () -> CustomExceptionSubclassMapper.INSTANCE.mapInverse( vehicles ) ) + .isInstanceOf( CustomSubclassMappingException.class ) + .hasMessage( EXPECTED_ERROR_MESSAGE ); + } + + @ProcessorTest + void customExceptionIsThrownForSingleVehicle() { + AbstractVehicle vehicle = new Motorcycle(); // undefine subclass + + assertThatThrownBy( () -> CustomExceptionSubclassMapper.INSTANCE.map( vehicle ) ) + .isInstanceOf( CustomSubclassMappingException.class ) + .hasMessage( EXPECTED_ERROR_MESSAGE ); + } + + @ProcessorTest + @WithClasses({ MapperConfigSubclassMapper.class }) + void customExceptionIsThrownForMapperConfig() { + VehicleCollection vehicles = new VehicleCollection(); + vehicles.getVehicles().add( new Car() ); + vehicles.getVehicles().add( new Motorcycle() ); // undefined subclass + + assertThatThrownBy( () -> MapperConfigSubclassMapper.INSTANCE.mapInverse( vehicles ) ) + .isInstanceOf( CustomSubclassMappingException.class ) + .hasMessage( EXPECTED_ERROR_MESSAGE ); + } + + @ProcessorTest + @WithClasses({ MapperSubclassMapper.class }) + void customExceptionIsThrownForMapper() { + VehicleCollection vehicles = new VehicleCollection(); + vehicles.getVehicles().add( new Car() ); + vehicles.getVehicles().add( new Motorcycle() ); // undefined subclass + + assertThatThrownBy( () -> MapperSubclassMapper.INSTANCE.mapInverse( vehicles ) ) + .isInstanceOf( CustomSubclassMappingException.class ) + .hasMessage( EXPECTED_ERROR_MESSAGE ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/subclassmapping/abstractsuperclass/MapperConfigSubclassMapper.java b/processor/src/test/java/org/mapstruct/ap/test/subclassmapping/abstractsuperclass/MapperConfigSubclassMapper.java new file mode 100644 index 000000000..b3c1a66bb --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/subclassmapping/abstractsuperclass/MapperConfigSubclassMapper.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.subclassmapping.abstractsuperclass; + +import org.mapstruct.Mapper; +import org.mapstruct.MapperConfig; +import org.mapstruct.SubclassExhaustiveStrategy; +import org.mapstruct.SubclassMapping; +import org.mapstruct.factory.Mappers; + +@Mapper(config = MapperConfigSubclassMapper.Config.class) +public interface MapperConfigSubclassMapper { + + MapperConfigSubclassMapper INSTANCE = Mappers.getMapper( MapperConfigSubclassMapper.class ); + + @MapperConfig(subclassExhaustiveStrategy = SubclassExhaustiveStrategy.RUNTIME_EXCEPTION, + subclassExhaustiveException = CustomSubclassMappingException.class) + interface Config { + } + + @SubclassMapping(source = Car.class, target = CarDto.class) + @SubclassMapping(source = Bike.class, target = BikeDto.class) + VehicleDto map(AbstractVehicle vehicle); + + VehicleCollectionDto mapInverse(VehicleCollection vehicles); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/subclassmapping/abstractsuperclass/MapperSubclassMapper.java b/processor/src/test/java/org/mapstruct/ap/test/subclassmapping/abstractsuperclass/MapperSubclassMapper.java new file mode 100644 index 000000000..34ed3fde2 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/subclassmapping/abstractsuperclass/MapperSubclassMapper.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.subclassmapping.abstractsuperclass; + +import org.mapstruct.Mapper; +import org.mapstruct.SubclassExhaustiveStrategy; +import org.mapstruct.SubclassMapping; +import org.mapstruct.factory.Mappers; + +@Mapper(subclassExhaustiveStrategy = SubclassExhaustiveStrategy.RUNTIME_EXCEPTION, + subclassExhaustiveException = CustomSubclassMappingException.class) +public interface MapperSubclassMapper { + + MapperSubclassMapper INSTANCE = Mappers.getMapper( MapperSubclassMapper.class ); + + @SubclassMapping(source = Car.class, target = CarDto.class) + @SubclassMapping(source = Bike.class, target = BikeDto.class) + VehicleDto map(AbstractVehicle vehicle); + + VehicleCollectionDto mapInverse(VehicleCollection vehicles); +}