#1997 Use builders to construct empty objects in update wrapper

This commit is contained in:
Filip Hrisafov 2022-01-23 22:04:12 +01:00
parent 464adc9143
commit aed3ff5295
9 changed files with 272 additions and 1 deletions

View File

@ -6,6 +6,7 @@
package org.mapstruct.ap.internal.model;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
@ -58,8 +59,10 @@ public class MethodReference extends ModelElement implements Assignment {
private final Type definingType;
private final List<ParameterBinding> parameterBindings;
private final Parameter providingParameter;
private final List<MethodReference> methodsToChain;
private final boolean isStatic;
private final boolean isConstructor;
private final boolean isMethodChaining;
/**
* Creates a new reference to the given method.
@ -95,6 +98,8 @@ public class MethodReference extends ModelElement implements Assignment {
this.isStatic = method.isStatic();
this.name = method.getName();
this.isConstructor = false;
this.methodsToChain = Collections.emptyList();
this.isMethodChaining = false;
}
private MethodReference(BuiltInMethod method, ConversionContext contextParam) {
@ -111,6 +116,8 @@ public class MethodReference extends ModelElement implements Assignment {
this.isStatic = method.isStatic();
this.name = method.getName();
this.isConstructor = false;
this.methodsToChain = Collections.emptyList();
this.isMethodChaining = false;
}
private MethodReference(String name, Type definingType, boolean isStatic) {
@ -127,6 +134,8 @@ public class MethodReference extends ModelElement implements Assignment {
this.providingParameter = null;
this.isStatic = isStatic;
this.isConstructor = false;
this.methodsToChain = Collections.emptyList();
this.isMethodChaining = false;
}
private MethodReference(Type definingType, List<ParameterBinding> parameterBindings) {
@ -142,6 +151,8 @@ public class MethodReference extends ModelElement implements Assignment {
this.providingParameter = null;
this.isStatic = false;
this.isConstructor = true;
this.methodsToChain = Collections.emptyList();
this.isMethodChaining = false;
if ( parameterBindings.isEmpty() ) {
this.importTypes = Collections.emptySet();
@ -159,6 +170,24 @@ public class MethodReference extends ModelElement implements Assignment {
}
}
private MethodReference(MethodReference... references) {
this.name = null;
this.definingType = null;
this.sourceParameters = Collections.emptyList();
this.returnType = null;
this.declaringMapper = null;
this.importTypes = Collections.emptySet();
this.thrownTypes = Collections.emptyList();
this.isUpdateMethod = false;
this.contextParam = null;
this.parameterBindings = null;
this.providingParameter = null;
this.isStatic = false;
this.isConstructor = false;
this.methodsToChain = Arrays.asList( references );
this.isMethodChaining = true;
}
public MapperReference getDeclaringMapper() {
return declaringMapper;
}
@ -267,6 +296,12 @@ public class MethodReference extends ModelElement implements Assignment {
if ( isStatic() ) {
imported.add( definingType );
}
if ( isMethodChaining() ) {
for ( MethodReference methodToChain : methodsToChain ) {
imported.addAll( methodToChain.getImportTypes() );
}
}
return imported;
}
@ -276,6 +311,12 @@ public class MethodReference extends ModelElement implements Assignment {
if ( assignment != null ) {
exceptions.addAll( assignment.getThrownTypes() );
}
if ( isMethodChaining() ) {
for ( MethodReference methodToChain : methodsToChain ) {
exceptions.addAll( methodToChain.getThrownTypes() );
}
}
return exceptions;
}
@ -311,6 +352,14 @@ public class MethodReference extends ModelElement implements Assignment {
return isConstructor;
}
public boolean isMethodChaining() {
return isMethodChaining;
}
public List<MethodReference> getMethodsToChain() {
return methodsToChain;
}
public List<ParameterBinding> getParameterBindings() {
return parameterBindings;
}
@ -385,6 +434,10 @@ public class MethodReference extends ModelElement implements Assignment {
return new MethodReference( type, parameterBindings );
}
public static MethodReference forMethodChaining(MethodReference... references) {
return new MethodReference( references );
}
@Override
public String toString() {
String mapper = declaringMapper != null ? declaringMapper.getType().getName() : "";

View File

@ -139,6 +139,10 @@ public class ObjectFactoryMethodResolver {
}
public static MethodReference getBuilderFactoryMethod(Method method, BuilderType builder ) {
return getBuilderFactoryMethod( method.getReturnType(), builder );
}
public static MethodReference getBuilderFactoryMethod(Type typeToBuild, BuilderType builder ) {
if ( builder == null ) {
return null;
}
@ -149,7 +153,7 @@ public class ObjectFactoryMethodResolver {
return null;
}
if ( !builder.getBuildingType().isAssignableTo( method.getReturnType() ) ) {
if ( !builder.getBuildingType().isAssignableTo( typeToBuild ) ) {
//TODO print error message
return null;
}

View File

@ -419,6 +419,28 @@ public class PropertyMapping extends ModelElement {
Assignment factory = ObjectFactoryMethodResolver
.getFactoryMethod( method, targetType, SelectionParameters.forSourceRHS( rightHandSide ), ctx );
if ( factory == null && targetBuilderType != null) {
// If there is no dedicated factory method and the target has a builder we will try to use that
MethodReference builderFactoryMethod = ObjectFactoryMethodResolver.getBuilderFactoryMethod(
targetType,
targetBuilderType
);
if ( builderFactoryMethod != null ) {
MethodReference finisherMethod = BuilderFinisherMethodResolver.getBuilderFinisherMethod(
method,
targetBuilderType,
ctx
);
if ( finisherMethod != null ) {
factory = MethodReference.forMethodChaining( builderFactoryMethod, finisherMethod );
}
}
}
return new UpdateWrapper(
rhs,
method.getThrownTypes(),

View File

@ -18,6 +18,8 @@
<@includeModel object=definingType/>.<@methodCall/>
<#elseif constructor>
new <@includeModel object=definingType/><#if (parameterBindings?size > 0)>( <@arguments/> )<#else>()</#if>
<#elseif methodChaining>
<#list methodsToChain as methodToChain><@includeModel object=methodToChain /><#if methodToChain_has_next>.</#if></#list>
<#else>
<@methodCall/>
</#if>

View File

@ -0,0 +1,44 @@
/*
* 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.bugs._1997;
/**
* @author Filip Hrisafov
*/
public class Car {
private String model;
private Car(Builder builder) {
this.model = builder.model;
}
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String model;
public Builder model(String model) {
this.model = model;
return this;
}
public Car build() {
return new Car( this );
}
}
}

