mirror of
https://github.com/mapstruct/mapstruct.git
synced 2025-07-12 00:00:08 +08:00
#3729 Support for using inner class Builder without using static factory method
This commit is contained in:
parent
2fb5776350
commit
fce73aee6a
@ -6,6 +6,9 @@
|
||||
### Enhancements
|
||||
|
||||
* Add support for locale parameter for numberFormat and dateFormat (#3628)
|
||||
* Detect Builder without a factory method (#3729) - With this if there is an inner class that ends with `Builder` and has a constructor with parameters,
|
||||
it will be treated as a potential builder.
|
||||
Builders through static methods on the type have a precedence.
|
||||
* Behaviour change: Warning when the target has no target properties (#1140)
|
||||
|
||||
|
||||
|
@ -421,8 +421,11 @@ If a Builder exists for a certain type, then that builder will be used for the m
|
||||
|
||||
The default implementation of the `BuilderProvider` assumes the following:
|
||||
|
||||
* The type has a parameterless public static builder creation method that returns a builder.
|
||||
So for example `Person` has a public static method that returns `PersonBuilder`.
|
||||
* The type has either
|
||||
** A parameterless public static builder creation method that returns a builder.
|
||||
e.g. `Person` has a public static method that returns `PersonBuilder`.
|
||||
** A public static inner class with the name having the suffix "Builder", and a public no-args constructor
|
||||
e.g. `Person` has an inner class `PersonBuilder` with a public no-args constructor.
|
||||
* The builder type has a parameterless public method (build method) that returns the type being built.
|
||||
In our example `PersonBuilder` has a method returning `Person`.
|
||||
* In case there are multiple build methods, MapStruct will look for a method called `build`, if such method exists
|
||||
|
@ -10,6 +10,8 @@ import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.element.ElementKind;
|
||||
import javax.lang.model.element.ExecutableElement;
|
||||
import javax.lang.model.element.Modifier;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
@ -184,32 +186,96 @@ public class DefaultBuilderProvider implements BuilderProvider {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<ExecutableElement> methods = ElementFilter.methodsIn( typeElement.getEnclosedElements() );
|
||||
List<BuilderInfo> builderInfo = new ArrayList<>();
|
||||
for ( ExecutableElement method : methods ) {
|
||||
if ( isPossibleBuilderCreationMethod( method, typeElement ) ) {
|
||||
TypeElement builderElement = getTypeElement( method.getReturnType() );
|
||||
Collection<ExecutableElement> buildMethods = findBuildMethods( builderElement, typeElement );
|
||||
if ( !buildMethods.isEmpty() ) {
|
||||
builderInfo.add( new BuilderInfo.Builder()
|
||||
.builderCreationMethod( method )
|
||||
.buildMethod( buildMethods )
|
||||
.build()
|
||||
);
|
||||
// Builder infos which are determined by a static method on the type itself
|
||||
List<BuilderInfo> methodBuilderInfos = new ArrayList<>();
|
||||
// Builder infos which are determined by an inner builder class in the type itself
|
||||
List<BuilderInfo> innerClassBuilderInfos = new ArrayList<>();
|
||||
|
||||
for ( Element enclosedElement : typeElement.getEnclosedElements() ) {
|
||||
if ( ElementKind.METHOD == enclosedElement.getKind() ) {
|
||||
ExecutableElement method = (ExecutableElement) enclosedElement;
|
||||
BuilderInfo builderInfo = determineMethodBuilderInfo( method, typeElement );
|
||||
if ( builderInfo != null ) {
|
||||
methodBuilderInfos.add( builderInfo );
|
||||
}
|
||||
}
|
||||
else if ( ElementKind.CLASS == enclosedElement.getKind() ) {
|
||||
if ( !methodBuilderInfos.isEmpty() ) {
|
||||
// Small optimization to not check the inner classes
|
||||
// if we already have at least one builder through a method
|
||||
continue;
|
||||
}
|
||||
TypeElement classElement = (TypeElement) enclosedElement;
|
||||
BuilderInfo builderInfo = determineInnerClassBuilderInfo( classElement, typeElement );
|
||||
if ( builderInfo != null ) {
|
||||
innerClassBuilderInfos.add( builderInfo );
|
||||
}
|
||||
}
|
||||
|
||||
if ( builderInfo.size() == 1 ) {
|
||||
return builderInfo.get( 0 );
|
||||
}
|
||||
else if ( builderInfo.size() > 1 ) {
|
||||
throw new MoreThanOneBuilderCreationMethodException( typeElement.asType(), builderInfo );
|
||||
|
||||
if ( methodBuilderInfos.size() == 1 ) {
|
||||
return methodBuilderInfos.get( 0 );
|
||||
}
|
||||
else if ( methodBuilderInfos.size() > 1 ) {
|
||||
throw new MoreThanOneBuilderCreationMethodException( typeElement.asType(), methodBuilderInfos );
|
||||
}
|
||||
else if ( innerClassBuilderInfos.size() == 1 ) {
|
||||
return innerClassBuilderInfos.get( 0 );
|
||||
}
|
||||
else if ( innerClassBuilderInfos.size() > 1 ) {
|
||||
throw new MoreThanOneBuilderCreationMethodException( typeElement.asType(), innerClassBuilderInfos );
|
||||
}
|
||||
|
||||
if ( checkParent ) {
|
||||
return findBuilderInfo( typeElement.getSuperclass() );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private BuilderInfo determineMethodBuilderInfo(ExecutableElement method,
|
||||
TypeElement typeElement) {
|
||||
if ( isPossibleBuilderCreationMethod( method, typeElement ) ) {
|
||||
TypeElement builderElement = getTypeElement( method.getReturnType() );
|
||||
Collection<ExecutableElement> buildMethods = findBuildMethods( builderElement, typeElement );
|
||||
if ( !buildMethods.isEmpty() ) {
|
||||
return new BuilderInfo.Builder()
|
||||
.builderCreationMethod( method )
|
||||
.buildMethod( buildMethods )
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private BuilderInfo determineInnerClassBuilderInfo(TypeElement innerClassElement,
|
||||
TypeElement typeElement) {
|
||||
if ( innerClassElement.getModifiers().contains( Modifier.PUBLIC )
|
||||
&& innerClassElement.getModifiers().contains( Modifier.STATIC )
|
||||
&& innerClassElement.getSimpleName().toString().endsWith( "Builder" ) ) {
|
||||
for ( Element element : innerClassElement.getEnclosedElements() ) {
|
||||
if ( ElementKind.CONSTRUCTOR == element.getKind() ) {
|
||||
ExecutableElement constructor = (ExecutableElement) element;
|
||||
if ( constructor.getParameters().isEmpty() ) {
|
||||
// We have a no-arg constructor
|
||||
// Now check if we have build methods
|
||||
Collection<ExecutableElement> buildMethods = findBuildMethods( innerClassElement, typeElement );
|
||||
if ( !buildMethods.isEmpty() ) {
|
||||
return new BuilderInfo.Builder()
|
||||
.builderCreationMethod( constructor )
|
||||
.buildMethod( buildMethods )
|
||||
.build();
|
||||
}
|
||||
// If we don't have any build methods
|
||||
// then we can stop since we are only interested in the no-arg constructor
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -3,12 +3,13 @@
|
||||
*
|
||||
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package org.mapstruct.ap.test.builder.simple;
|
||||
package org.mapstruct.ap.test.builder.simple.innerclass;
|
||||
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.Mappings;
|
||||
import org.mapstruct.ReportingPolicy;
|
||||
import org.mapstruct.ap.test.builder.simple.SimpleMutablePerson;
|
||||
|
||||
@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR)
|
||||
public interface ErroneousSimpleBuilderMapper {
|
||||
@ -18,5 +19,5 @@ public interface ErroneousSimpleBuilderMapper {
|
||||
@Mapping(target = "job", ignore = true ),
|
||||
@Mapping(target = "city", ignore = true )
|
||||
})
|
||||
SimpleImmutablePerson toImmutable(SimpleMutablePerson source);
|
||||
SimpleImmutablePersonWithInnerClassBuilder toImmutable(SimpleMutablePerson source);
|
||||
}
|
@ -3,12 +3,13 @@
|
||||
*
|
||||
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package org.mapstruct.ap.test.builder.simple;
|
||||
package org.mapstruct.ap.test.builder.simple.innerclass;
|
||||
|
||||
import org.mapstruct.CollectionMappingStrategy;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.Mappings;
|
||||
import org.mapstruct.ap.test.builder.simple.SimpleMutablePerson;
|
||||
|
||||
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED)
|
||||
public interface SimpleBuilderMapper {
|
||||
@ -18,5 +19,5 @@ public interface SimpleBuilderMapper {
|
||||
@Mapping(target = "job", constant = "programmer"),
|
||||
@Mapping(target = "city", expression = "java(\"Bengalore\")")
|
||||
})
|
||||
SimpleImmutablePerson toImmutable(SimpleMutablePerson source);
|
||||
SimpleImmutablePersonWithInnerClassBuilder toImmutable(SimpleMutablePerson source);
|
||||
}
|
@ -3,11 +3,12 @@
|
||||
*
|
||||
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package org.mapstruct.ap.test.builder.simple;
|
||||
package org.mapstruct.ap.test.builder.simple.innerclass;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
import org.mapstruct.ap.test.builder.simple.SimpleMutablePerson;
|
||||
import org.mapstruct.ap.testutil.ProcessorTest;
|
||||
import org.mapstruct.ap.testutil.WithClasses;
|
||||
import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult;
|
||||
@ -20,16 +21,16 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@WithClasses({
|
||||
SimpleMutablePerson.class,
|
||||
SimpleImmutablePerson.class
|
||||
SimpleImmutablePersonWithInnerClassBuilder.class
|
||||
})
|
||||
public class SimpleImmutableBuilderTest {
|
||||
public class SimpleImmutableBuilderThroughInnerClassConstructorTest {
|
||||
|
||||
@RegisterExtension
|
||||
final GeneratedSource generatedSource = new GeneratedSource();
|
||||
|
||||
@ProcessorTest
|
||||
@WithClasses({ SimpleBuilderMapper.class })
|
||||
public void testSimpleImmutableBuilderHappyPath() {
|
||||
public void testSimpleImmutableBuilderThroughInnerClassConstructorHappyPath() {
|
||||
SimpleBuilderMapper mapper = Mappers.getMapper( SimpleBuilderMapper.class );
|
||||
SimpleMutablePerson source = new SimpleMutablePerson();
|
||||
source.setAge( 3 );
|
||||
@ -37,7 +38,7 @@ public class SimpleImmutableBuilderTest {
|
||||
source.setChildren( Arrays.asList( "Alice", "Tom" ) );
|
||||
source.setAddress( "Plaza 1" );
|
||||
|
||||
SimpleImmutablePerson targetObject = mapper.toImmutable( source );
|
||||
SimpleImmutablePersonWithInnerClassBuilder targetObject = mapper.toImmutable( source );
|
||||
|
||||
assertThat( targetObject.getAge() ).isEqualTo( 3 );
|
||||
assertThat( targetObject.getName() ).isEqualTo( "Bob" );
|
||||
@ -53,8 +54,8 @@ public class SimpleImmutableBuilderTest {
|
||||
diagnostics = @Diagnostic(
|
||||
kind = javax.tools.Diagnostic.Kind.ERROR,
|
||||
type = ErroneousSimpleBuilderMapper.class,
|
||||
line = 21,
|
||||
line = 22,
|
||||
message = "Unmapped target property: \"name\"."))
|
||||
public void testSimpleImmutableBuilderMissingPropertyFailsToCompile() {
|
||||
public void testSimpleImmutableBuilderThroughInnerClassConstructorMissingPropertyFailsToCompile() {
|
||||
}
|
||||
}
|
@ -3,12 +3,12 @@
|
||||
*
|
||||
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
package org.mapstruct.ap.test.builder.simple;
|
||||
package org.mapstruct.ap.test.builder.simple.innerclass;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class SimpleImmutablePerson {
|
||||
public class SimpleImmutablePersonWithInnerClassBuilder {
|
||||
private final String name;
|
||||
private final int age;
|
||||
private final String job;
|
||||
@ -16,7 +16,7 @@ public class SimpleImmutablePerson {
|
||||
private final String address;
|
||||
private final List<String> children;
|
||||
|
||||
SimpleImmutablePerson(Builder builder) {
|
||||
SimpleImmutablePersonWithInnerClassBuilder(Builder builder) {
|
||||
this.name = builder.name;
|
||||
this.age = builder.age;
|
||||
this.job = builder.job;
|
||||
@ -25,10 +25,6 @@ public class SimpleImmutablePerson {
|
||||
this.children = new ArrayList<>(builder.children);
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public int getAge() {
|
||||
return age;
|
||||
}
|
||||
@ -66,8 +62,8 @@ public class SimpleImmutablePerson {
|
||||
return this;
|
||||
}
|
||||
|
||||
public SimpleImmutablePerson build() {
|
||||
return new SimpleImmutablePerson( this );
|
||||
public SimpleImmutablePersonWithInnerClassBuilder build() {
|
||||
return new SimpleImmutablePersonWithInnerClassBuilder( this );
|
||||
}
|
||||
|
||||
public Builder name(String name) {
|
@ -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.builder.simple.staticfactorymethod;
|
||||
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.Mappings;
|
||||
import org.mapstruct.ReportingPolicy;
|
||||
import org.mapstruct.ap.test.builder.simple.SimpleMutablePerson;
|
||||
|
||||
@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR)
|
||||
public interface ErroneousSimpleBuilderMapper {
|
||||
|
||||
@Mappings({
|
||||
@Mapping(target = "address", ignore = true ),
|
||||
@Mapping(target = "job", ignore = true ),
|
||||
@Mapping(target = "city", ignore = true )
|
||||
})
|
||||
SimpleImmutablePersonWithStaticFactoryMethodBuilder toImmutable(SimpleMutablePerson source);
|
||||
}
|
@ -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.builder.simple.staticfactorymethod;
|
||||
|
||||
import org.mapstruct.CollectionMappingStrategy;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.Mappings;
|
||||
import org.mapstruct.ap.test.builder.simple.SimpleMutablePerson;
|
||||
|
||||
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED)
|
||||
public interface SimpleBuilderMapper {
|
||||
|
||||
@Mappings({
|
||||
@Mapping(target = "name", source = "fullName"),
|
||||
@Mapping(target = "job", constant = "programmer"),
|
||||
@Mapping(target = "city", expression = "java(\"Bengalore\")")
|
||||
})
|
||||
SimpleImmutablePersonWithStaticFactoryMethodBuilder toImmutable(SimpleMutablePerson source);
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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.builder.simple.staticfactorymethod;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
import org.mapstruct.ap.test.builder.simple.SimpleMutablePerson;
|
||||
import org.mapstruct.ap.testutil.ProcessorTest;
|
||||
import org.mapstruct.ap.testutil.WithClasses;
|
||||
import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult;
|
||||
import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic;
|
||||
import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome;
|
||||
import org.mapstruct.ap.testutil.runner.GeneratedSource;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@WithClasses({
|
||||
SimpleMutablePerson.class,
|
||||
SimpleImmutablePersonWithStaticFactoryMethodBuilder.class
|
||||
})
|
||||
public class SimpleImmutableBuilderThroughStaticFactoryMethodTest {
|
||||
|
||||
@RegisterExtension
|
||||
final GeneratedSource generatedSource = new GeneratedSource();
|
||||
|
||||
@ProcessorTest
|
||||
@WithClasses({ SimpleBuilderMapper.class })
|
||||
public void testSimpleImmutableBuilderThroughStaticFactoryMethodHappyPath() {
|
||||
SimpleBuilderMapper mapper = Mappers.getMapper( SimpleBuilderMapper.class );
|
||||
SimpleMutablePerson source = new SimpleMutablePerson();
|
||||
source.setAge( 3 );
|
||||
source.setFullName( "Bob" );
|
||||
source.setChildren( Arrays.asList( "Alice", "Tom" ) );
|
||||
source.setAddress( "Plaza 1" );
|
||||
|
||||
SimpleImmutablePersonWithStaticFactoryMethodBuilder targetObject = mapper.toImmutable( source );
|
||||
|
||||
assertThat( targetObject.getAge() ).isEqualTo( 3 );
|
||||
assertThat( targetObject.getName() ).isEqualTo( "Bob" );
|
||||
assertThat( targetObject.getJob() ).isEqualTo( "programmer" );
|
||||
assertThat( targetObject.getCity() ).isEqualTo( "Bengalore" );
|
||||
assertThat( targetObject.getAddress() ).isEqualTo( "Plaza 1" );
|
||||
assertThat( targetObject.getChildren() ).contains( "Alice", "Tom" );
|
||||
}
|
||||
|
||||
@ProcessorTest
|
||||
@WithClasses({ ErroneousSimpleBuilderMapper.class })
|
||||
@ExpectedCompilationOutcome(value = CompilationResult.FAILED,
|
||||
diagnostics = @Diagnostic(
|
||||
kind = javax.tools.Diagnostic.Kind.ERROR,
|
||||
type = ErroneousSimpleBuilderMapper.class,
|
||||
line = 22,
|
||||
message = "Unmapped target property: \"name\"."))
|
||||
public void testSimpleImmutableBuilderThroughStaticFactoryMethodMissingPropertyFailsToCompile() {
|
||||
}
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 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.builder.simple.staticfactorymethod;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class SimpleImmutablePersonWithStaticFactoryMethodBuilder {
|
||||
private final String name;
|
||||
private final int age;
|
||||
private final String job;
|
||||
private final String city;
|
||||
private final String address;
|
||||
private final List<String> children;
|
||||
|
||||
SimpleImmutablePersonWithStaticFactoryMethodBuilder(Builder builder) {
|
||||
this.name = builder.name;
|
||||
this.age = builder.age;
|
||||
this.job = builder.job;
|
||||
this.city = builder.city;
|
||||
this.address = builder.address;
|
||||
this.children = new ArrayList<>( builder.children );
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public int getAge() {
|
||||
return age;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getJob() {
|
||||
return job;
|
||||
}
|
||||
|
||||
public String getCity() {
|
||||
return city;
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public List<String> getChildren() {
|
||||
return children;
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private String name;
|
||||
private int age;
|
||||
private String job;
|
||||
private String city;
|
||||
private String address;
|
||||
private List<String> children = new ArrayList<>();
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
|
||||
public Builder age(int age) {
|
||||
this.age = age;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SimpleImmutablePersonWithStaticFactoryMethodBuilder build() {
|
||||
return new SimpleImmutablePersonWithStaticFactoryMethodBuilder( this );
|
||||
}
|
||||
|
||||
public Builder name(String name) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder job(String job) {
|
||||
this.job = job;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder city(String city) {
|
||||
this.city = city;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder address(String address) {
|
||||
this.address = address;
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<String> getChildren() {
|
||||
throw new UnsupportedOperationException( "This is just a marker method" );
|
||||
}
|
||||
|
||||
public Builder addChild(String child) {
|
||||
this.children.add( child );
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user