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 extends Exception> 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 extends Exception> 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 extends Exception> 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>
<#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);
+}