View File

@ -0,0 +1,44 @@
/*
* 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.bugs._1997;
/**
* @author Filip Hrisafov
*/
public class CarDetail {
private String model;
private CarDetail(Builder builder) {
this.model = builder.model;
}
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String model;
public Builder model(String model) {
this.model = model;
return this;
}
public CarDetail build() {
return new CarDetail( this );
}
}
}

View File

@ -0,0 +1,44 @@
/*
* 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.bugs._1997;
/**
* @author Filip Hrisafov
*/
public class CarInsurance {
private CarDetail detail;
private CarInsurance(Builder builder) {
this.detail = builder.detail;
}
public CarDetail getDetail() {
return detail;
}
public void setDetail(CarDetail detail) {
this.detail = detail;
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private CarDetail detail;
public Builder detail(CarDetail detail) {
this.detail = detail;
return this;
}
public CarInsurance build() {
return new CarInsurance( this );
}
}
}

View File

@ -0,0 +1,23 @@
/*
* 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.bugs._1997;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import org.mapstruct.factory.Mappers;
/**
* @author Filip Hrisafov
*/
@Mapper
public interface CarInsuranceMapper {
CarInsuranceMapper INSTANCE = Mappers.getMapper( CarInsuranceMapper.class );
@Mapping(source = "model", target = "detail.model")
void update(Car source, @MappingTarget CarInsurance target);
}

View File

@ -0,0 +1,35 @@
/*
* 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.bugs._1997;
import org.mapstruct.ap.testutil.ProcessorTest;
import org.mapstruct.ap.testutil.WithClasses;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Filip Hrisafov
*/
@WithClasses({
Car.class,
CarDetail.class,
CarInsurance.class,
CarInsuranceMapper.class
})
class Issue1997Test {
@ProcessorTest
void shouldCorrectCreateIntermediateObjectsWithBuilder() {
Car source = Car.builder().model( "Model S" ).build();
CarInsurance target = CarInsurance.builder().build();
assertThat( target.getDetail() ).isNull();
CarInsuranceMapper.INSTANCE.update( source, target );
assertThat( target.getDetail() ).isNotNull();
assertThat( target.getDetail().getModel() ).isEqualTo( "Model S" );
}
}