-
-
Notifications
You must be signed in to change notification settings - Fork 1k
Description
Use case
The @SubclassMapping and @BeanMapping(subclassExhaustiveStrategy = SubclassExhaustiveStrategy.RUNTIME_EXCEPTION) annotations can be combined to allow mapping to an abstract class or interface. If a given source input is not one of the specified types, a RuntimeException will be thrown.
@SubclassMapping(target = SubclassTarget.class, source = SubclassSource.class)
@BeanMapping(subclassExhaustiveStrategy = SubclassExhaustiveStrategy.RUNTIME_EXCEPTION)
AbstractTarget map(AbstractSource source);=>
@Override
public AbstractTarget map(AbstractSource source) {
if ( source == null ) {
return null;
}
if (source instanceof SubclassSource) {
return subclassSourceToSubclassTarget( (SubclassSource) source );
}
else {
throw new IllegalArgumentException("Not all subclasses are supported for this mapping. Missing for " + source.getClass());
}
}However, in certain scenarios it is possible to guarantee at compile time that all possible source types are accounted for. In this case, subclassExhaustiveStrategy seems superfluous, and could arguably be dropped. One scenario where we know all source types are accounted for is if the source is a sealed class (or sealed interface), and all classes it permits are handled.
Example
Take the following type mapper & type hierarchy:
@Mapper
public interface SealedSourceMapper {
@SubclassMapping(target = FinalSubclassTarget.class, source = FinalSubclassSource.class)
@SubclassMapping(target = FinalGrandchildTarget.class, source = FinalGrandchildSource.class)
@SubclassMapping(target = NonSealedSubclassTarget.class, source = NonSealedSubclassSource.class)
Target mapToAbstractClass(SealedAbstractClassSource source);public abstract sealed class SealedAbstractClassSource
permits FinalSubclassSource, SealedSubclassSource, NonSealedSubclassSource {
private String property;
public String getProperty() {return property;}
public void setProperty(String property) {this.property = property;}
}
public final class FinalSubclassSource extends SealedAbstractClassSource {
}
public abstract sealed class SealedSubclassSource extends SealedAbstractClassSource
permits FinalGrandchildSource {
}
public final class FinalGrandchildSource extends SealedSubclassSource {
}
public non-sealed class NonSealedSubclassSource extends SealedAbstractClassSource {
}
public abstract class Target {
private String property;
public String getProperty() {return property;}
public void setProperty(String property) {this.property = property;}
}
public class FinalSubclassTarget extends Target {
}
public class FinalGrandchildTarget extends Target {
}
public class NonSealedSubclassTarget extends Target {
}In this case, at compile time, it can be determined that every possible type of the SealedAbstractClassSource source argument is accounted for. Therefore, specifying a subclassExhaustiveStrategy should not be required since it is known at compile time to be exhaustive.
Note: this is just as applicable if the source type is a sealed interface as if it were a sealed class.
Generated Code
public class SealedSourceMapperImpl implements SealedSourceMapper {
@Override
public Target mapToAbstractClass(SealedAbstractClassSource source) {
if ( source == null ) {
return null;
}
if (source instanceof FinalSubclassSource) {
return finalSubclassSourceToFinalSubclassTarget( (FinalSubclassSource) source );
}
else if (source instanceof FinalGrandchildSource) {
return finalGrandchildSourceToFinalGrandchildTarget( (FinalGrandchildSource) source );
}
else if (source instanceof NonSealedSubclassSource) {
return nonSealedSubclassSourceToNonSealedSubclassTarget( (NonSealedSubclassSource) source );
}
else {
// Same exception type as "Pattern Matching for switch" JEP if incompatible separate compilation
throw new IncompatibleClassChangeError("Not all subclasses are supported for this mapping. Missing for " + source.getClass());
}
}
}Possible workarounds
This is a minor quality of life enhancement that can easily be worked around by adding @BeanMapping(subclassExhaustiveStrategy = SubclassExhaustiveStrategy.RUNTIME_EXCEPTION) to the mapping method.
@SubclassMapping(target = FinalSubclassTarget.class, source = FinalSubclassSource.class)
@SubclassMapping(target = FinalGrandchildTarget.class, source = FinalGrandchildSource.class)
@SubclassMapping(target = NonSealedSubclassTarget.class, source = NonSealedSubclassSource.class)
@BeanMapping(subclassExhaustiveStrategy = SubclassExhaustiveStrategy.RUNTIME_EXCEPTION)
Target mapToAbstractClass(SealedAbstractClassSource source);MapStruct Version
org.mapstruct:mapstruct:1.5.3.Final