Skip to content

Commit

Permalink
mapstruct#1801 Using constructor as builderCreationMethod in custom b…
Browse files Browse the repository at this point in the history
…uilder (mapstruct#1905)
  • Loading branch information
sjaakd committed Sep 15, 2019
1 parent f3b0bad commit 7e03277
Show file tree
Hide file tree
Showing 10 changed files with 586 additions and 38 deletions.
Expand Up @@ -51,6 +51,7 @@ public Collection<String> getAdditionalCommandLineArguments(ProcessorType proces

// SPI not working correctly here.. (not picked up)
additionalExcludes.add( "org/mapstruct/ap/test/bugs/_1596/*.java" );
additionalExcludes.add( "org/mapstruct/ap/test/bugs/_1801/*.java" );

switch ( processorType ) {
case ORACLE_JAVA_9:
Expand Down
Expand Up @@ -140,21 +140,22 @@ public BeanMappingMethod build() {
Type returnTypeImpl = getReturnTypeToConstructFromSelectionParameters( selectionParameters );
if ( returnTypeImpl != null ) {
factoryMethod = getFactoryMethod( returnTypeImpl, selectionParameters );
if ( factoryMethod != null || canBeConstructed( returnTypeImpl ) ) {
if ( factoryMethod != null || canResultTypeFromBeanMappingBeConstructed( returnTypeImpl ) ) {
returnTypeToConstruct = returnTypeImpl;
}
else {
reportResultTypeFromBeanMappingNotConstructableError( returnTypeImpl );
}
}
else {
returnTypeImpl = isBuilderRequired() ? returnTypeBuilder.getBuilder() : method.getReturnType();
else if ( isBuilderRequired() ) {
returnTypeImpl = returnTypeBuilder.getBuilder();
factoryMethod = getFactoryMethod( returnTypeImpl, selectionParameters );
if ( factoryMethod != null || canBeConstructed( returnTypeImpl ) ) {
if ( factoryMethod != null || canReturnTypeBeConstructed( returnTypeImpl ) ) {
returnTypeToConstruct = returnTypeImpl;
}
else {
reportReturnTypeNotConstructableError( returnTypeImpl );
}
else if ( !method.isUpdateMethod() ) {
returnTypeImpl = method.getReturnType();
factoryMethod = getFactoryMethod( returnTypeImpl, selectionParameters );
if ( factoryMethod != null || canReturnTypeBeConstructed( returnTypeImpl ) ) {
returnTypeToConstruct = returnTypeImpl;
}
}
}
Expand Down Expand Up @@ -383,57 +384,60 @@ private Type getReturnTypeToConstructFromSelectionParameters(SelectionParameters
return null;
}

private boolean canBeConstructed(Type typeToBeConstructed) {
return !typeToBeConstructed.isAbstract()
&& typeToBeConstructed.isAssignableTo( this.method.getResultType() )
&& typeToBeConstructed.hasEmptyAccessibleConstructor();
}

private void reportResultTypeFromBeanMappingNotConstructableError(Type resultType) {
private boolean canResultTypeFromBeanMappingBeConstructed(Type resultType) {

boolean error = true;
if ( resultType.isAbstract() ) {
ctx.getMessager().printMessage(
method.getExecutable(),
BeanMappingPrism.getInstanceOn( method.getExecutable() ).mirror,
BEANMAPPING_ABSTRACT,
resultType,
method.getResultType()
method.getExecutable(),
BeanMappingPrism.getInstanceOn( method.getExecutable() ).mirror,
BEANMAPPING_ABSTRACT,
resultType,
method.getResultType()
);
error = false;
}
else if ( !resultType.isAssignableTo( method.getResultType() ) ) {
ctx.getMessager().printMessage(
method.getExecutable(),
BeanMappingPrism.getInstanceOn( method.getExecutable() ).mirror,
BEANMAPPING_NOT_ASSIGNABLE,
resultType,
method.getResultType()
method.getExecutable(),
BeanMappingPrism.getInstanceOn( method.getExecutable() ).mirror,
BEANMAPPING_NOT_ASSIGNABLE,
resultType,
method.getResultType()
);
error = false;
}
else if ( !resultType.hasEmptyAccessibleConstructor() ) {
ctx.getMessager().printMessage(
method.getExecutable(),
BeanMappingPrism.getInstanceOn( method.getExecutable() ).mirror,
Message.GENERAL_NO_SUITABLE_CONSTRUCTOR,
resultType
method.getExecutable(),
BeanMappingPrism.getInstanceOn( method.getExecutable() ).mirror,
Message.GENERAL_NO_SUITABLE_CONSTRUCTOR,
resultType
);
error = false;
}
return error;
}

private void reportReturnTypeNotConstructableError(Type returnType) {
private boolean canReturnTypeBeConstructed(Type returnType) {
boolean error = true;
if ( returnType.isAbstract() ) {
ctx.getMessager().printMessage(
method.getExecutable(),
GENERAL_ABSTRACT_RETURN_TYPE,
returnType
method.getExecutable(),
GENERAL_ABSTRACT_RETURN_TYPE,
returnType
);
error = false;
}
else if ( !returnType.hasEmptyAccessibleConstructor() ) {
ctx.getMessager().printMessage(
method.getExecutable(),
Message.GENERAL_NO_SUITABLE_CONSTRUCTOR,
returnType
method.getExecutable(),
Message.GENERAL_NO_SUITABLE_CONSTRUCTOR,
returnType
);
error = false;
}
return error;
}

/**
Expand Down Expand Up @@ -1021,4 +1025,3 @@ public boolean equals(Object obj) {
}

}

Expand Up @@ -6,6 +6,7 @@
package org.mapstruct.ap.internal.model.common;

import java.util.Collection;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Types;
Expand Down Expand Up @@ -102,6 +103,11 @@ else if ( typeUtils.isSameType( builder.getTypeMirror(), 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(
builder,
Expand Down
@@ -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 );
}
}
@@ -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" );
}
}
@@ -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);
}

0 comments on commit 7e03277

Please sign in to comment.