#3821: Add support for custom exception for subclass exhaustive strategy for @SubclassMapping

---------

Signed-off-by: TangYang <tangyang9464@163.com>
This commit is contained in:
Yang Tang 2025-05-18 00:40:51 +08:00 committed by GitHub
parent fce73aee6a
commit 6e6fd01a2e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 248 additions and 3 deletions

View File

@ -132,6 +132,18 @@ public @interface BeanMapping {
*/ */
SubclassExhaustiveStrategy subclassExhaustiveStrategy() default COMPILE_ERROR; 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()}.
* <p>
* 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 * 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. * warning will be issued on missing source or target properties.

View File

@ -281,6 +281,18 @@ public @interface Mapper {
*/ */
SubclassExhaustiveStrategy subclassExhaustiveStrategy() default COMPILE_ERROR; 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()}.
* <p>
* 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 * Determines whether to use field or constructor injection. This is only used on annotated based component models
* such as CDI, Spring and JSR 330. * such as CDI, Spring and JSR 330.

View File

@ -249,6 +249,18 @@ public @interface MapperConfig {
*/ */
SubclassExhaustiveStrategy subclassExhaustiveStrategy() default COMPILE_ERROR; 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()}.
* <p>
* 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 * Determines whether to use field or constructor injection. This is only used on annotated based component models
* such as CDI, Spring and JSR 330. * such as CDI, Spring and JSR 330.

View File

