2696: Invert @SubclassMappings with @InheritInverseConfiguration. (#2708)

* #2696: Added support for '@InheritInverseConfiguration' with '@SubclassMappings'.
* #2696: Overriding of inverse inheritence implemented. New order has preference over inherited order.
This commit is contained in:
Zegveld 2022-02-06 20:03:23 +01:00 committed by GitHub
parent 2a2c11e871
commit 7bb85d05c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 417 additions and 73 deletions

View File

@ -157,6 +157,7 @@ public class BeanMappingMethod extends NormalTypeMappingMethod {
return this; return this;
} }
@Override
public BeanMappingMethod build() { public BeanMappingMethod build() {
BeanMappingOptions beanMapping = method.getOptions().getBeanMapping(); BeanMappingOptions beanMapping = method.getOptions().getBeanMapping();

View File

@ -11,6 +11,7 @@ import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import javax.lang.model.element.AnnotationMirror;
import org.mapstruct.ap.internal.gem.CollectionMappingStrategyGem; import org.mapstruct.ap.internal.gem.CollectionMappingStrategyGem;
import org.mapstruct.ap.internal.model.common.Type; import org.mapstruct.ap.internal.model.common.Type;
@ -35,7 +36,8 @@ public class MappingMethodOptions {
null, null,
null, null,
Collections.emptyList(), Collections.emptyList(),
Collections.emptySet() Collections.emptySet(),
null
); );
private MapperOptions mapper; private MapperOptions mapper;
@ -46,14 +48,16 @@ public class MappingMethodOptions {
private EnumMappingOptions enumMappingOptions; private EnumMappingOptions enumMappingOptions;
private List<ValueMappingOptions> valueMappings; private List<ValueMappingOptions> valueMappings;
private boolean fullyInitialized; private boolean fullyInitialized;
private Set<SubclassMappingOptions> subclassMapping; private Set<SubclassMappingOptions> subclassMappings;
private SubclassValidator subclassValidator;
public MappingMethodOptions(MapperOptions mapper, Set<MappingOptions> mappings, public MappingMethodOptions(MapperOptions mapper, Set<MappingOptions> mappings,
IterableMappingOptions iterableMapping, IterableMappingOptions iterableMapping,
MapMappingOptions mapMapping, BeanMappingOptions beanMapping, MapMappingOptions mapMapping, BeanMappingOptions beanMapping,
EnumMappingOptions enumMappingOptions, EnumMappingOptions enumMappingOptions,
List<ValueMappingOptions> valueMappings, List<ValueMappingOptions> valueMappings,
Set<SubclassMappingOptions> subclassMapping) { Set<SubclassMappingOptions> subclassMappings, SubclassValidator subclassValidator) {
this.mapper = mapper; this.mapper = mapper;
this.mappings = mappings; this.mappings = mappings;
this.iterableMapping = iterableMapping; this.iterableMapping = iterableMapping;
@ -61,7 +65,8 @@ public class MappingMethodOptions {
this.beanMapping = beanMapping; this.beanMapping = beanMapping;
this.enumMappingOptions = enumMappingOptions; this.enumMappingOptions = enumMappingOptions;
this.valueMappings = valueMappings; this.valueMappings = valueMappings;
this.subclassMapping = subclassMapping; this.subclassMappings = subclassMappings;
this.subclassValidator = subclassValidator;
} }
/** /**
@ -102,7 +107,7 @@ public class MappingMethodOptions {
} }
public Set<SubclassMappingOptions> getSubclassMappings() { public Set<SubclassMappingOptions> getSubclassMappings() {
return subclassMapping; return subclassMappings;
} }
public void setIterableMapping(IterableMappingOptions iterableMapping) { public void setIterableMapping(IterableMappingOptions iterableMapping) {
@ -144,10 +149,13 @@ public class MappingMethodOptions {
/** /**
* Merges in all the mapping options configured, giving the already defined options precedence. * Merges in all the mapping options configured, giving the already defined options precedence.
* *
* @param sourceMethod the method which inherits the options.
* @param templateMethod the template method with the options to inherit, may be {@code null} * @param templateMethod the template method with the options to inherit, may be {@code null}
* @param isInverse if {@code true}, the specified options are from an inverse method * @param isInverse if {@code true}, the specified options are from an inverse method
* @param annotationMirror the annotation on which the compile errors will be shown.
*/ */
public void applyInheritedOptions(SourceMethod templateMethod, boolean isInverse) { public void applyInheritedOptions(SourceMethod sourceMethod, SourceMethod templateMethod, boolean isInverse,
AnnotationMirror annotationMirror) {
MappingMethodOptions templateOptions = templateMethod.getOptions(); MappingMethodOptions templateOptions = templateMethod.getOptions();
if ( null != templateOptions ) { if ( null != templateOptions ) {
if ( !getIterableMapping().hasAnnotation() && templateOptions.getIterableMapping().hasAnnotation() ) { if ( !getIterableMapping().hasAnnotation() && templateOptions.getIterableMapping().hasAnnotation() ) {
@ -184,7 +192,7 @@ public class MappingMethodOptions {
} }
else { else {
if ( templateOptions.getValueMappings() != null ) { if ( templateOptions.getValueMappings() != null ) {
// iff there are also inherited mappings, we inverse and add them. // if there are also inherited mappings, we inverse and add them.
for ( ValueMappingOptions inheritedValueMapping : templateOptions.getValueMappings() ) { for ( ValueMappingOptions inheritedValueMapping : templateOptions.getValueMappings() ) {
ValueMappingOptions valueMapping = ValueMappingOptions valueMapping =
isInverse ? inheritedValueMapping.inverse() : inheritedValueMapping; isInverse ? inheritedValueMapping.inverse() : inheritedValueMapping;
@ -196,7 +204,13 @@ public class MappingMethodOptions {
} }
} }
// Do NOT inherit subclass mapping options!!! if ( isInverse ) {
// normal inheritence of subclass mappings will result runtime in infinite loops.
List<SubclassMappingOptions> inheritedMappings = SubclassMappingOptions.copyForInverseInheritance(
templateOptions.getSubclassMappings(),
getBeanMapping() );
addAllNonRedefined( sourceMethod, annotationMirror, inheritedMappings );
}
Set<MappingOptions> newMappings = new LinkedHashSet<>(); Set<MappingOptions> newMappings = new LinkedHashSet<>();
for ( MappingOptions mapping : templateOptions.getMappings() ) { for ( MappingOptions mapping : templateOptions.getMappings() ) {
@ -218,6 +232,21 @@ public class MappingMethodOptions {
} }
} }
private void addAllNonRedefined(SourceMethod sourceMethod, AnnotationMirror annotationMirror,
List<SubclassMappingOptions> inheritedMappings) {
Set<SubclassMappingOptions> redefinedSubclassMappings = new HashSet<>( subclassMappings );
for ( SubclassMappingOptions subclassMappingOption : inheritedMappings ) {
if ( !redefinedSubclassMappings.contains( subclassMappingOption ) ) {
if ( subclassValidator.isValidUsage(
sourceMethod.getExecutable(),
annotationMirror,
subclassMappingOption.getSource() ) ) {
subclassMappings.add( subclassMappingOption );
}
}
}
}
private void addAllNonRedefined(Set<MappingOptions> inheritedMappings) { private void addAllNonRedefined(Set<MappingOptions> inheritedMappings) {
Set<String> redefinedSources = new HashSet<>(); Set<String> redefinedSources = new HashSet<>();
Set<String> redefinedTargets = new HashSet<>(); Set<String> redefinedTargets = new HashSet<>();
@ -328,7 +357,7 @@ public class MappingMethodOptions {
/** /**
* SubclassMappingOptions are not inherited to forged methods. They would result in an infinite loop if they were. * SubclassMappingOptions are not inherited to forged methods. They would result in an infinite loop if they were.
* *
* @return a MappingMethodOptions without SubclassMappingOptions. * @return a MappingMethodOptions without SubclassMappingOptions or SubclassValidator.
*/ */
public static MappingMethodOptions getForgedMethodInheritedOptions(MappingMethodOptions options) { public static MappingMethodOptions getForgedMethodInheritedOptions(MappingMethodOptions options) {
return new MappingMethodOptions( return new MappingMethodOptions(
@ -339,7 +368,8 @@ public class MappingMethodOptions {
options.beanMapping, options.beanMapping,
options.enumMappingOptions, options.enumMappingOptions,
options.valueMappings, options.valueMappings,
Collections.emptySet() ); Collections.emptySet(),
null );
} }
} }

View File

@ -16,15 +16,14 @@ import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier; import javax.lang.model.element.Modifier;
import org.mapstruct.ap.internal.gem.ConditionGem; import org.mapstruct.ap.internal.gem.ConditionGem;
import org.mapstruct.ap.internal.util.TypeUtils; import org.mapstruct.ap.internal.gem.ObjectFactoryGem;
import org.mapstruct.ap.internal.model.common.Accessibility; import org.mapstruct.ap.internal.model.common.Accessibility;
import org.mapstruct.ap.internal.model.common.Parameter; import org.mapstruct.ap.internal.model.common.Parameter;
import org.mapstruct.ap.internal.model.common.Type; import org.mapstruct.ap.internal.model.common.Type;
import org.mapstruct.ap.internal.model.common.TypeFactory; import org.mapstruct.ap.internal.model.common.TypeFactory;
import org.mapstruct.ap.internal.gem.ObjectFactoryGem;
import org.mapstruct.ap.internal.util.Executables; import org.mapstruct.ap.internal.util.Executables;
import org.mapstruct.ap.internal.util.Strings; import org.mapstruct.ap.internal.util.Strings;
import org.mapstruct.ap.internal.util.TypeUtils;
import static org.mapstruct.ap.internal.model.source.MappingMethodUtils.isEnumMapping; import static org.mapstruct.ap.internal.model.source.MappingMethodUtils.isEnumMapping;
import static org.mapstruct.ap.internal.util.Collections.first; import static org.mapstruct.ap.internal.util.Collections.first;
@ -98,6 +97,7 @@ public class SourceMethod implements Method {
private Set<SubclassMappingOptions> subclassMappings; private Set<SubclassMappingOptions> subclassMappings;
private boolean verboseLogging; private boolean verboseLogging;
private SubclassValidator subclassValidator;
public Builder setDeclaringMapper(Type declaringMapper) { public Builder setDeclaringMapper(Type declaringMapper) {
this.declaringMapper = declaringMapper; this.declaringMapper = declaringMapper;
@ -159,6 +159,11 @@ public class SourceMethod implements Method {
return this; return this;
} }
public Builder setSubclassValidator(SubclassValidator subclassValidator) {
this.subclassValidator = subclassValidator;
return this;
}
public Builder setTypeUtils(TypeUtils typeUtils) { public Builder setTypeUtils(TypeUtils typeUtils) {
this.typeUtils = typeUtils; this.typeUtils = typeUtils;
return this; return this;
@ -212,7 +217,8 @@ public class SourceMethod implements Method {
beanMapping, beanMapping,
enumMappingOptions, enumMappingOptions,
valueMappings, valueMappings,
subclassMappings subclassMappings,
subclassValidator
); );
this.typeParameters = this.executable.getTypeParameters() this.typeParameters = this.executable.getTypeParameters()

View File

@ -5,6 +5,7 @@
*/ */
package org.mapstruct.ap.internal.model.source; package org.mapstruct.ap.internal.model.source;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.ExecutableElement;
@ -31,11 +32,13 @@ public class SubclassMappingOptions extends DelegatingOptions {
private final TypeMirror source; private final TypeMirror source;
private final TypeMirror target; private final TypeMirror target;
private final TypeUtils typeUtils;
public SubclassMappingOptions(TypeMirror source, TypeMirror target, DelegatingOptions next) { public SubclassMappingOptions(TypeMirror source, TypeMirror target, TypeUtils typeUtils, DelegatingOptions next) {
super( next ); super( next );
this.source = source; this.source = source;
this.target = target; this.target = target;
this.typeUtils = typeUtils;
} }
@Override @Override
@ -84,7 +87,9 @@ public class SubclassMappingOptions extends DelegatingOptions {
targetSubclass.toString() ); targetSubclass.toString() );
isConsistent = false; isConsistent = false;
} }
subclassValidator.isInCorrectOrder( method, gem.mirror(), targetSubclass ); if ( !subclassValidator.isValidUsage( method, gem.mirror(), sourceSubclass ) ) {
isConsistent = false;
}
return isConsistent; return isConsistent;
} }
@ -115,10 +120,10 @@ public class SubclassMappingOptions extends DelegatingOptions {
public static void addInstances(SubclassMappingsGem gem, ExecutableElement method, public static void addInstances(SubclassMappingsGem gem, ExecutableElement method,
BeanMappingOptions beanMappingOptions, FormattingMessager messager, BeanMappingOptions beanMappingOptions, FormattingMessager messager,
TypeUtils typeUtils, Set<SubclassMappingOptions> mappings, TypeUtils typeUtils, Set<SubclassMappingOptions> mappings,
List<Parameter> sourceParameters, Type resultType) { List<Parameter> sourceParameters, Type resultType,
SubclassValidator subclassValidator = new SubclassValidator( messager, typeUtils ); SubclassValidator subclassValidator) {
for ( SubclassMappingGem subclassMappingGem : gem.value().get() ) { for ( SubclassMappingGem subclassMappingGem : gem.value().get() ) {
addAndValidateInstance( addInstance(
subclassMappingGem, subclassMappingGem,
method, method,
beanMappingOptions, beanMappingOptions,
@ -134,24 +139,8 @@ public class SubclassMappingOptions extends DelegatingOptions {
public static void addInstance(SubclassMappingGem subclassMapping, ExecutableElement method, public static void addInstance(SubclassMappingGem subclassMapping, ExecutableElement method,
BeanMappingOptions beanMappingOptions, FormattingMessager messager, BeanMappingOptions beanMappingOptions, FormattingMessager messager,
TypeUtils typeUtils, Set<SubclassMappingOptions> mappings, TypeUtils typeUtils, Set<SubclassMappingOptions> mappings,
List<Parameter> sourceParameters, Type resultType) { List<Parameter> sourceParameters, Type resultType,
addAndValidateInstance( SubclassValidator subclassValidator) {
subclassMapping,
method,
beanMappingOptions,
messager,
typeUtils,
mappings,
sourceParameters,
resultType,
new SubclassValidator( messager, typeUtils ) );
}
private static void addAndValidateInstance(SubclassMappingGem subclassMapping, ExecutableElement method,
BeanMappingOptions beanMappingOptions, FormattingMessager messager,
TypeUtils typeUtils, Set<SubclassMappingOptions> mappings,
List<Parameter> sourceParameters, Type resultType,
SubclassValidator subclassValidator) {
if ( !isConsistent( if ( !isConsistent(
subclassMapping, subclassMapping,
method, method,
@ -166,6 +155,41 @@ public class SubclassMappingOptions extends DelegatingOptions {
TypeMirror sourceSubclass = subclassMapping.source().getValue(); TypeMirror sourceSubclass = subclassMapping.source().getValue();
TypeMirror targetSubclass = subclassMapping.target().getValue(); TypeMirror targetSubclass = subclassMapping.target().getValue();
mappings.add( new SubclassMappingOptions( sourceSubclass, targetSubclass, beanMappingOptions ) ); mappings
.add(
new SubclassMappingOptions(
sourceSubclass,
targetSubclass,
typeUtils,
beanMappingOptions ) );
}
public static List<SubclassMappingOptions> copyForInverseInheritance(Set<SubclassMappingOptions> subclassMappings,
BeanMappingOptions beanMappingOptions) {
// we are not interested in keeping it unique at this point.
List<SubclassMappingOptions> mappings = new ArrayList<>();
for ( SubclassMappingOptions subclassMapping : subclassMappings ) {
mappings.add(
new SubclassMappingOptions(
subclassMapping.target,
subclassMapping.source,
subclassMapping.typeUtils,
beanMappingOptions ) );
}
return mappings;
}
@Override
public boolean equals(Object obj) {
if ( obj == null || !( obj instanceof SubclassMappingOptions ) ) {
return false;
}
SubclassMappingOptions other = (SubclassMappingOptions) obj;
return typeUtils.isSameType( source, other.source );
}
@Override
public int hashCode() {
return 1; // use a stable value because TypeMirror is not safe to use for hashCode.
} }
} }

View File

@ -20,19 +20,28 @@ import org.mapstruct.ap.internal.util.TypeUtils;
* *
* @author Ben Zegveld * @author Ben Zegveld
*/ */
class SubclassValidator { public class SubclassValidator {
private final FormattingMessager messager; private final FormattingMessager messager;
private final List<TypeMirror> handledSubclasses = new ArrayList<>(); private final List<TypeMirror> handledSubclasses = new ArrayList<>();
private final TypeUtils typeUtils; private final TypeUtils typeUtils;
SubclassValidator(FormattingMessager messager, TypeUtils typeUtils) { public SubclassValidator(FormattingMessager messager, TypeUtils typeUtils) {
this.messager = messager; this.messager = messager;
this.typeUtils = typeUtils; this.typeUtils = typeUtils;
} }
public boolean isInCorrectOrder(Element e, AnnotationMirror annotation, TypeMirror sourceType) { public boolean isValidUsage(Element e, AnnotationMirror annotation, TypeMirror sourceType) {
for ( TypeMirror typeMirror : handledSubclasses ) { for ( TypeMirror typeMirror : handledSubclasses ) {
if ( typeUtils.isSameType( sourceType, typeMirror ) ) {
messager
.printMessage(
e,
annotation,
Message.SUBCLASSMAPPING_DOUBLE_SOURCE_SUBCLASS,
sourceType );
return false;
}
if ( typeUtils.isAssignable( sourceType, typeMirror ) ) { if ( typeUtils.isAssignable( sourceType, typeMirror ) ) {
messager messager
.printMessage( .printMessage(
@ -43,11 +52,11 @@ class SubclassValidator {
typeMirror, typeMirror,
sourceType, sourceType,
typeMirror ); typeMirror );
return true; return false;
} }
} }
handledSubclasses.add( sourceType ); handledSubclasses.add( sourceType );
return false; return true;
} }
} }

View File

@ -14,6 +14,7 @@ import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.SortedSet; import java.util.SortedSet;
import java.util.TreeSet; import java.util.TreeSet;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element; import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind; import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.ExecutableElement;
@ -320,7 +321,7 @@ public class MapperCreationProcessor implements ModelElementProcessor<List<Sourc
continue; continue;
} }
mergeInheritedOptions( method, mapperAnnotation, methods, new ArrayList<>() ); mergeInheritedOptions( method, mapperAnnotation, methods, new ArrayList<>(), null );
MappingMethodOptions mappingOptions = method.getOptions(); MappingMethodOptions mappingOptions = method.getOptions();
@ -462,7 +463,8 @@ public class MapperCreationProcessor implements ModelElementProcessor<List<Sourc
} }
private void mergeInheritedOptions(SourceMethod method, MapperOptions mapperConfig, private void mergeInheritedOptions(SourceMethod method, MapperOptions mapperConfig,
List<SourceMethod> availableMethods, List<SourceMethod> initializingMethods) { List<SourceMethod> availableMethods, List<SourceMethod> initializingMethods,
AnnotationMirror annotationMirror) {
if ( initializingMethods.contains( method ) ) { if ( initializingMethods.contains( method ) ) {
// cycle detected // cycle detected
@ -497,10 +499,10 @@ public class MapperCreationProcessor implements ModelElementProcessor<List<Sourc
// apply defined (@InheritConfiguration, @InheritInverseConfiguration) mappings // apply defined (@InheritConfiguration, @InheritInverseConfiguration) mappings
if ( forwardTemplateMethod != null ) { if ( forwardTemplateMethod != null ) {
mappingOptions.applyInheritedOptions( forwardTemplateMethod, false ); mappingOptions.applyInheritedOptions( method, forwardTemplateMethod, false, annotationMirror );
} }
if ( inverseTemplateMethod != null ) { if ( inverseTemplateMethod != null ) {
mappingOptions.applyInheritedOptions( inverseTemplateMethod, true ); mappingOptions.applyInheritedOptions( method, inverseTemplateMethod, true, annotationMirror );
} }
// apply auto inherited options // apply auto inherited options
@ -510,7 +512,8 @@ public class MapperCreationProcessor implements ModelElementProcessor<List<Sourc
// but.. there should not be an @InheritedConfiguration // but.. there should not be an @InheritedConfiguration
if ( forwardTemplateMethod == null && inheritanceStrategy.isApplyForward() ) { if ( forwardTemplateMethod == null && inheritanceStrategy.isApplyForward() ) {
if ( applicablePrototypeMethods.size() == 1 ) { if ( applicablePrototypeMethods.size() == 1 ) {
mappingOptions.applyInheritedOptions( first( applicablePrototypeMethods ), false ); mappingOptions.applyInheritedOptions( method, first( applicablePrototypeMethods ), false,
annotationMirror );
} }
else if ( applicablePrototypeMethods.size() > 1 ) { else if ( applicablePrototypeMethods.size() > 1 ) {
messager.printMessage( messager.printMessage(
@ -523,7 +526,8 @@ public class MapperCreationProcessor implements ModelElementProcessor<List<Sourc
// or no @InheritInverseConfiguration // or no @InheritInverseConfiguration
if ( inverseTemplateMethod == null && inheritanceStrategy.isApplyReverse() ) { if ( inverseTemplateMethod == null && inheritanceStrategy.isApplyReverse() ) {
if ( applicableReversePrototypeMethods.size() == 1 ) { if ( applicableReversePrototypeMethods.size() == 1 ) {
mappingOptions.applyInheritedOptions( first( applicableReversePrototypeMethods ), true ); mappingOptions.applyInheritedOptions( method, first( applicableReversePrototypeMethods ), true,
annotationMirror );
} }
else if ( applicableReversePrototypeMethods.size() > 1 ) { else if ( applicableReversePrototypeMethods.size() > 1 ) {
messager.printMessage( messager.printMessage(
@ -607,16 +611,23 @@ public class MapperCreationProcessor implements ModelElementProcessor<List<Sourc
} }
} }
return extractInitializedOptions( resultMethod, rawMethods, mapperConfig, initializingMethods ); return extractInitializedOptions( resultMethod, rawMethods, mapperConfig, initializingMethods,
getAnnotationMirror( inverseConfiguration ) );
}
private AnnotationMirror getAnnotationMirror(InheritInverseConfigurationGem inverseConfiguration) {
return inverseConfiguration == null ? null : inverseConfiguration.mirror();
} }
private SourceMethod extractInitializedOptions(SourceMethod resultMethod, private SourceMethod extractInitializedOptions(SourceMethod resultMethod,
List<SourceMethod> rawMethods, List<SourceMethod> rawMethods,
MapperOptions mapperConfig, MapperOptions mapperConfig,
List<SourceMethod> initializingMethods) { List<SourceMethod> initializingMethods,
AnnotationMirror annotationMirror) {
if ( resultMethod != null ) { if ( resultMethod != null ) {
if ( !resultMethod.getOptions().isFullyInitialized() ) { if ( !resultMethod.getOptions().isFullyInitialized() ) {
mergeInheritedOptions( resultMethod, mapperConfig, rawMethods, initializingMethods ); mergeInheritedOptions( resultMethod, mapperConfig, rawMethods, initializingMethods,
annotationMirror );
} }
return resultMethod; return resultMethod;
@ -684,7 +695,12 @@ public class MapperCreationProcessor implements ModelElementProcessor<List<Sourc
} }
} }
return extractInitializedOptions( resultMethod, rawMethods, mapperConfig, initializingMethods ); return extractInitializedOptions( resultMethod, rawMethods, mapperConfig, initializingMethods,
getAnnotationMirror( inheritConfiguration ) );
}
private AnnotationMirror getAnnotationMirror(InheritConfigurationGem inheritConfiguration) {
return inheritConfiguration == null ? null : inheritConfiguration.mirror();
} }
private void reportErrorWhenAmbigousReverseMapping(List<SourceMethod> candidates, SourceMethod method, private void reportErrorWhenAmbigousReverseMapping(List<SourceMethod> candidates, SourceMethod method,

View File

@ -45,6 +45,7 @@ import org.mapstruct.ap.internal.model.source.MappingOptions;
import org.mapstruct.ap.internal.model.source.ParameterProvidedMethods; import org.mapstruct.ap.internal.model.source.ParameterProvidedMethods;
import org.mapstruct.ap.internal.model.source.SourceMethod; import org.mapstruct.ap.internal.model.source.SourceMethod;
import org.mapstruct.ap.internal.model.source.SubclassMappingOptions; import org.mapstruct.ap.internal.model.source.SubclassMappingOptions;
import org.mapstruct.ap.internal.model.source.SubclassValidator;
import org.mapstruct.ap.internal.model.source.ValueMappingOptions; import org.mapstruct.ap.internal.model.source.ValueMappingOptions;
import org.mapstruct.ap.internal.option.Options; import org.mapstruct.ap.internal.option.Options;
import org.mapstruct.ap.internal.util.AccessorNamingUtils; import org.mapstruct.ap.internal.util.AccessorNamingUtils;
@ -307,11 +308,13 @@ public class MethodRetrievalProcessor implements ModelElementProcessor<Void, Lis
// We want to get as much error reporting as possible. // We want to get as much error reporting as possible.
// If targetParameter is not null it means we have an update method // If targetParameter is not null it means we have an update method
SubclassValidator subclassValidator = new SubclassValidator( messager, typeUtils );
Set<SubclassMappingOptions> subclassMappingOptions = getSubclassMappings( Set<SubclassMappingOptions> subclassMappingOptions = getSubclassMappings(
sourceParameters, sourceParameters,
targetParameter != null ? null : resultType, targetParameter != null ? null : resultType,
method, method,
beanMappingOptions beanMappingOptions,
subclassValidator
); );
return new SourceMethod.Builder() return new SourceMethod.Builder()
@ -327,6 +330,7 @@ public class MethodRetrievalProcessor implements ModelElementProcessor<Void, Lis
.setValueMappingOptionss( getValueMappings( method ) ) .setValueMappingOptionss( getValueMappings( method ) )
.setEnumMappingOptions( enumMappingOptions ) .setEnumMappingOptions( enumMappingOptions )
.setSubclassMappings( subclassMappingOptions ) .setSubclassMappings( subclassMappingOptions )
.setSubclassValidator( subclassValidator )
.setTypeUtils( typeUtils ) .setTypeUtils( typeUtils )
.setTypeFactory( typeFactory ) .setTypeFactory( typeFactory )
.setPrototypeMethods( prototypeMethods ) .setPrototypeMethods( prototypeMethods )
@ -601,8 +605,10 @@ public class MethodRetrievalProcessor implements ModelElementProcessor<Void, Lis
* @return The subclass mappings for the given method * @return The subclass mappings for the given method
*/ */
private Set<SubclassMappingOptions> getSubclassMappings(List<Parameter> sourceParameters, Type resultType, private Set<SubclassMappingOptions> getSubclassMappings(List<Parameter> sourceParameters, Type resultType,
ExecutableElement method, BeanMappingOptions beanMapping) { ExecutableElement method, BeanMappingOptions beanMapping,
return new RepeatableSubclassMappings( sourceParameters, resultType ).getMappings( method, beanMapping ); SubclassValidator validator) {
return new RepeatableSubclassMappings( sourceParameters, resultType, validator )
.getMappings( method, beanMapping );
} }
private class RepeatableMappings extends RepeatableMappingAnnotations<MappingGem, MappingsGem, MappingOptions> { private class RepeatableMappings extends RepeatableMappingAnnotations<MappingGem, MappingsGem, MappingOptions> {
@ -637,11 +643,13 @@ public class MethodRetrievalProcessor implements ModelElementProcessor<Void, Lis
extends RepeatableMappingAnnotations<SubclassMappingGem, SubclassMappingsGem, SubclassMappingOptions> { extends RepeatableMappingAnnotations<SubclassMappingGem, SubclassMappingsGem, SubclassMappingOptions> {
private final List<Parameter> sourceParameters; private final List<Parameter> sourceParameters;
private final Type resultType; private final Type resultType;
private SubclassValidator validator;
RepeatableSubclassMappings(List<Parameter> sourceParameters, Type resultType) { RepeatableSubclassMappings(List<Parameter> sourceParameters, Type resultType, SubclassValidator validator) {
super( SUB_CLASS_MAPPING_FQN, SUB_CLASS_MAPPINGS_FQN ); super( SUB_CLASS_MAPPING_FQN, SUB_CLASS_MAPPINGS_FQN );
this.sourceParameters = sourceParameters; this.sourceParameters = sourceParameters;
this.resultType = resultType; this.resultType = resultType;
this.validator = validator;
} }
@Override @Override
@ -666,7 +674,8 @@ public class MethodRetrievalProcessor implements ModelElementProcessor<Void, Lis
typeUtils, typeUtils,
mappings, mappings,
sourceParameters, sourceParameters,
resultType ); resultType,
validator );
} }
@Override @Override
@ -681,7 +690,8 @@ public class MethodRetrievalProcessor implements ModelElementProcessor<Void, Lis
typeUtils, typeUtils,
mappings, mappings,
sourceParameters, sourceParameters,
resultType ); resultType,
validator );
} }
} }

View File

@ -117,6 +117,7 @@ public enum Message {
ENUMMAPPING_NO_ELEMENTS( "'nameTransformationStrategy', 'configuration' and 'unexpectedValueMappingException' are undefined in @EnumMapping, define at least one of them." ), ENUMMAPPING_NO_ELEMENTS( "'nameTransformationStrategy', 'configuration' and 'unexpectedValueMappingException' are undefined in @EnumMapping, define at least one of them." ),
ENUMMAPPING_ILLEGAL_TRANSFORMATION( "Illegal transformation for '%s' EnumTransformationStrategy. Error: '%s'." ), ENUMMAPPING_ILLEGAL_TRANSFORMATION( "Illegal transformation for '%s' EnumTransformationStrategy. Error: '%s'." ),
SUBCLASSMAPPING_DOUBLE_SOURCE_SUBCLASS( "Subclass '%s' is already defined as a source." ),
SUBCLASSMAPPING_ILLEGAL_SUBCLASS( "Class '%s' is not a subclass of '%s'." ), SUBCLASSMAPPING_ILLEGAL_SUBCLASS( "Class '%s' is not a subclass of '%s'." ),
SUBCLASSMAPPING_NO_VALID_SUPERCLASS( "Could not find a parameter that is a superclass for '%s'." ), SUBCLASSMAPPING_NO_VALID_SUPERCLASS( "Could not find a parameter that is a superclass for '%s'." ),
SUBCLASSMAPPING_UPDATE_METHODS_NOT_SUPPORTED( "SubclassMapping annotation can not be used for update methods." ), SUBCLASSMAPPING_UPDATE_METHODS_NOT_SUPPORTED( "SubclassMapping annotation can not be used for update methods." ),

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;
import org.mapstruct.InheritInverseConfiguration;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.SubclassMapping;
import org.mapstruct.ap.test.subclassmapping.mappables.Bike;
import org.mapstruct.ap.test.subclassmapping.mappables.Car;
import org.mapstruct.ap.test.subclassmapping.mappables.Vehicle;
import org.mapstruct.ap.test.subclassmapping.mappables.VehicleDto;
import org.mapstruct.factory.Mappers;
@Mapper
public interface ErroneousInverseSubclassMapper {
ErroneousInverseSubclassMapper INSTANCE = Mappers.getMapper( ErroneousInverseSubclassMapper.class );
@SubclassMapping( source = Bike.class, target = VehicleDto.class )
@SubclassMapping( source = Car.class, target = VehicleDto.class )
@Mapping( target = "maker", source = "vehicleManufacturingCompany" )
VehicleDto map(Vehicle vehicle);
@InheritInverseConfiguration
Vehicle mapInverse(VehicleDto dto);
}

View File

@ -0,0 +1,40 @@
/*
* 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;
import org.mapstruct.InheritInverseConfiguration;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.SubclassMapping;
import org.mapstruct.ap.test.subclassmapping.mappables.Bike;
import org.mapstruct.ap.test.subclassmapping.mappables.BikeDto;
import org.mapstruct.ap.test.subclassmapping.mappables.Car;
import org.mapstruct.ap.test.subclassmapping.mappables.CarDto;
import org.mapstruct.ap.test.subclassmapping.mappables.HatchBack;
import org.mapstruct.ap.test.subclassmapping.mappables.Vehicle;
import org.mapstruct.ap.test.subclassmapping.mappables.VehicleCollection;
import org.mapstruct.ap.test.subclassmapping.mappables.VehicleCollectionDto;
import org.mapstruct.ap.test.subclassmapping.mappables.VehicleDto;
import org.mapstruct.factory.Mappers;
@Mapper
public interface InverseOrderSubclassMapper {
InverseOrderSubclassMapper INSTANCE = Mappers.getMapper( InverseOrderSubclassMapper.class );
VehicleCollectionDto map(VehicleCollection vehicles);
@SubclassMapping( source = HatchBack.class, target = CarDto.class )
@SubclassMapping( source = Car.class, target = CarDto.class )
@SubclassMapping( source = Bike.class, target = BikeDto.class )
@Mapping( source = "vehicleManufacturingCompany", target = "maker")
VehicleDto map(Vehicle vehicle);
VehicleCollection mapInverse(VehicleCollectionDto vehicles);
@SubclassMapping( source = CarDto.class, target = Car.class )
@InheritInverseConfiguration
Vehicle mapInverse(VehicleDto dto);
}

View File

@ -5,6 +5,7 @@
*/ */
package org.mapstruct.ap.test.subclassmapping; package org.mapstruct.ap.test.subclassmapping;
import org.mapstruct.InheritInverseConfiguration;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.Mapping; import org.mapstruct.Mapping;
import org.mapstruct.SubclassMapping; import org.mapstruct.SubclassMapping;
@ -28,4 +29,9 @@ public interface SimpleSubclassMapper {
@SubclassMapping( source = Bike.class, target = BikeDto.class ) @SubclassMapping( source = Bike.class, target = BikeDto.class )
@Mapping( source = "vehicleManufacturingCompany", target = "maker") @Mapping( source = "vehicleManufacturingCompany", target = "maker")
VehicleDto map(Vehicle vehicle); VehicleDto map(Vehicle vehicle);
VehicleCollection mapInverse(VehicleCollectionDto vehicles);
@InheritInverseConfiguration
Vehicle mapInverse(VehicleDto dto);
} }

View File

@ -34,12 +34,11 @@ import static org.assertj.core.api.Assertions.assertThat;
VehicleCollectionDto.class, VehicleCollectionDto.class,
Vehicle.class, Vehicle.class,
VehicleDto.class, VehicleDto.class,
SimpleSubclassMapper.class,
SubclassMapperUsingExistingMappings.class,
}) })
public class SubclassMappingTest { public class SubclassMappingTest {
@ProcessorTest @ProcessorTest
@WithClasses( SimpleSubclassMapper.class )
void mappingIsDoneUsingSubclassMapping() { void mappingIsDoneUsingSubclassMapping() {
VehicleCollection vehicles = new VehicleCollection(); VehicleCollection vehicles = new VehicleCollection();
vehicles.getVehicles().add( new Car() ); vehicles.getVehicles().add( new Car() );
@ -54,6 +53,22 @@ public class SubclassMappingTest {
} }
@ProcessorTest @ProcessorTest
@WithClasses( SimpleSubclassMapper.class )
void inverseMappingIsDoneUsingSubclassMapping() {
VehicleCollectionDto vehicles = new VehicleCollectionDto();
vehicles.getVehicles().add( new CarDto() );
vehicles.getVehicles().add( new BikeDto() );
VehicleCollection result = SimpleSubclassMapper.INSTANCE.mapInverse( vehicles );
assertThat( result.getVehicles() ).doesNotContainNull();
assertThat( result.getVehicles() ) // remove generic so that test works.
.extracting( vehicle -> (Class) vehicle.getClass() )
.containsExactly( Car.class, Bike.class );
}
@ProcessorTest
@WithClasses( SubclassMapperUsingExistingMappings.class )
void existingMappingsAreUsedWhenFound() { void existingMappingsAreUsedWhenFound() {
VehicleCollection vehicles = new VehicleCollection(); VehicleCollection vehicles = new VehicleCollection();
vehicles.getVehicles().add( new Car() ); vehicles.getVehicles().add( new Car() );
@ -66,19 +81,38 @@ public class SubclassMappingTest {
} }
@ProcessorTest @ProcessorTest
void subclassMappingInheritsMapping() { @WithClasses( SimpleSubclassMapper.class )
VehicleCollection vehicles = new VehicleCollection(); void subclassMappingInheritsInverseMapping() {
Car car = new Car(); VehicleCollectionDto vehiclesDto = new VehicleCollectionDto();
car.setVehicleManufacturingCompany( "BenZ" ); CarDto carDto = new CarDto();
vehicles.getVehicles().add( car ); carDto.setMaker( "BenZ" );
vehiclesDto.getVehicles().add( carDto );
VehicleCollectionDto result = SimpleSubclassMapper.INSTANCE.map( vehicles ); VehicleCollection result = SimpleSubclassMapper.INSTANCE.mapInverse( vehiclesDto );
assertThat( result.getVehicles() ) assertThat( result.getVehicles() )
.extracting( VehicleDto::getMaker ) .extracting( Vehicle::getVehicleManufacturingCompany )
.containsExactly( "BenZ" ); .containsExactly( "BenZ" );
} }
@ProcessorTest
@WithClasses( {
HatchBack.class,
InverseOrderSubclassMapper.class
} )
void subclassMappingOverridesInverseInheritsMapping() {
VehicleCollectionDto vehicleDtos = new VehicleCollectionDto();
CarDto carDto = new CarDto();
carDto.setMaker( "BenZ" );
vehicleDtos.getVehicles().add( carDto );
VehicleCollection result = InverseOrderSubclassMapper.INSTANCE.mapInverse( vehicleDtos );
assertThat( result.getVehicles() ) // remove generic so that test works.
.extracting( vehicle -> (Class) vehicle.getClass() )
.containsExactly( Car.class );
}
@ProcessorTest @ProcessorTest
@WithClasses({ @WithClasses({
HatchBack.class, HatchBack.class,
@ -91,11 +125,11 @@ public class SubclassMappingTest {
line = 28, line = 28,
alternativeLine = 30, alternativeLine = 30,
message = "SubclassMapping annotation for " message = "SubclassMapping annotation for "
+ "'org.mapstruct.ap.test.subclassmapping.mappables.HatchBackDto' found after " + "'org.mapstruct.ap.test.subclassmapping.mappables.HatchBack' found after "
+ "'org.mapstruct.ap.test.subclassmapping.mappables.CarDto', but all " + "'org.mapstruct.ap.test.subclassmapping.mappables.Car', but all "
+ "'org.mapstruct.ap.test.subclassmapping.mappables.HatchBackDto' " + "'org.mapstruct.ap.test.subclassmapping.mappables.HatchBack' "
+ "objects are also instances of " + "objects are also instances of "
+ "'org.mapstruct.ap.test.subclassmapping.mappables.CarDto'.") + "'org.mapstruct.ap.test.subclassmapping.mappables.Car'.")
}) })
void subclassOrderWarning() { void subclassOrderWarning() {
} }
@ -136,4 +170,18 @@ public class SubclassMappingTest {
}) })
void erroneousMethodWithSourceTargetType() { void erroneousMethodWithSourceTargetType() {
} }
@ProcessorTest
@WithClasses({ ErroneousInverseSubclassMapper.class })
@ExpectedCompilationOutcome( value = CompilationResult.FAILED, diagnostics = {
@Diagnostic(type = ErroneousInverseSubclassMapper.class,
kind = javax.tools.Diagnostic.Kind.ERROR,
line = 28,
message = "Subclass "
+ "'org.mapstruct.ap.test.subclassmapping.mappables.VehicleDto'"
+ " is already defined as a source."
)
})
void inverseSubclassMappingNotPossible() {
}
} }

View File

@ -0,0 +1,18 @@
/*
* 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.fixture;
public class SubSourceOverride extends ImplementedParentSource implements InterfaceParentSource {
private final String finalValue;
public SubSourceOverride(String finalValue) {
this.finalValue = finalValue;
}
public String getFinalValue() {
return finalValue;
}
}

View File

@ -0,0 +1,18 @@
/*
* 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.fixture;
public class SubSourceSeparate extends ImplementedParentSource implements InterfaceParentSource {
private final String separateValue;
public SubSourceSeparate(String separateValue) {
this.separateValue = separateValue;
}
public String getSeparateValue() {
return separateValue;
}
}

View File

@ -0,0 +1,18 @@
/*
* 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.fixture;
public class SubTargetSeparate extends ImplementedParentTarget implements InterfaceParentTarget {
private final String separateValue;
public SubTargetSeparate(String separateValue) {
this.separateValue = separateValue;
}
public String getSeparateValue() {
return separateValue;
}
}

View File

@ -6,6 +6,7 @@
package org.mapstruct.ap.test.subclassmapping.fixture; package org.mapstruct.ap.test.subclassmapping.fixture;
import org.mapstruct.BeanMapping; import org.mapstruct.BeanMapping;
import org.mapstruct.InheritInverseConfiguration;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.SubclassMapping; import org.mapstruct.SubclassMapping;
@ -18,4 +19,9 @@ public interface SubclassAbstractMapper {
@SubclassMapping( source = SubSource.class, target = SubTarget.class ) @SubclassMapping( source = SubSource.class, target = SubTarget.class )
@SubclassMapping( source = SubSourceOther.class, target = SubTargetOther.class ) @SubclassMapping( source = SubSourceOther.class, target = SubTargetOther.class )
AbstractParentTarget map(AbstractParentSource item); AbstractParentTarget map(AbstractParentSource item);
@SubclassMapping( source = SubTargetSeparate.class, target = SubSourceSeparate.class )
@InheritInverseConfiguration
@SubclassMapping( source = SubTargetOther.class, target = SubSourceOverride.class )
AbstractParentSource map(AbstractParentTarget item);
} }

View File

@ -6,6 +6,7 @@
package org.mapstruct.ap.test.subclassmapping.fixture; package org.mapstruct.ap.test.subclassmapping.fixture;
import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.extension.RegisterExtension;
import org.mapstruct.ap.testutil.ProcessorTest; import org.mapstruct.ap.testutil.ProcessorTest;
import org.mapstruct.ap.testutil.WithClasses; import org.mapstruct.ap.testutil.WithClasses;
import org.mapstruct.ap.testutil.runner.GeneratedSource; import org.mapstruct.ap.testutil.runner.GeneratedSource;
@ -37,6 +38,9 @@ public class SubclassFixtureTest {
@ProcessorTest @ProcessorTest
@WithClasses( { @WithClasses( {
SubSourceOverride.class,
SubSourceSeparate.class,
SubTargetSeparate.class,
SubclassAbstractMapper.class, SubclassAbstractMapper.class,
} ) } )
void subclassAbstractParentFixture() { void subclassAbstractParentFixture() {

View File

@ -9,7 +9,7 @@ import javax.annotation.processing.Generated;
@Generated( @Generated(
value = "org.mapstruct.ap.MappingProcessor", value = "org.mapstruct.ap.MappingProcessor",
date = "2021-09-12T14:37:10+0200", date = "2022-01-31T19:35:15+0100",
comments = "version: , compiler: Eclipse JDT (Batch) 3.20.0.v20191203-2131, environment: Java 11.0.12 (Azul Systems, Inc.)" comments = "version: , compiler: Eclipse JDT (Batch) 3.20.0.v20191203-2131, environment: Java 11.0.12 (Azul Systems, Inc.)"
) )
public class SubclassAbstractMapperImpl implements SubclassAbstractMapper { public class SubclassAbstractMapperImpl implements SubclassAbstractMapper {
@ -31,6 +31,26 @@ public class SubclassAbstractMapperImpl implements SubclassAbstractMapper {
} }
} }
@Override
public AbstractParentSource map(AbstractParentTarget item) {
if ( item == null ) {
return null;
}
if (item instanceof SubTargetSeparate) {
return subTargetSeparateToSubSourceSeparate( (SubTargetSeparate) item );
}
else if (item instanceof SubTargetOther) {
return subTargetOtherToSubSourceOverride( (SubTargetOther) item );
}
else if (item instanceof SubTarget) {
return subTargetToSubSource( (SubTarget) item );
}
else {
throw new IllegalArgumentException("Not all subclasses are supported for this mapping. Missing for " + item.getClass());
}
}
protected SubTarget subSourceToSubTarget(SubSource subSource) { protected SubTarget subSourceToSubTarget(SubSource subSource) {
if ( subSource == null ) { if ( subSource == null ) {
return null; return null;
@ -56,4 +76,44 @@ public class SubclassAbstractMapperImpl implements SubclassAbstractMapper {
return subTargetOther; return subTargetOther;
} }
protected SubSourceSeparate subTargetSeparateToSubSourceSeparate(SubTargetSeparate subTargetSeparate) {
if ( subTargetSeparate == null ) {
return null;
}
String separateValue = null;
separateValue = subTargetSeparate.getSeparateValue();
SubSourceSeparate subSourceSeparate = new SubSourceSeparate( separateValue );
return subSourceSeparate;
}
protected SubSourceOverride subTargetOtherToSubSourceOverride(SubTargetOther subTargetOther) {
if ( subTargetOther == null ) {
return null;
}
String finalValue = null;
finalValue = subTargetOther.getFinalValue();
SubSourceOverride subSourceOverride = new SubSourceOverride( finalValue );
return subSourceOverride;
}
protected SubSource subTargetToSubSource(SubTarget subTarget) {
if ( subTarget == null ) {
return null;
}
SubSource subSource = new SubSource();
subSource.setValue( subTarget.getValue() );
return subSource;
}
} }