#1801 Using constructor as builderCreationMethod in custom builder (#1905)

This commit is contained in:
Sjaak Derksen 2019-09-15 21:43:22 +02:00 committed by GitHub
parent f3b0badcef
commit 7e0327767f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 586 additions and 38 deletions

View File

@ -51,6 +51,7 @@ public class FullFeatureCompilationTest {
// SPI not working correctly here.. (not picked up) // SPI not working correctly here.. (not picked up)
additionalExcludes.add( "org/mapstruct/ap/test/bugs/_1596/*.java" ); additionalExcludes.add( "org/mapstruct/ap/test/bugs/_1596/*.java" );
additionalExcludes.add( "org/mapstruct/ap/test/bugs/_1801/*.java" );
switch ( processorType ) { switch ( processorType ) {
case ORACLE_JAVA_9: case ORACLE_JAVA_9:

View File

@ -140,21 +140,22 @@ public class BeanMappingMethod extends NormalTypeMappingMethod {
Type returnTypeImpl = getReturnTypeToConstructFromSelectionParameters( selectionParameters ); Type returnTypeImpl = getReturnTypeToConstructFromSelectionParameters( selectionParameters );
if ( returnTypeImpl != null ) { if ( returnTypeImpl != null ) {
factoryMethod = getFactoryMethod( returnTypeImpl, selectionParameters ); factoryMethod = getFactoryMethod( returnTypeImpl, selectionParameters );
if ( factoryMethod != null || canBeConstructed( returnTypeImpl ) ) { if ( factoryMethod != null || canResultTypeFromBeanMappingBeConstructed( returnTypeImpl ) ) {
returnTypeToConstruct = returnTypeImpl; returnTypeToConstruct = returnTypeImpl;
} }
else {
reportResultTypeFromBeanMappingNotConstructableError( returnTypeImpl );
}
} }
else { else if ( isBuilderRequired() ) {
returnTypeImpl = isBuilderRequired() ? returnTypeBuilder.getBuilder() : method.getReturnType(); returnTypeImpl = returnTypeBuilder.getBuilder();
factoryMethod = getFactoryMethod( returnTypeImpl, selectionParameters ); factoryMethod = getFactoryMethod( returnTypeImpl, selectionParameters );
if ( factoryMethod != null || canBeConstructed( returnTypeImpl ) ) { if ( factoryMethod != null || canReturnTypeBeConstructed( returnTypeImpl ) ) {
returnTypeToConstruct = returnTypeImpl; returnTypeToConstruct = returnTypeImpl;
} }
else { }
reportReturnTypeNotConstructableError( returnTypeImpl ); else if ( !method.isUpdateMethod() ) {
returnTypeImpl = method.getReturnType();
factoryMethod = getFactoryMethod( returnTypeImpl, selectionParameters );
if ( factoryMethod != null || canReturnTypeBeConstructed( returnTypeImpl ) ) {
returnTypeToConstruct = returnTypeImpl;
} }
} }
} }
@ -383,57 +384,60 @@ public class BeanMappingMethod extends NormalTypeMappingMethod {
return null; return null;
} }
private boolean canBeConstructed(Type typeToBeConstructed) { private boolean canResultTypeFromBeanMappingBeConstructed(Type resultType) {
return !typeToBeConstructed.isAbstract()
&& typeToBeConstructed.isAssignableTo( this.method.getResultType() )
&& typeToBeConstructed.hasEmptyAccessibleConstructor();
}
private void reportResultTypeFromBeanMappingNotConstructableError(Type resultType) {
boolean error = true;
if ( resultType.isAbstract() ) { if ( resultType.isAbstract() ) {
ctx.getMessager().printMessage( ctx.getMessager().printMessage(
method.getExecutable(), method.getExecutable(),
BeanMappingPrism.getInstanceOn( method.getExecutable() ).mirror, BeanMappingPrism.getInstanceOn( method.getExecutable() ).mirror,
BEANMAPPING_ABSTRACT, BEANMAPPING_ABSTRACT,
resultType, resultType,
method.getResultType() method.getResultType()
); );
error = false;
} }
else if ( !resultType.isAssignableTo( method.getResultType() ) ) { else if ( !resultType.isAssignableTo( method.getResultType() ) ) {
ctx.getMessager().printMessage( ctx.getMessager().printMessage(
method.getExecutable(), method.getExecutable(),
BeanMappingPrism.getInstanceOn( method.getExecutable() ).mirror, BeanMappingPrism.getInstanceOn( method.getExecutable() ).mirror,
BEANMAPPING_NOT_ASSIGNABLE, BEANMAPPING_NOT_ASSIGNABLE,
resultType, resultType,
method.getResultType() method.getResultType()
); );
error = false;
} }
else if ( !resultType.hasEmptyAccessibleConstructor() ) { else if ( !resultType.hasEmptyAccessibleConstructor() ) {
ctx.getMessager().printMessage( ctx.getMessager().printMessage(
method.getExecutable(), method.getExecutable(),
BeanMappingPrism.getInstanceOn( method.getExecutable() ).mirror, BeanMappingPrism.getInstanceOn( method.getExecutable() ).mirror,
Message.GENERAL_NO_SUITABLE_CONSTRUCTOR, Message.GENERAL_NO_SUITABLE_CONSTRUCTOR,
resultType resultType
); );
error = false;
} }
return error;
} }
private void reportReturnTypeNotConstructableError(Type returnType) { private boolean canReturnTypeBeConstructed(Type returnType) {
boolean error = true;
if ( returnType.isAbstract() ) { if ( returnType.isAbstract() ) {
ctx.getMessager().printMessage( ctx.getMessager().printMessage(
method.getExecutable(), method.getExecutable(),
GENERAL_ABSTRACT_RETURN_TYPE, GENERAL_ABSTRACT_RETURN_TYPE,
returnType returnType
); );
error = false;
} }
else if ( !returnType.hasEmptyAccessibleConstructor() ) { else if ( !returnType.hasEmptyAccessibleConstructor() ) {
ctx.getMessager().printMessage( ctx.getMessager().printMessage(
method.getExecutable(), method.getExecutable(),
Message.GENERAL_NO_SUITABLE_CONSTRUCTOR, Message.GENERAL_NO_SUITABLE_CONSTRUCTOR,
returnType returnType
); );
error = false;
} }
return error;
} }
/** /**
@ -1021,4 +1025,3 @@ public class BeanMappingMethod extends NormalTypeMappingMethod {
} }
} }

View File

@ -6,6 +6,7 @@
package org.mapstruct.ap.internal.model.common; package org.mapstruct.ap.internal.model.common;
import java.util.Collection; import java.util.Collection;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.ExecutableElement;
import javax.lang.model.type.TypeMirror; import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Types; import javax.lang.model.util.Types;
@ -102,6 +103,11 @@ public class BuilderType {
owner = typeFactory.getType( builderCreationOwner ); owner = typeFactory.getType( builderCreationOwner );
} }
// When the builderCreationMethod is constructor, its return type is Void. In this case the
// builder type should be the owner type.
if (builderInfo.getBuilderCreationMethod().getKind() == ElementKind.CONSTRUCTOR) {
builder = owner;
}
return new BuilderType( return new BuilderType(
builder, builder,

View File

@ -0,0 +1,96 @@
/*
* 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._1801;
import java.util.List;
import java.util.Objects;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.ElementFilter;
import org.mapstruct.ap.spi.BuilderInfo;
import org.mapstruct.ap.spi.BuilderProvider;
import org.mapstruct.ap.spi.ImmutablesBuilderProvider;
/**
* @author Zhizhi Deng
*/
public class Issue1801BuilderProvider extends ImmutablesBuilderProvider implements BuilderProvider {
@Override
protected BuilderInfo findBuilderInfo(TypeElement typeElement) {
Name name = typeElement.getQualifiedName();
if ( name.toString().endsWith( ".Item" ) ) {
BuilderInfo info = findBuilderInfoFromInnerBuilderClass( typeElement );
if ( info != null ) {
return info;
}
}
return super.findBuilderInfo( typeElement );
}
/**
* Looks for inner builder class in the Immutable interface / abstract class.
*
* The inner builder class should be be declared with the following line
*
* <pre>
* public static Builder() extends ImmutableItem.Builder { }
* </pre>
*
* The Immutable instance should be created with the following line
*
* <pre>
* new Item.Builder().withId("123").build();
* </pre>
*
* @see org.mapstruct.ap.test.bugs._1801.domain.Item
*
* @param typeElement
* @return
*/
private BuilderInfo findBuilderInfoFromInnerBuilderClass(TypeElement typeElement) {
if (shouldIgnore( typeElement )) {
return null;
}
List<TypeElement> innerTypes = ElementFilter.typesIn( typeElement.getEnclosedElements() );
ExecutableElement defaultConstructor = innerTypes.stream()
.filter( this::isBuilderCandidate )
.map( this::getEmptyArgPublicConstructor )
.filter( Objects::nonNull )
.findAny()
.orElse( null );
if ( defaultConstructor != null ) {
return new BuilderInfo.Builder()
.builderCreationMethod( defaultConstructor )
.buildMethod( findBuildMethods( (TypeElement) defaultConstructor.getEnclosingElement(), typeElement ) )
.build();
}
return null;
}
private boolean isBuilderCandidate(TypeElement innerType ) {
TypeElement outerType = (TypeElement) innerType.getEnclosingElement();
String packageName = this.elementUtils.getPackageOf( outerType ).getQualifiedName().toString();
Name outerSimpleName = outerType.getSimpleName();
String builderClassName = packageName + ".Immutable" + outerSimpleName + ".Builder";
return innerType.getSimpleName().contentEquals( "Builder" )
&& getTypeElement( innerType.getSuperclass() ).getQualifiedName().contentEquals( builderClassName )
&& innerType.getModifiers().contains( Modifier.PUBLIC );
}
private ExecutableElement getEmptyArgPublicConstructor(TypeElement builderType) {
return ElementFilter.constructorsIn( builderType.getEnclosedElements() ).stream()
.filter( c -> c.getParameters().isEmpty() )
.filter( c -> c.getModifiers().contains( Modifier.PUBLIC ) )
.findAny()
.orElse( null );
}
}

View File

@ -0,0 +1,53 @@
/*
* 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._1801;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mapstruct.ap.spi.AccessorNamingStrategy;
import org.mapstruct.ap.spi.BuilderProvider;
import org.mapstruct.ap.spi.ImmutablesAccessorNamingStrategy;
import org.mapstruct.ap.test.bugs._1801.domain.ImmutableItem;
import org.mapstruct.ap.test.bugs._1801.domain.Item;
import org.mapstruct.ap.test.bugs._1801.dto.ImmutableItemDTO;
import org.mapstruct.ap.test.bugs._1801.dto.ItemDTO;
import org.mapstruct.ap.testutil.IssueKey;
import org.mapstruct.ap.testutil.WithClasses;
import org.mapstruct.ap.testutil.WithServiceImplementation;
import org.mapstruct.ap.testutil.WithServiceImplementations;
import org.mapstruct.ap.testutil.runner.AnnotationProcessorTestRunner;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Zhizhi Deng
*/
@WithClasses({
ItemMapper.class,
Item.class,
ImmutableItem.class,
ItemDTO.class,
ImmutableItemDTO.class
})
@RunWith(AnnotationProcessorTestRunner.class)
@IssueKey("1801")
@WithServiceImplementations( {
@WithServiceImplementation( provides = BuilderProvider.class, value = Issue1801BuilderProvider.class),
@WithServiceImplementation( provides = AccessorNamingStrategy.class, value = ImmutablesAccessorNamingStrategy.class)
})
public class Issue1801Test {
@Test
public void shouldIncludeBuildeType() {
ItemDTO item = ImmutableItemDTO.builder().id( "test" ).build();
Item target = ItemMapper.INSTANCE.map( item );
assertThat( target ).isNotNull();
assertThat( target.getId() ).isEqualTo( "test" );
}
}

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._1801;
import org.mapstruct.Builder;
import org.mapstruct.Mapper;
import org.mapstruct.ap.test.bugs._1801.domain.Item;
import org.mapstruct.ap.test.bugs._1801.dto.ItemDTO;
import org.mapstruct.factory.Mappers;
/**
* @author Zhizhi Deng
*/
@Mapper( builder = @Builder)
public abstract class ItemMapper {
public static final ItemMapper INSTANCE = Mappers.getMapper( ItemMapper.class );
public abstract Item map(ItemDTO itemDTO);
}

View File

@ -0,0 +1,154 @@
/*
* 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._1801.domain;
import java.util.ArrayList;
import java.util.List;
/**
* Immutable implementation of {@link Item}.
* <p>
* Superclass should expose a static subclass of the Builder to create immutable instance
* {@code public static Builder extends ImmutableItem.Builder}.
*
* @author Zhizhi Deng
*/
@SuppressWarnings({"all"})
public final class ImmutableItem extends Item {
private final String id;
private ImmutableItem(String id) {
this.id = id;
}
/**
* @return The value of the {@code id} attribute
*/
@Override
public String getId() {
return id;
}
/**
* Copy the current immutable object by setting a value for the {@link Item#getId() id} attribute.
* A shallow reference equality check is used to prevent copying of the same value by returning {@code this}.
* @param value A new value for id
* @return A modified copy of the {@code this} object
*/
public final ImmutableItem withId(String value) {
if (this.id == value) return this;
return new ImmutableItem(value);
}
/**
* This instance is equal to all instances of {@code ImmutableItem} that have equal attribute values.
* @return {@code true} if {@code this} is equal to {@code another} instance
*/
@Override
public boolean equals(Object another) {
if (this == another) return true;
return another instanceof ImmutableItem
&& equalTo((ImmutableItem) another);
}
private boolean equalTo(ImmutableItem another) {
return id.equals(another.id);
}
/**
* Computes a hash code from attributes: {@code id}.
* @return hashCode value
*/
@Override
public int hashCode() {
int h = 5381;
h += (h << 5) + id.hashCode();
return h;
}
/**
* Prints the immutable value {@code Item} with attribute values.
* @return A string representation of the value
*/
@Override
public String toString() {
return "Item{"
+ "id=" + id
+ "}";
}
/**
* Creates an immutable copy of a {@link Item} value.
* Uses accessors to get values to initialize the new immutable instance.
* If an instance is already immutable, it is returned as is.
* @param instance The instance to copy
* @return A copied immutable Item instance
*/
public static ImmutableItem copyOf(Item instance) {
if (instance instanceof ImmutableItem) {
return (ImmutableItem) instance;
}
return new Builder()
.from(instance)
.build();
}
/**
* Builds instances of type {@link ImmutableItem ImmutableItem}.
* <p><em>{@code Builder} is not thread-safe and generally should not be stored in a field or collection,
* but instead used immediately to create instances.</em>
*/
public static class Builder {
private static final long INIT_BIT_ID = 0x1L;
private long initBits = 0x1L;
private String id;
Builder() {
}
/**
* Fill a builder with attribute values from the provided {@code Item} instance.
* Regular attribute values will be replaced with those from the given instance.
* Absent optional values will not replace present values.
* @param instance The instance from which to copy values
* @return {@code this} builder for use in a chained invocation
*/
public final Builder from(Item instance) {
id(instance.getId());
return this;
}
/**
* Initializes the value for the {@link Item#getId() id} attribute.
* @param id The value for id
* @return {@code this} builder for use in a chained invocation
*/
public final Builder id(String id) {
this.id = id;
initBits &= ~INIT_BIT_ID;
return this;
}
/**
* Builds a new {@link ImmutableItem ImmutableItem}.
* @return An immutable instance of Item
* @throws java.lang.IllegalStateException if any required attributes are missing
*/
public ImmutableItem build() {
if (initBits != 0) {
throw new IllegalStateException(formatRequiredAttributesMessage());
}
return new ImmutableItem(id);
}
private String formatRequiredAttributesMessage() {
List<String> attributes = new ArrayList<String>();
if ((initBits & INIT_BIT_ID) != 0) attributes.add("id");
return "Cannot build Item, some of required attributes are not set " + attributes;
}
}
}

View File

@ -0,0 +1,15 @@
/*
* 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._1801.domain;
/**
* @author Zhizhi Deng
*/
public abstract class Item {
public abstract String getId();
public static class Builder extends ImmutableItem.Builder { }
}

View File

@ -0,0 +1,184 @@
/*
* 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._1801.dto;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* Immutable implementation of {@link ItemDTO}.
* <p>
* Use the builder to create immutable instances:
* {@code ImmutableItemDTO.builder()}.
*
* @author Zhizhi Deng
*/
public final class ImmutableItemDTO extends ItemDTO {
private final String id;
private ImmutableItemDTO(String id) {
this.id = id;
}
/**
* @return The value of the {@code id} attribute
*/
@Override
public String getId() {
return id;
}
/**
* Copy the current immutable object by setting a value for the {@link ItemDTO#getId() id} attribute.
* A shallow reference equality check is used to prevent copying of the same value by returning {@code this}.
*
* @param value A new value for id
*
* @return A modified copy of the {@code this} object
*/
public ImmutableItemDTO withId(String value) {
if ( Objects.equals( this.id, value ) ) {
return this;
}
return new ImmutableItemDTO( value );
}
/**
* This instance is equal to all instances of {@code ImmutableItemDTO} that have equal attribute values.
*
* @return {@code true} if {@code this} is equal to {@code another} instance
*/
@Override
public boolean equals(Object another) {
if ( this == another ) {
return true;
}
return another instanceof ImmutableItemDTO
&& equalTo( (ImmutableItemDTO) another );
}
private boolean equalTo(ImmutableItemDTO another) {
return id.equals( another.id );
}
/**
* Computes a hash code from attributes: {@code id}.
*
* @return hashCode value
*/
@Override
public int hashCode() {
int h = 5381;
h += ( h << 5 ) + id.hashCode();
return h;
}
/**
* Prints the immutable value {@code ItemDTO} with attribute values.
*
* @return A string representation of the value
*/
@Override
public String toString() {
return "ItemDTO{"
+ "id=" + id
+ "}";
}
/**
* Creates an immutable copy of a {@link ItemDTO} value.
* Uses accessors to get values to initialize the new immutable instance.
* If an instance is already immutable, it is returned as is.
*
* @param instance The instance to copy
*
* @return A copied immutable ItemDTO instance
*/
public static ImmutableItemDTO copyOf(ItemDTO instance) {
if ( instance instanceof ImmutableItemDTO ) {
return (ImmutableItemDTO) instance;
}
return ImmutableItemDTO.builder()
.from( instance )
.build();
}
/**
* Creates a builder for {@link ImmutableItemDTO ImmutableItemDTO}.
*
* @return A new ImmutableItemDTO builder
*/
public static ImmutableItemDTO.Builder builder() {
return new ImmutableItemDTO.Builder();
}
/**
* Builds instances of type {@link ImmutableItemDTO ImmutableItemDTO}.
* Initialize attributes and then invoke the {@link #build()} method to create an
* immutable instance.
* <p><em>{@code Builder} is not thread-safe and generally should not be stored in a field or collection,
* but instead used immediately to create instances.</em>
*/
public static final class Builder {
private static final long INIT_BIT_ID = 0x1L;
private long initBits = 0x1L;
private String id;
private Builder() {
}
/**
* Fill a builder with attribute values from the provided {@code ItemDTO} instance.
* Regular attribute values will be replaced with those from the given instance.
* Absent optional values will not replace present values.
*
* @param instance The instance from which to copy values
*
* @return {@code this} builder for use in a chained invocation
*/
public Builder from(ItemDTO instance) {
id( instance.getId() );
return this;
}
/**
* Initializes the value for the {@link ItemDTO#getId() id} attribute.
*
* @param id The value for id
*
* @return {@code this} builder for use in a chained invocation
*/
public Builder id(String id) {
this.id = id;
initBits &= ~INIT_BIT_ID;
return this;
}
/**
* Builds a new {@link ImmutableItemDTO ImmutableItemDTO}.
*
* @return An immutable instance of ItemDTO
*
* @throws java.lang.IllegalStateException if any required attributes are missing
*/
public ImmutableItemDTO build() {
if ( initBits != 0 ) {
throw new IllegalStateException( formatRequiredAttributesMessage() );
}
return new ImmutableItemDTO( id );
}
private String formatRequiredAttributesMessage() {
List<String> attributes = new ArrayList<>();
if ( ( initBits & INIT_BIT_ID ) != 0 ) {
attributes.add( "id" );
}
return "Cannot build ItemDTO, some of required attributes are not set " + attributes;
}
}
}

View File

@ -0,0 +1,13 @@
/*
* 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._1801.dto;
/**
* @author Zhizhi Deng
*/
public abstract class ItemDTO {
public abstract String getId();
}