@ -100,6 +100,7 @@ public class BeanMappingMethod extends NormalTypeMappingMethod {
private final String finalizedResultName; private final String finalizedResultName;
private final List<LifecycleCallbackMethodReference> beforeMappingReferencesWithFinalizedReturnType; private final List<LifecycleCallbackMethodReference> beforeMappingReferencesWithFinalizedReturnType;
private final List<LifecycleCallbackMethodReference> afterMappingReferencesWithFinalizedReturnType; private final List<LifecycleCallbackMethodReference> afterMappingReferencesWithFinalizedReturnType;
private final Type subclassExhaustiveException;
private final MappingReferences mappingReferences; 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<SubclassMapping> subclasses = new ArrayList<>(); List<SubclassMapping> subclasses = new ArrayList<>();
for ( SubclassMappingOptions subclassMappingOptions : method.getOptions().getSubclassMappings() ) { for ( SubclassMappingOptions subclassMappingOptions : method.getOptions().getSubclassMappings() ) {
subclasses.add( createSubclassMapping( subclassMappingOptions ) ); subclasses.add( createSubclassMapping( subclassMappingOptions ) );
@ -451,7 +457,8 @@ public class BeanMappingMethod extends NormalTypeMappingMethod {
finalizeMethod, finalizeMethod,
mappingReferences, mappingReferences,
subclasses, subclasses,
presenceChecksByParameter presenceChecksByParameter,
subclassExhaustiveExceptionType
); );
} }
@ -1954,7 +1961,8 @@ public class BeanMappingMethod extends NormalTypeMappingMethod {
MethodReference finalizerMethod, MethodReference finalizerMethod,
MappingReferences mappingReferences, MappingReferences mappingReferences,
List<SubclassMapping> subclassMappings, List<SubclassMapping> subclassMappings,
Map<String, PresenceCheck> presenceChecksByParameter) { Map<String, PresenceCheck> presenceChecksByParameter,
Type subclassExhaustiveException) {
super( super(
method, method,
annotations, annotations,
@ -1969,6 +1977,7 @@ public class BeanMappingMethod extends NormalTypeMappingMethod {
this.propertyMappings = propertyMappings; this.propertyMappings = propertyMappings;
this.returnTypeBuilder = returnTypeBuilder; this.returnTypeBuilder = returnTypeBuilder;
this.finalizerMethod = finalizerMethod; this.finalizerMethod = finalizerMethod;
this.subclassExhaustiveException = subclassExhaustiveException;
if ( this.finalizerMethod != null ) { if ( this.finalizerMethod != null ) {
this.finalizedResultName = this.finalizedResultName =
Strings.getSafeVariableName( getResultName() + "Result", existingVariableNames ); Strings.getSafeVariableName( getResultName() + "Result", existingVariableNames );
@ -2017,6 +2026,10 @@ public class BeanMappingMethod extends NormalTypeMappingMethod {
this.subclassMappings = subclassMappings; this.subclassMappings = subclassMappings;
} }
public Type getSubclassExhaustiveException() {
return subclassExhaustiveException;
}
public List<PropertyMapping> getConstantMappings() { public List<PropertyMapping> getConstantMappings() {
return constantMappings; return constantMappings;
} }

View File

@ -11,6 +11,7 @@ import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.ExecutableElement; 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.BeanMappingGem;
import org.mapstruct.ap.internal.gem.BuilderGem; import org.mapstruct.ap.internal.gem.BuilderGem;
@ -182,6 +183,14 @@ public class BeanMappingOptions extends DelegatingOptions {
.orElse( next().getSubclassExhaustiveStrategy() ); .orElse( next().getSubclassExhaustiveStrategy() );
} }
@Override
public TypeMirror getSubclassExhaustiveException() {
return Optional.ofNullable( beanMapping ).map( BeanMappingGem::subclassExhaustiveException )
.filter( GemValue::hasValue )
.map( GemValue::getValue )
.orElse( next().getSubclassExhaustiveException() );
}
@Override @Override
public ReportingPolicyGem unmappedTargetPolicy() { public ReportingPolicyGem unmappedTargetPolicy() {
return Optional.ofNullable( beanMapping ).map( BeanMappingGem::unmappedTargetPolicy ) return Optional.ofNullable( beanMapping ).map( BeanMappingGem::unmappedTargetPolicy )

View File

@ -131,6 +131,10 @@ public class DefaultOptions extends DelegatingOptions {
return SubclassExhaustiveStrategyGem.valueOf( mapper.subclassExhaustiveStrategy().getDefaultValue() ); return SubclassExhaustiveStrategyGem.valueOf( mapper.subclassExhaustiveStrategy().getDefaultValue() );
} }
public TypeMirror getSubclassExhaustiveException() {
return mapper.subclassExhaustiveException().getDefaultValue();
}
public NullValueMappingStrategyGem getNullValueIterableMappingStrategy() { public NullValueMappingStrategyGem getNullValueIterableMappingStrategy() {
NullValueMappingStrategyGem nullValueIterableMappingStrategy = options.getNullValueIterableMappingStrategy(); NullValueMappingStrategyGem nullValueIterableMappingStrategy = options.getNullValueIterableMappingStrategy();
if ( nullValueIterableMappingStrategy != null ) { if ( nullValueIterableMappingStrategy != null ) {

View File

@ -106,6 +106,10 @@ public abstract class DelegatingOptions {
return next.getSubclassExhaustiveStrategy(); return next.getSubclassExhaustiveStrategy();
} }
public TypeMirror getSubclassExhaustiveException() {
return next.getSubclassExhaustiveException();
}
public NullValueMappingStrategyGem getNullValueIterableMappingStrategy() { public NullValueMappingStrategyGem getNullValueIterableMappingStrategy() {
return next.getNullValueIterableMappingStrategy(); return next.getNullValueIterableMappingStrategy();
} }

View File

@ -141,6 +141,12 @@ public class MapperConfigOptions extends DelegatingOptions {
next().getSubclassExhaustiveStrategy(); next().getSubclassExhaustiveStrategy();
} }
public TypeMirror getSubclassExhaustiveException() {
return mapperConfig.subclassExhaustiveException().hasValue() ?
mapperConfig.subclassExhaustiveException().get() :
next().getSubclassExhaustiveException();
}
@Override @Override
public NullValueMappingStrategyGem getNullValueIterableMappingStrategy() { public NullValueMappingStrategyGem getNullValueIterableMappingStrategy() {
if ( mapperConfig.nullValueIterableMappingStrategy().hasValue() ) { if ( mapperConfig.nullValueIterableMappingStrategy().hasValue() ) {

View File

@ -170,6 +170,13 @@ public class MapperOptions extends DelegatingOptions {
next().getSubclassExhaustiveStrategy(); next().getSubclassExhaustiveStrategy();
} }
@Override
public TypeMirror getSubclassExhaustiveException() {
return mapper.subclassExhaustiveException().hasValue() ?
mapper.subclassExhaustiveException().get() :
next().getSubclassExhaustiveException();
}
@Override @Override
public NullValueMappingStrategyGem getNullValueIterableMappingStrategy() { public NullValueMappingStrategyGem getNullValueIterableMappingStrategy() {
if ( mapper.nullValueIterableMappingStrategy().hasValue() ) { if ( mapper.nullValueIterableMappingStrategy().hasValue() ) {

View File

@ -42,7 +42,7 @@
else { else {
</#if> </#if>
<#if isAbstractReturnType()> <#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> <#else>
<#if !existingInstanceMapping> <#if !existingInstanceMapping>
<#if hasConstructorMappings()> <#if hasConstructorMappings()>

View File

@ -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);
}

View File

@ -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 );
}
}

View File

@ -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 );
}
}

View File

@ -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);
}

View File

@ -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);
}