#1454 Support for lifecycle methods on type being built with builders

Add missing support for lifecycle methods with builders:

* `@BeforeMapping` with `@TargetType` the type being build
* `@AftereMapping` with `@TargetType` the type being build
* `@AfterMapping` with `@MappingTarget` the type being build
This commit is contained in:
Oliver Erhart 2023-05-21 22:49:41 +02:00 committed by GitHub
parent 7c90592d05
commit 6d205e5bc4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 119 additions and 14 deletions

View File

@ -29,7 +29,9 @@
<!-- See http://checkstyle.sf.net/config_misc.html#Translation --> <!-- See http://checkstyle.sf.net/config_misc.html#Translation -->
<module name="Translation"/> <module name="Translation"/>
<module name="FileLength"/> <module name="FileLength">
<property name="max" value="2500"/>
</module>
<module name="LineLength"> <module name="LineLength">
<property name="max" value="120"/> <property name="max" value="120"/>

View File

@ -248,9 +248,8 @@ All before/after-mapping methods that *can* be applied to a mapping method *will
The order of the method invocation is determined primarily by their variant: The order of the method invocation is determined primarily by their variant:
1. `@BeforeMapping` methods without an `@MappingTarget` parameter are called before any null-checks on source 1. `@BeforeMapping` methods without parameters, a `@MappingTarget` parameter or a `@TargetType` parameter are called before any null-checks on source parameters and constructing a new target bean.
parameters and constructing a new target bean. 2. `@BeforeMapping` methods with a `@MappingTarget` parameter are called after constructing a new target bean.
2. `@BeforeMapping` methods with an `@MappingTarget` parameter are called after constructing a new target bean.
3. `@AfterMapping` methods are called at the end of the mapping method before the last `return` statement. 3. `@AfterMapping` methods are called at the end of the mapping method before the last `return` statement.
Within those groups, the method invocations are ordered by their location of definition: Within those groups, the method invocations are ordered by their location of definition:
@ -262,4 +261,11 @@ Within those groups, the method invocations are ordered by their location of def
*Important:* the order of methods declared within one type can not be guaranteed, as it depends on the compiler and the processing environment implementation. *Important:* the order of methods declared within one type can not be guaranteed, as it depends on the compiler and the processing environment implementation.
*Important:* when using a builder, the `@AfterMapping` annotated method must have the builder as `@MappingTarget` annotated parameter so that the method is able to modify the object going to be build. The `build` method is called when the `@AfterMapping` annotated method scope finishes. MapStruct will not call the `@AfterMapping` annotated method if the real target is used as `@MappingTarget` annotated parameter. [NOTE]
====
Before/After-mapping methods can also be used with builders:
* `@BeforeMapping` methods with a `@MappingTarget` parameter of the real target will not be invoked because it is only available after the mapping was already performed.
* To be able to modify the object that is going to be built, the `@AfterMapping` annotated method must have the builder as `@MappingTarget` annotated parameter. The `build` method is called when the `@AfterMapping` annotated method scope finishes.
* The `@AfterMapping` annotated method can also have the real target as `@TargetType` or `@MappingTarget`. It will be invoked after the real target was built (first the methods annotated with `@TargetType`, then the methods annotated with `@MappingTarget`)
====

View File

@ -71,7 +71,7 @@ public class GolfPlayerDto {
public GolfPlayerDto withName(String name) { public GolfPlayerDto withName(String name) {
this.name = name; this.name = name;
return this return this;
} }
} }
---- ----

View File

@ -94,6 +94,9 @@ public class BeanMappingMethod extends NormalTypeMappingMethod {
private final Type returnTypeToConstruct; private final Type returnTypeToConstruct;
private final BuilderType returnTypeBuilder; private final BuilderType returnTypeBuilder;
private final MethodReference finalizerMethod; private final MethodReference finalizerMethod;
private final String finalizedResultName;
private final List<LifecycleCallbackMethodReference> beforeMappingReferencesWithFinalizedReturnType;
private final List<LifecycleCallbackMethodReference> afterMappingReferencesWithFinalizedReturnType;
private final MappingReferences mappingReferences; private final MappingReferences mappingReferences;
@ -368,8 +371,35 @@ public class BeanMappingMethod extends NormalTypeMappingMethod {
MethodReference finalizeMethod = null; MethodReference finalizeMethod = null;
List<LifecycleCallbackMethodReference> beforeMappingReferencesWithFinalizedReturnType = new ArrayList<>();
List<LifecycleCallbackMethodReference> afterMappingReferencesWithFinalizedReturnType = new ArrayList<>();
if ( shouldCallFinalizerMethod( returnTypeToConstruct ) ) { if ( shouldCallFinalizerMethod( returnTypeToConstruct ) ) {
finalizeMethod = getFinalizerMethod(); finalizeMethod = getFinalizerMethod();
Type actualReturnType = method.getReturnType();
beforeMappingReferencesWithFinalizedReturnType.addAll( filterMappingTarget(
LifecycleMethodResolver.beforeMappingMethods(
method,
actualReturnType,
selectionParameters,
ctx,
existingVariableNames
),
false
) );
afterMappingReferencesWithFinalizedReturnType.addAll( LifecycleMethodResolver.afterMappingMethods(
method,
actualReturnType,
selectionParameters,
ctx,
existingVariableNames
) );
// remove methods without parameters as they are already being invoked
removeMappingReferencesWithoutSourceParameters( beforeMappingReferencesWithFinalizedReturnType );
removeMappingReferencesWithoutSourceParameters( afterMappingReferencesWithFinalizedReturnType );
} }
return new BeanMappingMethod( return new BeanMappingMethod(
@ -383,12 +413,18 @@ public class BeanMappingMethod extends NormalTypeMappingMethod {
returnTypeBuilder, returnTypeBuilder,
beforeMappingMethods, beforeMappingMethods,
afterMappingMethods, afterMappingMethods,
beforeMappingReferencesWithFinalizedReturnType,
afterMappingReferencesWithFinalizedReturnType,
finalizeMethod, finalizeMethod,
mappingReferences, mappingReferences,
subclasses subclasses
); );
} }
private void removeMappingReferencesWithoutSourceParameters(List<LifecycleCallbackMethodReference> references) {
references.removeIf( r -> r.getSourceParameters().isEmpty() && r.getReturnType().isVoid() );
}
private boolean doesNotAllowAbstractReturnTypeAndCanBeConstructed(Type returnTypeImpl) { private boolean doesNotAllowAbstractReturnTypeAndCanBeConstructed(Type returnTypeImpl) {
return !isAbstractReturnTypeAllowed() return !isAbstractReturnTypeAllowed()
&& canReturnTypeBeConstructed( returnTypeImpl ); && canReturnTypeBeConstructed( returnTypeImpl );
@ -706,7 +742,6 @@ public class BeanMappingMethod extends NormalTypeMappingMethod {
* Find a factory method for a return type or for a builder. * Find a factory method for a return type or for a builder.
* @param returnTypeImpl the return type implementation to construct * @param returnTypeImpl the return type implementation to construct
* @param @selectionParameters * @param @selectionParameters
* @return
*/ */
private void initializeFactoryMethod(Type returnTypeImpl, SelectionParameters selectionParameters) { private void initializeFactoryMethod(Type returnTypeImpl, SelectionParameters selectionParameters) {
List<SelectedMethod<SourceMethod>> matchingFactoryMethods = List<SelectedMethod<SourceMethod>> matchingFactoryMethods =
@ -1380,7 +1415,7 @@ public class BeanMappingMethod extends NormalTypeMappingMethod {
* <p> * <p>
* When a target property matches its name with the (nested) source property, it is added to the list if and * When a target property matches its name with the (nested) source property, it is added to the list if and
* only if it is an unprocessed target property. * only if it is an unprocessed target property.
* * <p>
* duplicates will be handled by {@link #applyPropertyNameBasedMapping(List)} * duplicates will be handled by {@link #applyPropertyNameBasedMapping(List)}
*/ */
private void applyTargetThisMapping() { private void applyTargetThisMapping() {
@ -1766,6 +1801,8 @@ public class BeanMappingMethod extends NormalTypeMappingMethod {
BuilderType returnTypeBuilder, BuilderType returnTypeBuilder,
List<LifecycleCallbackMethodReference> beforeMappingReferences, List<LifecycleCallbackMethodReference> beforeMappingReferences,
List<LifecycleCallbackMethodReference> afterMappingReferences, List<LifecycleCallbackMethodReference> afterMappingReferences,
List<LifecycleCallbackMethodReference> beforeMappingReferencesWithFinalizedReturnType,
List<LifecycleCallbackMethodReference> afterMappingReferencesWithFinalizedReturnType,
MethodReference finalizerMethod, MethodReference finalizerMethod,
MappingReferences mappingReferences, MappingReferences mappingReferences,
List<SubclassMapping> subclassMappings) { List<SubclassMapping> subclassMappings) {
@ -1783,9 +1820,20 @@ public class BeanMappingMethod extends NormalTypeMappingMethod {
this.propertyMappings = propertyMappings; this.propertyMappings = propertyMappings;
this.returnTypeBuilder = returnTypeBuilder; this.returnTypeBuilder = returnTypeBuilder;
this.finalizerMethod = finalizerMethod; this.finalizerMethod = finalizerMethod;
if ( this.finalizerMethod != null ) {
this.finalizedResultName =
Strings.getSafeVariableName( getResultName() + "Result", existingVariableNames );
existingVariableNames.add( this.finalizedResultName );
}
else {
this.finalizedResultName = null;
}
this.mappingReferences = mappingReferences; this.mappingReferences = mappingReferences;
// intialize constant mappings as all mappings, but take out the ones that can be contributed to a this.beforeMappingReferencesWithFinalizedReturnType = beforeMappingReferencesWithFinalizedReturnType;
this.afterMappingReferencesWithFinalizedReturnType = afterMappingReferencesWithFinalizedReturnType;
// initialize constant mappings as all mappings, but take out the ones that can be contributed to a
// parameter mapping. // parameter mapping.
this.mappingsByParameter = new HashMap<>(); this.mappingsByParameter = new HashMap<>();
this.constantMappings = new ArrayList<>( propertyMappings.size() ); this.constantMappings = new ArrayList<>( propertyMappings.size() );
@ -1830,6 +1878,18 @@ public class BeanMappingMethod extends NormalTypeMappingMethod {
return subclassMappings; return subclassMappings;
} }
public String getFinalizedResultName() {
return finalizedResultName;
}
public List<LifecycleCallbackMethodReference> getBeforeMappingReferencesWithFinalizedReturnType() {
return beforeMappingReferencesWithFinalizedReturnType;
}
public List<LifecycleCallbackMethodReference> getAfterMappingReferencesWithFinalizedReturnType() {
return afterMappingReferencesWithFinalizedReturnType;
}
public List<PropertyMapping> propertyMappingsByParameter(Parameter parameter) { public List<PropertyMapping> propertyMappingsByParameter(Parameter parameter) {
// issues: #909 and #1244. FreeMarker has problem getting values from a map when the search key is size or value // issues: #909 and #1244. FreeMarker has problem getting values from a map when the search key is size or value
return mappingsByParameter.getOrDefault( parameter.getName(), Collections.emptyList() ); return mappingsByParameter.getOrDefault( parameter.getName(), Collections.emptyList() );
@ -1882,6 +1942,12 @@ public class BeanMappingMethod extends NormalTypeMappingMethod {
if ( returnTypeBuilder != null ) { if ( returnTypeBuilder != null ) {
types.add( returnTypeBuilder.getOwningType() ); types.add( returnTypeBuilder.getOwningType() );
} }
for ( LifecycleCallbackMethodReference reference : beforeMappingReferencesWithFinalizedReturnType ) {
types.addAll( reference.getImportTypes() );
}
for ( LifecycleCallbackMethodReference reference : afterMappingReferencesWithFinalizedReturnType ) {
types.addAll( reference.getImportTypes() );
}
return types; return types;
} }

View File

@ -186,8 +186,8 @@ public abstract class MappingMethod extends ModelElement {
return returnType + " " + getName() + "(" + join( parameters, ", " ) + ")"; return returnType + " " + getName() + "(" + join( parameters, ", " ) + ")";
} }
private List<LifecycleCallbackMethodReference> filterMappingTarget(List<LifecycleCallbackMethodReference> methods, protected static List<LifecycleCallbackMethodReference> filterMappingTarget(
boolean mustHaveMappingTargetParameter) { List<LifecycleCallbackMethodReference> methods, boolean mustHaveMappingTargetParameter) {
if ( methods == null ) { if ( methods == null ) {
return Collections.emptyList(); return Collections.emptyList();
} }

View File

@ -21,6 +21,12 @@
</#if> </#if>
</#list> </#list>
<#list beforeMappingReferencesWithFinalizedReturnType as callback>
<@includeModel object=callback targetBeanName=finalizedResultName targetType=returnType/>
<#if !callback_has_next>
</#if>
</#list>
<#if !mapNullToDefault> <#if !mapNullToDefault>
if ( <#list sourceParametersExcludingPrimitives as sourceParam>${sourceParam.name} == null<#if sourceParam_has_next> && </#if></#list> ) { if ( <#list sourceParametersExcludingPrimitives as sourceParam>${sourceParam.name} == null<#if sourceParam_has_next> && </#if></#list> ) {
return<#if returnType.name != "void"> <#if existingInstanceMapping>${resultName}<#if finalizerMethod??>.<@includeModel object=finalizerMethod /></#if><#else>null</#if></#if>; return<#if returnType.name != "void"> <#if existingInstanceMapping>${resultName}<#if finalizerMethod??>.<@includeModel object=finalizerMethod /></#if><#else>null</#if></#if>;
@ -129,7 +135,20 @@
<#if returnType.name != "void"> <#if returnType.name != "void">
<#if finalizerMethod??> <#if finalizerMethod??>
return ${resultName}.<@includeModel object=finalizerMethod />; <#if (afterMappingReferencesWithFinalizedReturnType?size > 0)>
${returnType.name} ${finalizedResultName} = ${resultName}.<@includeModel object=finalizerMethod />;
<#list afterMappingReferencesWithFinalizedReturnType as callback>
<#if callback_index = 0>
</#if>
<@includeModel object=callback targetBeanName=finalizedResultName targetType=returnType/>
</#list>
return ${finalizedResultName};
<#else>
return ${resultName}.<@includeModel object=finalizerMethod />;
</#if>
<#else> <#else>
return ${resultName}; return ${resultName};
</#if> </#if>

View File

@ -43,12 +43,16 @@ public class BuilderLifecycleCallbacksTest {
assertThat( context.getInvokedMethods() ) assertThat( context.getInvokedMethods() )
.contains( .contains(
"beforeWithoutParameters", "beforeWithoutParameters",
"beforeWithTargetType",
"beforeWithBuilderTargetType", "beforeWithBuilderTargetType",
"beforeWithBuilderTarget", "beforeWithBuilderTarget",
"afterWithoutParameters", "afterWithoutParameters",
"afterWithBuilderTargetType", "afterWithBuilderTargetType",
"afterWithBuilderTarget", "afterWithBuilderTarget",
"afterWithBuilderTargetReturningTarget" "afterWithBuilderTargetReturningTarget",
"afterWithTargetType",
"afterWithTarget",
"afterWithTargetReturningTarget"
); );
} }
} }

View File

@ -74,7 +74,15 @@ public class MappingContext {
public Order afterWithBuilderTargetReturningTarget(@MappingTarget Order.Builder orderBuilder) { public Order afterWithBuilderTargetReturningTarget(@MappingTarget Order.Builder orderBuilder) {
invokedMethods.add( "afterWithBuilderTargetReturningTarget" ); invokedMethods.add( "afterWithBuilderTargetReturningTarget" );
return orderBuilder.create(); // return null, so that @AfterMapping methods on the finalized object will be called in the tests
return null;
}
@AfterMapping
public Order afterWithTargetReturningTarget(@MappingTarget Order order) {
invokedMethods.add( "afterWithTargetReturningTarget" );
return order;
} }
public List<String> getInvokedMethods() { public List<String> getInvokedMethods() {