From ef270caecbebfa035ea642eb64ce1952d2fea653 Mon Sep 17 00:00:00 2001 From: Filip Hrisafov Date: Thu, 12 Jul 2018 23:16:53 +0200 Subject: [PATCH] #1479 Add support for Builders with multiple build methods (#1498) * Add new @Builder annotation for defining a build method * When there are multiple build methods look for a method named `build` and if found use it * If @Builder is defined than look for a build method with the defined method * When a type has multiple builder creation methods throw an exception and don't use the builder Defaulting to a method named `build` will make sure that a correct method is selected for: * FreeBuilder - it has two methods: `build` and `buildPartial` * Protobuf - it has three methods: `getDefaultInstanceForType`, `build` and `buildPartial` --- .../main/java/org/mapstruct/BeanMapping.java | 19 ++ .../src/main/java/org/mapstruct/Builder.java | 45 +++++ .../src/main/java/org/mapstruct/Mapper.java | 22 ++- .../main/java/org/mapstruct/MapperConfig.java | 24 ++- .../mapstruct-reference-guide.asciidoc | 6 + .../ap/internal/model/BeanMappingMethod.java | 2 +- .../model/BuilderFinisherMethodResolver.java | 101 ++++++++++ .../ap/internal/model/common/BuilderType.java | 25 +-- .../ap/internal/model/common/TypeFactory.java | 35 +++- .../ap/internal/model/source/BeanMapping.java | 25 ++- .../ap/internal/prism/PrismGenerator.java | 2 + .../DefaultModelElementProcessorContext.java | 1 + .../ap/internal/util/MapperConfiguration.java | 13 ++ .../mapstruct/ap/internal/util/Message.java | 4 + .../org/mapstruct/ap/spi/BuilderInfo.java | 32 ++-- .../org/mapstruct/ap/spi/BuilderProvider.java | 2 + .../ap/spi/DefaultBuilderProvider.java | 58 ++++-- ...ThanOneBuilderCreationMethodException.java | 49 +++++ .../multiple/BuilderConfigDefinedMapper.java | 39 ++++ .../multiple/BuilderDefinedMapper.java | 39 ++++ .../builder/multiple/BuilderMapperConfig.java | 29 +++ .../multiple/DefaultBuildMethodMapper.java | 33 ++++ ...ErroneousMoreThanOneBuildMethodMapper.java | 36 ++++ ...dMethodWithMapperDefinedMappingMapper.java | 32 ++++ .../multiple/MultipleBuilderMapperTest.java | 174 ++++++++++++++++++ .../ap/test/builder/multiple/Source.java | 35 ++++ .../ap/test/builder/multiple/Task.java | 84 +++++++++ .../TooManyBuilderCreationMethodsMapper.java | 34 ++++ .../test/builder/multiple/build/Process.java | 84 +++++++++ .../test/builder/multiple/builder/Case.java | 83 +++++++++ 30 files changed, 1109 insertions(+), 58 deletions(-) create mode 100644 core-common/src/main/java/org/mapstruct/Builder.java create mode 100644 processor/src/main/java/org/mapstruct/ap/internal/model/BuilderFinisherMethodResolver.java create mode 100644 processor/src/main/java/org/mapstruct/ap/spi/MoreThanOneBuilderCreationMethodException.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/builder/multiple/BuilderConfigDefinedMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/builder/multiple/BuilderDefinedMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/builder/multiple/BuilderMapperConfig.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/builder/multiple/DefaultBuildMethodMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/builder/multiple/ErroneousMoreThanOneBuildMethodMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/builder/multiple/ErroneousMoreThanOneBuildMethodWithMapperDefinedMappingMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/builder/multiple/MultipleBuilderMapperTest.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/builder/multiple/Source.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/builder/multiple/Task.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/builder/multiple/TooManyBuilderCreationMethodsMapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/builder/multiple/build/Process.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/builder/multiple/builder/Case.java diff --git a/core-common/src/main/java/org/mapstruct/BeanMapping.java b/core-common/src/main/java/org/mapstruct/BeanMapping.java index 10d0e70f7..03a0ee10f 100644 --- a/core-common/src/main/java/org/mapstruct/BeanMapping.java +++ b/core-common/src/main/java/org/mapstruct/BeanMapping.java @@ -97,4 +97,23 @@ public @interface BeanMapping { * @since 1.3 */ String[] ignoreUnmappedSourceProperties() default {}; + + /** + * The information that should be used for the builder mappings. This can be used to define custom build methods + * for the builder strategy that one uses. + * + * If no builder is defined the builder given via {@link MapperConfig#builder()} or {@link Mapper#builder()} + * will be applied. + *

+ * NOTE: In case no builder is defined here, in {@link Mapper} or {@link MapperConfig} and there is a single + * build method, then that method would be used. + *

+ * If the builder is defined and there is a single method that does not match the name of the finisher than + * a compile error will occurs + * + * @return the builder information for the method level + * + * @since 1.3 + */ + Builder builder() default @Builder; } diff --git a/core-common/src/main/java/org/mapstruct/Builder.java b/core-common/src/main/java/org/mapstruct/Builder.java new file mode 100644 index 000000000..68d7eb9fc --- /dev/null +++ b/core-common/src/main/java/org/mapstruct/Builder.java @@ -0,0 +1,45 @@ +/** + * Copyright 2012-2017 Gunnar Morling (http://www.gunnarmorling.de/) + * and/or other contributors as indicated by the @authors tag. See the + * copyright.txt file in the distribution for a full listing of all + * contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mapstruct; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.mapstruct.util.Experimental; + +/** + * Configuration of builders, e.g. the name of the final build method. + * + * @author Filip Hrisafov + * + * @since 1.3 + */ +@Retention(RetentionPolicy.CLASS) +@Target({}) +@Experimental +public @interface Builder { + + /** + * The name of the build method that needs to be invoked on the builder to create the type being build + * + * @return the method that needs to tbe invoked on the builder + */ + String buildMethod() default "build"; +} diff --git a/core-common/src/main/java/org/mapstruct/Mapper.java b/core-common/src/main/java/org/mapstruct/Mapper.java index 17bb1115c..a4a19ba50 100644 --- a/core-common/src/main/java/org/mapstruct/Mapper.java +++ b/core-common/src/main/java/org/mapstruct/Mapper.java @@ -18,8 +18,6 @@ */ package org.mapstruct; -import static org.mapstruct.NullValueCheckStrategy.ON_IMPLICIT_CONVERSION; - import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -27,6 +25,8 @@ import java.lang.annotation.Target; import org.mapstruct.factory.Mappers; +import static org.mapstruct.NullValueCheckStrategy.ON_IMPLICIT_CONVERSION; + /** * Marks an interface or abstract class as a mapper and activates the generation of a implementation of that type via * MapStruct. @@ -200,4 +200,22 @@ public @interface Mapper { */ boolean disableSubMappingMethodsGeneration() default false; + /** + * The information that should be used for the builder mappings. This can be used to define custom build methods + * for the builder strategy that one uses. + * + * If no builder is defined the builder given via {@link MapperConfig#builder()} will be applied. + * + *

+ * NOTE: In case no builder is defined here, in {@link BeanMapping} or {@link MapperConfig} and there is a single + * build method, then that method would be used. + *

+ * If the builder is defined and there is a single method that does not match the name of the finisher than + * a compile error will occurs + * + * @return the builder information + * + * @since 1.3 + */ + Builder builder() default @Builder; } diff --git a/core-common/src/main/java/org/mapstruct/MapperConfig.java b/core-common/src/main/java/org/mapstruct/MapperConfig.java index e9bbec0aa..45a95a1bf 100644 --- a/core-common/src/main/java/org/mapstruct/MapperConfig.java +++ b/core-common/src/main/java/org/mapstruct/MapperConfig.java @@ -18,8 +18,6 @@ */ package org.mapstruct; -import static org.mapstruct.NullValueCheckStrategy.ON_IMPLICIT_CONVERSION; - import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -27,6 +25,8 @@ import java.lang.annotation.Target; import org.mapstruct.factory.Mappers; +import static org.mapstruct.NullValueCheckStrategy.ON_IMPLICIT_CONVERSION; + /** * Marks a class or interface as configuration source for generated mappers. This allows to share common configurations * between several mapper classes. @@ -186,4 +186,24 @@ public @interface MapperConfig { * @since 1.2 */ boolean disableSubMappingMethodsGeneration() default false; + + /** + * The information that should be used for the builder mappings. This can be used to define custom build methods + * for the builder strategy that one uses. + * + *

+ * Can be overridden by {@link MapperConfig#builder()}. + * + *

+ * NOTE: In case no builder is defined here, in {@link BeanMapping} or {@link Mapper} and there is a single + * build method, then that method would be used. + *

+ * If the builder is defined and there is a single method that does not match the name of the finisher than + * a compile error will occurs + * + * @return the builder information + * + * @since 1.3 + */ + Builder builder() default @Builder; } diff --git a/documentation/src/main/asciidoc/mapstruct-reference-guide.asciidoc b/documentation/src/main/asciidoc/mapstruct-reference-guide.asciidoc index 0efe2c8da..1a2d481dc 100644 --- a/documentation/src/main/asciidoc/mapstruct-reference-guide.asciidoc +++ b/documentation/src/main/asciidoc/mapstruct-reference-guide.asciidoc @@ -639,6 +639,12 @@ The default implementation of the `BuilderProvider` assumes the following: So for example `Person` has a public static method that returns `PersonBuilder`. * The builder type has a parameterless public method (build method) that returns the type being build 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 methods exists +than this would be used, otherwise a compilation error would be created. +* A specific build method can be defined by using `@Builder` within: `@BeanMapping`, `@Mapper` or `@MapperConfig` +* In case there are multiple builder creation methods that satisfy the above conditions then a `MoreThanOneBuilderCreationMethodException` +will be thrown from the `DefaultBuilderProvider` SPI. +In case of a `MoreThanOneBuilderCreationMethodException` MapStruct will write a warning in the compilation and not use any builder. If such type is found then MapStruct will use that type to perform the mapping to (i.e. it will look for setters into that type). To finish the mapping MapStruct generates code that will invoke the build method of the builder. diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java index 02d2358cf..090ac070b 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java @@ -287,7 +287,7 @@ public class BeanMappingMethod extends NormalTypeMappingMethod { return null; } - return MethodReference.forMethodCall( builderType.getBuildMethod() ); + return BuilderFinisherMethodResolver.getBuilderFinisherMethod( method, builderType, ctx ); } /** diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/BuilderFinisherMethodResolver.java b/processor/src/main/java/org/mapstruct/ap/internal/model/BuilderFinisherMethodResolver.java new file mode 100644 index 000000000..0948c7d11 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/BuilderFinisherMethodResolver.java @@ -0,0 +1,101 @@ +/** + * Copyright 2012-2017 Gunnar Morling (http://www.gunnarmorling.de/) + * and/or other contributors as indicated by the @authors tag. See the + * copyright.txt file in the distribution for a full listing of all + * contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mapstruct.ap.internal.model; + +import java.util.Collection; +import javax.lang.model.element.ExecutableElement; + +import org.mapstruct.ap.internal.model.common.BuilderType; +import org.mapstruct.ap.internal.model.source.BeanMapping; +import org.mapstruct.ap.internal.model.source.Method; +import org.mapstruct.ap.internal.prism.BuilderPrism; +import org.mapstruct.ap.internal.util.MapperConfiguration; +import org.mapstruct.ap.internal.util.Message; +import org.mapstruct.ap.internal.util.Strings; + +import static org.mapstruct.ap.internal.util.Collections.first; + +/** + * @author Filip Hrisafov + */ +public class BuilderFinisherMethodResolver { + + private static final String DEFAULT_BUILD_METHOD_NAME = "build"; + + private BuilderFinisherMethodResolver() { + } + + public static MethodReference getBuilderFinisherMethod(Method method, BuilderType builderType, + MappingBuilderContext ctx) { + Collection buildMethods = builderType.getBuildMethods(); + if ( buildMethods.isEmpty() ) { + //If we reach this method this should never happen + return null; + } + + BuilderPrism builderMapping = builderMappingPrism( method, ctx ); + if ( builderMapping == null && buildMethods.size() == 1 ) { + return MethodReference.forMethodCall( first( buildMethods ).getSimpleName().toString() ); + } + else { + String buildMethodPattern = DEFAULT_BUILD_METHOD_NAME; + if ( builderMapping != null ) { + buildMethodPattern = builderMapping.buildMethod(); + } + for ( ExecutableElement buildMethod : buildMethods ) { + String methodName = buildMethod.getSimpleName().toString(); + if ( methodName.matches( buildMethodPattern ) ) { + return MethodReference.forMethodCall( methodName ); + } + } + + if ( builderMapping == null ) { + ctx.getMessager().printMessage( + method.getExecutable(), + Message.BUILDER_NO_BUILD_METHOD_FOUND_DEFAULT, + buildMethodPattern, + builderType.getBuilder(), + builderType.getBuildingType(), + Strings.join( buildMethods, ", " ) + ); + } + else { + ctx.getMessager().printMessage( + method.getExecutable(), + builderMapping.mirror, + Message.BUILDER_NO_BUILD_METHOD_FOUND, + buildMethodPattern, + builderType.getBuilder(), + builderType.getBuildingType(), + Strings.join( buildMethods, ", " ) + ); + } + } + + return null; + } + + private static BuilderPrism builderMappingPrism(Method method, MappingBuilderContext ctx) { + BeanMapping beanMapping = method.getMappingOptions().getBeanMapping(); + if ( beanMapping != null && beanMapping.getBuilder() != null ) { + return beanMapping.getBuilder(); + } + return MapperConfiguration.getInstanceOn( ctx.getMapperTypeElement() ).getBuilderPrism(); + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/common/BuilderType.java b/processor/src/main/java/org/mapstruct/ap/internal/model/common/BuilderType.java index 25ed3beae..1cfba42f2 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/common/BuilderType.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/common/BuilderType.java @@ -18,6 +18,7 @@ */ package org.mapstruct.ap.internal.model.common; +import java.util.Collection; import javax.lang.model.element.ExecutableElement; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; @@ -33,20 +34,20 @@ public class BuilderType { private final Type owningType; private final Type buildingType; private final ExecutableElement builderCreationMethod; - private final ExecutableElement buildMethod; + private final Collection buildMethods; private BuilderType( Type builder, Type owningType, Type buildingType, ExecutableElement builderCreationMethod, - ExecutableElement buildMethod + Collection buildMethods ) { this.builder = builder; this.owningType = owningType; this.buildingType = buildingType; this.builderCreationMethod = builderCreationMethod; - this.buildMethod = buildMethod; + this.buildMethods = buildMethods; } /** @@ -87,18 +88,17 @@ public class BuilderType { } /** - * The name of the method that needs to be invoked on the builder to create the type being built. - * - * @return the name of the method that needs to be invoked on the type that is being built + * The build methods that can be invoked to create the type being built. + * @return the build methods that can be invoked to create the type being built */ - public String getBuildMethod() { - return buildMethod.getSimpleName().toString(); + public Collection getBuildMethods() { + return buildMethods; } public BuilderInfo asBuilderInfo() { return new BuilderInfo.Builder() .builderCreationMethod( this.builderCreationMethod ) - .buildMethod( this.buildMethod ) + .buildMethod( this.buildMethods ) .build(); } @@ -107,11 +107,6 @@ public class BuilderType { if ( builderInfo == null ) { return null; } - ExecutableElement buildMethod = builderInfo.getBuildMethod(); - if ( !typeUtils.isAssignable( buildMethod.getReturnType(), typeToBuild.getTypeMirror() ) ) { - //TODO throw error - throw new IllegalArgumentException( "Build return type is not assignable" ); - } Type builder = typeFactory.getType( builderInfo.getBuilderCreationMethod().getReturnType() ); ExecutableElement builderCreationMethod = builderInfo.getBuilderCreationMethod(); @@ -133,7 +128,7 @@ public class BuilderType { owner, typeToBuild, builderCreationMethod, - buildMethod + builderInfo.getBuildMethods() ); } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/common/TypeFactory.java b/processor/src/main/java/org/mapstruct/ap/internal/model/common/TypeFactory.java index 476898fd7..13e38cc5c 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/common/TypeFactory.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/common/TypeFactory.java @@ -55,11 +55,16 @@ import javax.lang.model.util.Types; import org.mapstruct.ap.internal.util.AnnotationProcessingException; import org.mapstruct.ap.internal.util.Collections; +import org.mapstruct.ap.internal.util.Extractor; +import org.mapstruct.ap.internal.util.FormattingMessager; import org.mapstruct.ap.internal.util.JavaStreamConstants; +import org.mapstruct.ap.internal.util.Message; import org.mapstruct.ap.internal.util.RoundContext; +import org.mapstruct.ap.internal.util.Strings; import org.mapstruct.ap.internal.util.accessor.Accessor; import org.mapstruct.ap.spi.AstModifyingAnnotationProcessor; import org.mapstruct.ap.spi.BuilderInfo; +import org.mapstruct.ap.spi.MoreThanOneBuilderCreationMethodException; import org.mapstruct.ap.spi.TypeHierarchyErroneousException; import static org.mapstruct.ap.internal.model.common.ImplementationType.withDefaultConstructor; @@ -73,8 +78,17 @@ import static org.mapstruct.ap.internal.model.common.ImplementationType.withLoad */ public class TypeFactory { + private static final Extractor BUILDER_INFO_CREATION_METHOD_EXTRACTOR = + new Extractor() { + @Override + public String apply(BuilderInfo builderInfo) { + return builderInfo.getBuilderCreationMethod().toString(); + } + }; + private final Elements elementUtils; private final Types typeUtils; + private final FormattingMessager messager; private final RoundContext roundContext; private final TypeMirror iterableType; @@ -85,9 +99,10 @@ public class TypeFactory { private final Map implementationTypes = new HashMap(); private final Map importedQualifiedTypesBySimpleName = new HashMap(); - public TypeFactory(Elements elementUtils, Types typeUtils, RoundContext roundContext) { + public TypeFactory(Elements elementUtils, Types typeUtils, FormattingMessager messager, RoundContext roundContext) { this.elementUtils = elementUtils; this.typeUtils = typeUtils; + this.messager = messager; this.roundContext = roundContext; iterableType = typeUtils.erasure( elementUtils.getTypeElement( Iterable.class.getCanonicalName() ).asType() ); @@ -502,9 +517,21 @@ public class TypeFactory { } private BuilderInfo findBuilder(TypeMirror type) { - return roundContext.getAnnotationProcessorContext() - .getBuilderProvider() - .findBuilderInfo( type, elementUtils, typeUtils ); + try { + return roundContext.getAnnotationProcessorContext() + .getBuilderProvider() + .findBuilderInfo( type, elementUtils, typeUtils ); + } + catch ( MoreThanOneBuilderCreationMethodException ex ) { + messager.printMessage( + typeUtils.asElement( type ), + Message.BUILDER_MORE_THAN_ONE_BUILDER_CREATION_METHOD, + type, + Strings.join( ex.getBuilderInfo(), ", ", BUILDER_INFO_CREATION_METHOD_EXTRACTOR ) + ); + } + + return null; } private TypeMirror getComponentType(TypeMirror mirror) { diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/source/BeanMapping.java b/processor/src/main/java/org/mapstruct/ap/internal/model/source/BeanMapping.java index e3580de65..a181c3a99 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/source/BeanMapping.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/source/BeanMapping.java @@ -25,6 +25,7 @@ import javax.lang.model.type.TypeKind; import javax.lang.model.util.Types; import org.mapstruct.ap.internal.prism.BeanMappingPrism; +import org.mapstruct.ap.internal.prism.BuilderPrism; import org.mapstruct.ap.internal.prism.NullValueMappingStrategyPrism; import org.mapstruct.ap.internal.prism.ReportingPolicyPrism; import org.mapstruct.ap.internal.util.FormattingMessager; @@ -42,6 +43,7 @@ public class BeanMapping { private final ReportingPolicyPrism reportingPolicy; private final boolean ignoreByDefault; private final List ignoreUnmappedSourceProperties; + private final BuilderPrism builder; /** * creates a mapping for inheritance. Will set ignoreByDefault to false. @@ -55,7 +57,8 @@ public class BeanMapping { map.nullValueMappingStrategy, map.reportingPolicy, false, - map.ignoreUnmappedSourceProperties + map.ignoreUnmappedSourceProperties, + map.builder ); } @@ -74,9 +77,15 @@ public class BeanMapping { : NullValueMappingStrategyPrism.valueOf( beanMapping.nullValueMappingStrategy() ); boolean ignoreByDefault = beanMapping.ignoreByDefault(); + BuilderPrism builderMapping = null; + if ( beanMapping.values.builder() != null ) { + builderMapping = beanMapping.builder(); + } + if ( !resultTypeIsDefined && beanMapping.qualifiedBy().isEmpty() && beanMapping.qualifiedByName().isEmpty() && beanMapping.ignoreUnmappedSourceProperties().isEmpty() - && ( nullValueMappingStrategy == null ) && !ignoreByDefault ) { + && ( nullValueMappingStrategy == null ) && !ignoreByDefault + && builderMapping == null ) { messager.printMessage( method, Message.BEANMAPPING_NO_ELEMENTS ); } @@ -94,7 +103,8 @@ public class BeanMapping { nullValueMappingStrategy, null, ignoreByDefault, - beanMapping.ignoreUnmappedSourceProperties() + beanMapping.ignoreUnmappedSourceProperties(), + builderMapping ); } @@ -105,17 +115,18 @@ public class BeanMapping { * @return bean mapping that needs to be used for Mappings */ public static BeanMapping forForgedMethods() { - return new BeanMapping( null, null, ReportingPolicyPrism.IGNORE, false, Collections.emptyList() ); + return new BeanMapping( null, null, ReportingPolicyPrism.IGNORE, false, Collections.emptyList(), null ); } private BeanMapping(SelectionParameters selectionParameters, NullValueMappingStrategyPrism nvms, ReportingPolicyPrism reportingPolicy, boolean ignoreByDefault, - List ignoreUnmappedSourceProperties) { + List ignoreUnmappedSourceProperties, BuilderPrism builder) { this.selectionParameters = selectionParameters; this.nullValueMappingStrategy = nvms; this.reportingPolicy = reportingPolicy; this.ignoreByDefault = ignoreByDefault; this.ignoreUnmappedSourceProperties = ignoreUnmappedSourceProperties; + this.builder = builder; } public SelectionParameters getSelectionParameters() { @@ -137,4 +148,8 @@ public class BeanMapping { public List getIgnoreUnmappedSourceProperties() { return ignoreUnmappedSourceProperties; } + + public BuilderPrism getBuilder() { + return builder; + } } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/prism/PrismGenerator.java b/processor/src/main/java/org/mapstruct/ap/internal/prism/PrismGenerator.java index c07d5c371..5b2a6fb7d 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/prism/PrismGenerator.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/prism/PrismGenerator.java @@ -24,6 +24,7 @@ import javax.xml.bind.annotation.XmlElementRef; import org.mapstruct.AfterMapping; import org.mapstruct.BeanMapping; import org.mapstruct.BeforeMapping; +import org.mapstruct.Builder; import org.mapstruct.Context; import org.mapstruct.DecoratedWith; import org.mapstruct.InheritConfiguration; @@ -71,6 +72,7 @@ import net.java.dev.hickory.prism.GeneratePrisms; @GeneratePrism(value = ValueMapping.class, publicAccess = true), @GeneratePrism(value = ValueMappings.class, publicAccess = true), @GeneratePrism(value = Context.class, publicAccess = true), + @GeneratePrism(value = Builder.class, publicAccess = true), // external types @GeneratePrism(value = XmlElementDecl.class, publicAccess = true), diff --git a/processor/src/main/java/org/mapstruct/ap/internal/processor/DefaultModelElementProcessorContext.java b/processor/src/main/java/org/mapstruct/ap/internal/processor/DefaultModelElementProcessorContext.java index 07e0e1a90..70ef02e45 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/processor/DefaultModelElementProcessorContext.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/processor/DefaultModelElementProcessorContext.java @@ -64,6 +64,7 @@ public class DefaultModelElementProcessorContext implements ProcessorContext { this.typeFactory = new TypeFactory( processingEnvironment.getElementUtils(), delegatingTypes, + messager, roundContext ); this.options = options; diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/MapperConfiguration.java b/processor/src/main/java/org/mapstruct/ap/internal/util/MapperConfiguration.java index 457601538..d7bb90260 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/MapperConfiguration.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/MapperConfiguration.java @@ -28,6 +28,7 @@ import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeMirror; import org.mapstruct.ap.internal.option.Options; +import org.mapstruct.ap.internal.prism.BuilderPrism; import org.mapstruct.ap.internal.prism.CollectionMappingStrategyPrism; import org.mapstruct.ap.internal.prism.InjectionStrategyPrism; import org.mapstruct.ap.internal.prism.MapperConfigPrism; @@ -238,6 +239,18 @@ public class MapperConfiguration { return mapperPrism.disableSubMappingMethodsGeneration(); // fall back to default defined in the annotation } + public BuilderPrism getBuilderPrism() { + if ( mapperPrism.values.builder() != null ) { + return mapperPrism.builder(); + } + else if ( mapperConfigPrism != null && mapperConfigPrism.values.builder() != null ) { + return mapperConfigPrism.builder(); + } + else { + return null; + } + } + public DeclaredType config() { return config; } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/Message.java b/processor/src/main/java/org/mapstruct/ap/internal/util/Message.java index 0bb6b788f..de257c2e6 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/Message.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/Message.java @@ -100,6 +100,10 @@ public enum Message { GENERAL_NOT_ALL_FORGED_CREATED( "Internal Error in creation of Forged Methods, it was expected all Forged Methods to finished with creation, but %s did not" ), GENERAL_NO_SUITABLE_CONSTRUCTOR( "%s does not have an accessible parameterless constructor." ), + BUILDER_MORE_THAN_ONE_BUILDER_CREATION_METHOD( "More than one builder creation method for \"%s\". Found methods: \"%s\". Builder will not be used. Consider implementing a custom BuilderProvider SPI.", Diagnostic.Kind.WARNING ), + BUILDER_NO_BUILD_METHOD_FOUND("No build method \"%s\" found in \"%s\" for \"%s\". Found methods: \"%s\".", Diagnostic.Kind.ERROR ), + BUILDER_NO_BUILD_METHOD_FOUND_DEFAULT("No build method \"%s\" found in \"%s\" for \"%s\". Found methods: \"%s\". Consider to add @Builder in order to select the correct build method.", Diagnostic.Kind.ERROR ), + RETRIEVAL_NO_INPUT_ARGS( "Can't generate mapping method with no input arguments." ), RETRIEVAL_DUPLICATE_MAPPING_TARGETS( "Can't generate mapping method with more than one @MappingTarget parameter." ), RETRIEVAL_VOID_MAPPING_METHOD( "Can't generate mapping method with return type void." ), diff --git a/processor/src/main/java/org/mapstruct/ap/spi/BuilderInfo.java b/processor/src/main/java/org/mapstruct/ap/spi/BuilderInfo.java index 9e6157688..2be9c01fb 100644 --- a/processor/src/main/java/org/mapstruct/ap/spi/BuilderInfo.java +++ b/processor/src/main/java/org/mapstruct/ap/spi/BuilderInfo.java @@ -18,6 +18,7 @@ */ package org.mapstruct.ap.spi; +import java.util.Collection; import javax.lang.model.element.ExecutableElement; /** @@ -28,11 +29,11 @@ import javax.lang.model.element.ExecutableElement; public class BuilderInfo { private final ExecutableElement builderCreationMethod; - private final ExecutableElement buildMethod; + private final Collection buildMethods; - private BuilderInfo(ExecutableElement builderCreationMethod, ExecutableElement buildMethod) { + private BuilderInfo(ExecutableElement builderCreationMethod, Collection buildMethods) { this.builderCreationMethod = builderCreationMethod; - this.buildMethod = buildMethod; + this.buildMethods = buildMethods; } /** @@ -50,18 +51,18 @@ public class BuilderInfo { } /** - * The method that can be used to build the type being built. - * This should be a {@code public} method within the builder itself + * The methods that can be used to build the type being built. + * This should be {@code public} methods within the builder itself * * @return the build method for the type */ - public ExecutableElement getBuildMethod() { - return buildMethod; + public Collection getBuildMethods() { + return buildMethods; } public static class Builder { private ExecutableElement builderCreationMethod; - private ExecutableElement buildMethod; + private Collection buildMethods; /** * @see BuilderInfo#getBuilderCreationMethod() @@ -72,10 +73,10 @@ public class BuilderInfo { } /** - * @see BuilderInfo#getBuildMethod() + * @see BuilderInfo#getBuildMethods() */ - public Builder buildMethod(ExecutableElement method) { - this.buildMethod = method; + public Builder buildMethod(Collection methods) { + this.buildMethods = methods; return this; } @@ -87,10 +88,13 @@ public class BuilderInfo { if ( builderCreationMethod == null ) { throw new IllegalArgumentException( "Builder creation method is mandatory" ); } - else if ( buildMethod == null ) { - throw new IllegalArgumentException( "Build method is mandatory" ); + else if ( buildMethods == null ) { + throw new IllegalArgumentException( "Build methods are mandatory" ); } - return new BuilderInfo( builderCreationMethod, buildMethod ); + else if ( buildMethods.isEmpty() ) { + throw new IllegalArgumentException( "Build methods must not be empty" ); + } + return new BuilderInfo( builderCreationMethod, buildMethods ); } } } diff --git a/processor/src/main/java/org/mapstruct/ap/spi/BuilderProvider.java b/processor/src/main/java/org/mapstruct/ap/spi/BuilderProvider.java index 96c337543..fb48fcce6 100644 --- a/processor/src/main/java/org/mapstruct/ap/spi/BuilderProvider.java +++ b/processor/src/main/java/org/mapstruct/ap/spi/BuilderProvider.java @@ -40,6 +40,8 @@ public interface BuilderProvider { * * @throws TypeHierarchyErroneousException if the type that needs to be visited is not ready yet, this signals the * MapStruct processor to postpone the generation of the mappers to the next round + * @throws MoreThanOneBuilderCreationMethodException if {@code type} has more than one method that can create the + * builder */ BuilderInfo findBuilderInfo(TypeMirror type, Elements elements, Types types); } diff --git a/processor/src/main/java/org/mapstruct/ap/spi/DefaultBuilderProvider.java b/processor/src/main/java/org/mapstruct/ap/spi/DefaultBuilderProvider.java index 75b83f55a..365efa02f 100644 --- a/processor/src/main/java/org/mapstruct/ap/spi/DefaultBuilderProvider.java +++ b/processor/src/main/java/org/mapstruct/ap/spi/DefaultBuilderProvider.java @@ -18,6 +18,9 @@ */ package org.mapstruct.ap.spi; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.regex.Pattern; import javax.lang.model.element.ExecutableElement; @@ -141,17 +144,22 @@ public class DefaultBuilderProvider implements BuilderProvider { *

* The default implementation iterates over all the methods in {@code typeElement} and uses * {@link DefaultBuilderProvider#isPossibleBuilderCreationMethod(ExecutableElement, TypeElement, Types)} and - * {@link DefaultBuilderProvider#findBuildMethod(TypeElement, TypeElement, Types)} to create the + * {@link DefaultBuilderProvider#findBuildMethods(TypeElement, TypeElement, Types)} to create the * {@link BuilderInfo}. *

* The default implementation uses {@link DefaultBuilderProvider#shouldIgnore(TypeElement)} to check if the * {@code typeElement} should be ignored. + *

+ * In case there are multiple {@link BuilderInfo} then a {@link MoreThanOneBuilderCreationMethodException} is + * thrown. * * @param typeElement the type element for which a builder searched * @param elements the util elements that can be used for operating on the type element * @param types the util types that can be used for operation on {@link TypeMirror}(s) * * @return the {@link BuilderInfo} or {@code null} if no builder was found for the type + * {@link DefaultBuilderProvider#findBuildMethods(TypeElement, TypeElement, Types)} + * @throws MoreThanOneBuilderCreationMethodException if there are multiple builder creation methods */ protected BuilderInfo findBuilderInfo(TypeElement typeElement, Elements elements, Types types) { if ( shouldIgnore( typeElement ) ) { @@ -159,18 +167,28 @@ public class DefaultBuilderProvider implements BuilderProvider { } List methods = ElementFilter.methodsIn( typeElement.getEnclosedElements() ); + List builderInfo = new ArrayList(); for ( ExecutableElement method : methods ) { if ( isPossibleBuilderCreationMethod( method, typeElement, types ) ) { TypeElement builderElement = getTypeElement( method.getReturnType() ); - ExecutableElement buildMethod = findBuildMethod( builderElement, typeElement, types ); - if ( buildMethod != null ) { - return new BuilderInfo.Builder() + Collection buildMethods = findBuildMethods( builderElement, typeElement, types ); + if ( !buildMethods.isEmpty() ) { + builderInfo.add( new BuilderInfo.Builder() .builderCreationMethod( method ) - .buildMethod( buildMethod ) - .build(); + .buildMethod( buildMethods ) + .build() + ); } } } + + if ( builderInfo.size() == 1 ) { + return builderInfo.get( 0 ); + } + else if ( builderInfo.size() > 1 ) { + throw new MoreThanOneBuilderCreationMethodException( typeElement.asType(), builderInfo ); + } + return findBuilderInfo( typeElement.getSuperclass(), elements, types ); } @@ -207,30 +225,40 @@ public class DefaultBuilderProvider implements BuilderProvider { *

* The default implementation uses {@link DefaultBuilderProvider#shouldIgnore(TypeElement)} to check if the * {@code builderElement} should be ignored, i.e. not checked for build elements. - * + *

+ * If there are multiple methods that satisfy + * {@link DefaultBuilderProvider#isBuildMethod(ExecutableElement, TypeElement, Types)} and one of those methods + * is names {@code build} that that method would be considered as a build method. * @param builderElement the element for the builder * @param typeElement the element for the type that is being built * @param types the util types tat can be used for operations on {@link TypeMirror}(s) * * @return the build method for the {@code typeElement} if it exists, or {@code null} if it does not + * {@code build} */ - protected ExecutableElement findBuildMethod(TypeElement builderElement, TypeElement typeElement, Types types) { + protected Collection findBuildMethods(TypeElement builderElement, TypeElement typeElement, + Types types) { if ( shouldIgnore( builderElement ) ) { - return null; + return Collections.emptyList(); } List builderMethods = ElementFilter.methodsIn( builderElement.getEnclosedElements() ); + List buildMethods = new ArrayList(); for ( ExecutableElement buildMethod : builderMethods ) { if ( isBuildMethod( buildMethod, typeElement, types ) ) { - return buildMethod; + buildMethods.add( buildMethod ); } } - return findBuildMethod( - getTypeElement( builderElement.getSuperclass() ), - typeElement, - types - ); + if ( buildMethods.isEmpty() ) { + return findBuildMethods( + getTypeElement( builderElement.getSuperclass() ), + typeElement, + types + ); + } + + return buildMethods; } /** diff --git a/processor/src/main/java/org/mapstruct/ap/spi/MoreThanOneBuilderCreationMethodException.java b/processor/src/main/java/org/mapstruct/ap/spi/MoreThanOneBuilderCreationMethodException.java new file mode 100644 index 000000000..8d1038aa0 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/spi/MoreThanOneBuilderCreationMethodException.java @@ -0,0 +1,49 @@ +/** + * Copyright 2012-2017 Gunnar Morling (http://www.gunnarmorling.de/) + * and/or other contributors as indicated by the @authors tag. See the + * copyright.txt file in the distribution for a full listing of all + * contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mapstruct.ap.spi; + +import java.util.List; +import javax.lang.model.type.TypeMirror; + +/** + * Indicates that a type has too many builder creation methods. + * This exception can be used to signal the MapStruct processor that more than one builder creation method was found. + * + * @author Filip Hrisafov + */ +public class MoreThanOneBuilderCreationMethodException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + private final TypeMirror type; + private final List builderCreationMethods; + + public MoreThanOneBuilderCreationMethodException(TypeMirror type, List builderCreationMethods) { + this.type = type; + this.builderCreationMethods = builderCreationMethods; + } + + public TypeMirror getType() { + return type; + } + + public List getBuilderInfo() { + return builderCreationMethods; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/builder/multiple/BuilderConfigDefinedMapper.java b/processor/src/test/java/org/mapstruct/ap/test/builder/multiple/BuilderConfigDefinedMapper.java new file mode 100644 index 000000000..79eb4de38 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/builder/multiple/BuilderConfigDefinedMapper.java @@ -0,0 +1,39 @@ +/** + * Copyright 2012-2017 Gunnar Morling (http://www.gunnarmorling.de/) + * and/or other contributors as indicated by the @authors tag. See the + * copyright.txt file in the distribution for a full listing of all + * contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mapstruct.ap.test.builder.multiple; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Builder; +import org.mapstruct.Mapper; +import org.mapstruct.ap.test.builder.multiple.build.Process; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper(config = BuilderMapperConfig.class) +public interface BuilderConfigDefinedMapper { + + BuilderConfigDefinedMapper INSTANCE = Mappers.getMapper( BuilderConfigDefinedMapper.class ); + + Process map(Source source); + + @BeanMapping(builder = @Builder(buildMethod = "wrongCreate")) + Process wrongMap(Source source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/builder/multiple/BuilderDefinedMapper.java b/processor/src/test/java/org/mapstruct/ap/test/builder/multiple/BuilderDefinedMapper.java new file mode 100644 index 000000000..85e497ce2 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/builder/multiple/BuilderDefinedMapper.java @@ -0,0 +1,39 @@ +/** + * Copyright 2012-2017 Gunnar Morling (http://www.gunnarmorling.de/) + * and/or other contributors as indicated by the @authors tag. See the + * copyright.txt file in the distribution for a full listing of all + * contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mapstruct.ap.test.builder.multiple; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Builder; +import org.mapstruct.Mapper; +import org.mapstruct.ap.test.builder.multiple.build.Process; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper(builder = @Builder(buildMethod = "create")) +public interface BuilderDefinedMapper { + + BuilderDefinedMapper INSTANCE = Mappers.getMapper( BuilderDefinedMapper.class ); + + Process map(Source source); + + @BeanMapping(builder = @Builder(buildMethod = "wrongCreate")) + Process wrongMap(Source source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/builder/multiple/BuilderMapperConfig.java b/processor/src/test/java/org/mapstruct/ap/test/builder/multiple/BuilderMapperConfig.java new file mode 100644 index 000000000..90f69a149 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/builder/multiple/BuilderMapperConfig.java @@ -0,0 +1,29 @@ +/** + * Copyright 2012-2017 Gunnar Morling (http://www.gunnarmorling.de/) + * and/or other contributors as indicated by the @authors tag. See the + * copyright.txt file in the distribution for a full listing of all + * contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mapstruct.ap.test.builder.multiple; + +import org.mapstruct.Builder; +import org.mapstruct.MapperConfig; + +/** + * @author Filip Hrisafov + */ +@MapperConfig(builder = @Builder(buildMethod = "create")) +public interface BuilderMapperConfig { +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/builder/multiple/DefaultBuildMethodMapper.java b/processor/src/test/java/org/mapstruct/ap/test/builder/multiple/DefaultBuildMethodMapper.java new file mode 100644 index 000000000..ce39c7e36 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/builder/multiple/DefaultBuildMethodMapper.java @@ -0,0 +1,33 @@ +/** + * Copyright 2012-2017 Gunnar Morling (http://www.gunnarmorling.de/) + * and/or other contributors as indicated by the @authors tag. See the + * copyright.txt file in the distribution for a full listing of all + * contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mapstruct.ap.test.builder.multiple; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface DefaultBuildMethodMapper { + + DefaultBuildMethodMapper INSTANCE = Mappers.getMapper( DefaultBuildMethodMapper.class ); + + Task map(Source source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/builder/multiple/ErroneousMoreThanOneBuildMethodMapper.java b/processor/src/test/java/org/mapstruct/ap/test/builder/multiple/ErroneousMoreThanOneBuildMethodMapper.java new file mode 100644 index 000000000..dedad6f2d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/builder/multiple/ErroneousMoreThanOneBuildMethodMapper.java @@ -0,0 +1,36 @@ +/** + * Copyright 2012-2017 Gunnar Morling (http://www.gunnarmorling.de/) + * and/or other contributors as indicated by the @authors tag. See the + * copyright.txt file in the distribution for a full listing of all + * contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mapstruct.ap.test.builder.multiple; + +import org.mapstruct.BeanMapping; +import org.mapstruct.Builder; +import org.mapstruct.Mapper; +import org.mapstruct.ap.test.builder.multiple.build.Process; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface ErroneousMoreThanOneBuildMethodMapper { + + Process map(Source source); + + @BeanMapping(builder = @Builder(buildMethod = "missingBuild")) + Process map2(Source source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/builder/multiple/ErroneousMoreThanOneBuildMethodWithMapperDefinedMappingMapper.java b/processor/src/test/java/org/mapstruct/ap/test/builder/multiple/ErroneousMoreThanOneBuildMethodWithMapperDefinedMappingMapper.java new file mode 100644 index 000000000..a7ff63573 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/builder/multiple/ErroneousMoreThanOneBuildMethodWithMapperDefinedMappingMapper.java @@ -0,0 +1,32 @@ +/** + * Copyright 2012-2017 Gunnar Morling (http://www.gunnarmorling.de/) + * and/or other contributors as indicated by the @authors tag. See the + * copyright.txt file in the distribution for a full listing of all + * contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mapstruct.ap.test.builder.multiple; + +import org.mapstruct.Builder; +import org.mapstruct.Mapper; +import org.mapstruct.ap.test.builder.multiple.build.Process; + +/** + * @author Filip Hrisafov + */ +@Mapper(builder = @Builder(buildMethod = "mapperBuild")) +public interface ErroneousMoreThanOneBuildMethodWithMapperDefinedMappingMapper { + + Process map(Source source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/builder/multiple/MultipleBuilderMapperTest.java b/processor/src/test/java/org/mapstruct/ap/test/builder/multiple/MultipleBuilderMapperTest.java new file mode 100644 index 000000000..12bd0dd63 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/builder/multiple/MultipleBuilderMapperTest.java @@ -0,0 +1,174 @@ +/** + * Copyright 2012-2017 Gunnar Morling (http://www.gunnarmorling.de/) + * and/or other contributors as indicated by the @authors tag. See the + * copyright.txt file in the distribution for a full listing of all + * contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mapstruct.ap.test.builder.multiple; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mapstruct.ap.test.builder.multiple.build.Process; +import org.mapstruct.ap.test.builder.multiple.builder.Case; +import org.mapstruct.ap.testutil.IssueKey; +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.AnnotationProcessorTestRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Filip Hrisafov + */ +@RunWith(AnnotationProcessorTestRunner.class) +@IssueKey("1479") +@WithClasses({ + Process.class, + Case.class, + Task.class, + Source.class +}) +public class MultipleBuilderMapperTest { + + @WithClasses({ + ErroneousMoreThanOneBuildMethodMapper.class + }) + @ExpectedCompilationOutcome(value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic( + type = ErroneousMoreThanOneBuildMethodMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 32, + messageRegExp = "No build method \"build\" found in \".*\\.multiple\\.build\\.Process\\.Builder\" " + + "for \".*\\.multiple\\.build\\.Process\"\\. " + + "Found methods: " + + "\".*wrongCreate\\(\\) ?, " + + ".*create\\(\\) ?\"\\. " + + "Consider to add @Builder in order to select the correct build method." + ), + @Diagnostic( + type = ErroneousMoreThanOneBuildMethodMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 34, + messageRegExp = "No build method \"missingBuild\" found " + + "in \".*\\.multiple\\.build\\.Process\\.Builder\" " + + "for \".*\\.multiple\\.build\\.Process\"\\. " + + "Found methods: " + + "\".*wrongCreate\\(\\) ?, " + + ".*create\\(\\) ?\"\\." + ) + }) + @Test + public void moreThanOneBuildMethod() { + } + + @WithClasses({ + ErroneousMoreThanOneBuildMethodWithMapperDefinedMappingMapper.class + }) + @ExpectedCompilationOutcome(value = CompilationResult.FAILED, + diagnostics = { + @Diagnostic( + type = ErroneousMoreThanOneBuildMethodWithMapperDefinedMappingMapper.class, + kind = javax.tools.Diagnostic.Kind.ERROR, + line = 31, + messageRegExp = + "No build method \"mapperBuild\" found in \".*\\.multiple\\.build\\.Process\\.Builder\" " + + "for \".*\\.multiple\\.build\\.Process\"\\. " + + "Found methods: " + + "\".*wrongCreate\\(\\) ?, " + + ".*create\\(\\) ?\"\\." + ) + }) + @Test + public void moreThanOneBuildMethodDefinedOnMapper() { + } + + @WithClasses({ + BuilderDefinedMapper.class + }) + @Test + public void builderMappingDefined() { + Process map = BuilderDefinedMapper.INSTANCE.map( new Source( "map" ) ); + Process wrongMap = BuilderDefinedMapper.INSTANCE.wrongMap( new Source( "wrongMap" ) ); + + assertThat( map.getBuildMethod() ).isEqualTo( "create" ); + assertThat( wrongMap.getBuildMethod() ).isEqualTo( "wrongCreate" ); + } + + @WithClasses({ + BuilderMapperConfig.class, + BuilderConfigDefinedMapper.class + }) + @Test + public void builderMappingMapperConfigDefined() { + Process map = BuilderConfigDefinedMapper.INSTANCE.map( new Source( "map" ) ); + Process wrongMap = BuilderConfigDefinedMapper.INSTANCE.wrongMap( new Source( "wrongMap" ) ); + + assertThat( map.getBuildMethod() ).isEqualTo( "create" ); + assertThat( wrongMap.getBuildMethod() ).isEqualTo( "wrongCreate" ); + } + + @WithClasses({ + TooManyBuilderCreationMethodsMapper.class + }) + @ExpectedCompilationOutcome(value = CompilationResult.SUCCEEDED, + // We have 2 diagnostics, as we don't do caching of the types, so a type is processed multiple types + diagnostics = { + @Diagnostic( + type = Case.class, + kind = javax.tools.Diagnostic.Kind.WARNING, + line = 24, + messageRegExp = "More than one builder creation method for \".*\\.multiple\\.builder.Case\"\\. " + + "Found methods: " + + "\".*wrongBuilder\\(\\) ?, " + + ".*builder\\(\\) ?\"\\. " + + "Builder will not be used\\. Consider implementing a custom BuilderProvider SPI\\." + ), + @Diagnostic( + type = Case.class, + kind = javax.tools.Diagnostic.Kind.WARNING, + line = 24, + messageRegExp = "More than one builder creation method for \".*\\.multiple\\.builder.Case\"\\. " + + "Found methods: " + + "\".*wrongBuilder\\(\\) ?, " + + ".*builder\\(\\) ?\"\\. " + + "Builder will not be used\\. Consider implementing a custom BuilderProvider SPI\\." + ) + }) + @Test + public void tooManyBuilderCreationMethods() { + Case caseTarget = TooManyBuilderCreationMethodsMapper.INSTANCE.map( new Source( "test" ) ); + + assertThat( caseTarget ).isNotNull(); + assertThat( caseTarget.getName() ).isEqualTo( "test" ); + assertThat( caseTarget.getBuilderCreationMethod() ).isNull(); + assertThat( caseTarget.getBuildMethod() ).isEqualTo( "constructor" ); + } + + @WithClasses( { + DefaultBuildMethodMapper.class + } ) + @Test + public void defaultBuildMethod() { + Task task = DefaultBuildMethodMapper.INSTANCE.map( new Source( "test" ) ); + + assertThat( task ).isNotNull(); + assertThat( task.getName() ).isEqualTo( "test" ); + assertThat( task.getBuilderCreationMethod() ).isEqualTo( "builder" ); + assertThat( task.getBuildMethod() ).isEqualTo( "build" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/builder/multiple/Source.java b/processor/src/test/java/org/mapstruct/ap/test/builder/multiple/Source.java new file mode 100644 index 000000000..93c0ab59f --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/builder/multiple/Source.java @@ -0,0 +1,35 @@ +/** + * Copyright 2012-2017 Gunnar Morling (http://www.gunnarmorling.de/) + * and/or other contributors as indicated by the @authors tag. See the + * copyright.txt file in the distribution for a full listing of all + * contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mapstruct.ap.test.builder.multiple; + +/** + * @author Filip Hrisafov + */ +public class Source { + + private final String name; + + public Source(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/builder/multiple/Task.java b/processor/src/test/java/org/mapstruct/ap/test/builder/multiple/Task.java new file mode 100644 index 000000000..234fe6e85 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/builder/multiple/Task.java @@ -0,0 +1,84 @@ +/** + * Copyright 2012-2017 Gunnar Morling (http://www.gunnarmorling.de/) + * and/or other contributors as indicated by the @authors tag. See the + * copyright.txt file in the distribution for a full listing of all + * contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mapstruct.ap.test.builder.multiple; + +/** + * @author Filip Hrisafov + */ +public class Task { + + private final String builderCreationMethod; + private final String buildMethod; + private String name; + + public Task() { + this.builderCreationMethod = null; + this.buildMethod = "constructor"; + } + + public Task(Builder builder, String buildMethod) { + this.builderCreationMethod = builder.builderCreationMethod; + this.buildMethod = buildMethod; + this.name = builder.name; + } + + public String getBuilderCreationMethod() { + return builderCreationMethod; + } + + public String getBuildMethod() { + return buildMethod; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public static Builder builder() { + return new Builder( "builder" ); + } + + public static class Builder { + + private String name; + private String builderCreationMethod; + + protected Builder(String builderCreationMethod) { + this.builderCreationMethod = builderCreationMethod; + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Task wrongCreate() { + return new Task( this, "wrongCreate" ); + } + + public Task build() { + return new Task( this, "build" ); + } + + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/builder/multiple/TooManyBuilderCreationMethodsMapper.java b/processor/src/test/java/org/mapstruct/ap/test/builder/multiple/TooManyBuilderCreationMethodsMapper.java new file mode 100644 index 000000000..1b935eb2e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/builder/multiple/TooManyBuilderCreationMethodsMapper.java @@ -0,0 +1,34 @@ +/** + * Copyright 2012-2017 Gunnar Morling (http://www.gunnarmorling.de/) + * and/or other contributors as indicated by the @authors tag. See the + * copyright.txt file in the distribution for a full listing of all + * contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mapstruct.ap.test.builder.multiple; + +import org.mapstruct.Mapper; +import org.mapstruct.ap.test.builder.multiple.builder.Case; +import org.mapstruct.factory.Mappers; + +/** + * @author Filip Hrisafov + */ +@Mapper +public interface TooManyBuilderCreationMethodsMapper { + + TooManyBuilderCreationMethodsMapper INSTANCE = Mappers.getMapper( TooManyBuilderCreationMethodsMapper.class ); + + Case map(Source source); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/builder/multiple/build/Process.java b/processor/src/test/java/org/mapstruct/ap/test/builder/multiple/build/Process.java new file mode 100644 index 000000000..939249f53 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/builder/multiple/build/Process.java @@ -0,0 +1,84 @@ +/** + * Copyright 2012-2017 Gunnar Morling (http://www.gunnarmorling.de/) + * and/or other contributors as indicated by the @authors tag. See the + * copyright.txt file in the distribution for a full listing of all + * contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mapstruct.ap.test.builder.multiple.build; + +/** + * @author Filip Hrisafov + */ +public class Process { + + private final String builderCreationMethod; + private final String buildMethod; + private String name; + + public Process() { + this.builderCreationMethod = null; + this.buildMethod = "constructor"; + } + + public Process(Builder builder, String buildMethod) { + this.builderCreationMethod = builder.builderCreationMethod; + this.buildMethod = buildMethod; + this.name = builder.name; + } + + public String getBuilderCreationMethod() { + return builderCreationMethod; + } + + public String getBuildMethod() { + return buildMethod; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public static Builder builder() { + return new Builder( "builder" ); + } + + public static class Builder { + + private String name; + private String builderCreationMethod; + + protected Builder(String builderCreationMethod) { + this.builderCreationMethod = builderCreationMethod; + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Process wrongCreate() { + return new Process( this, "wrongCreate" ); + } + + public Process create() { + return new Process( this, "create" ); + } + + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/builder/multiple/builder/Case.java b/processor/src/test/java/org/mapstruct/ap/test/builder/multiple/builder/Case.java new file mode 100644 index 000000000..a4251abd5 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/builder/multiple/builder/Case.java @@ -0,0 +1,83 @@ +/** + * Copyright 2012-2017 Gunnar Morling (http://www.gunnarmorling.de/) + * and/or other contributors as indicated by the @authors tag. See the + * copyright.txt file in the distribution for a full listing of all + * contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mapstruct.ap.test.builder.multiple.builder; + +/** + * @author Filip Hrisafov + */ +public class Case { + + private final String builderCreationMethod; + private final String buildMethod; + private String name; + + public Case() { + this.builderCreationMethod = null; + this.buildMethod = "constructor"; + } + + public Case(Builder builder, String buildMethod) { + this.builderCreationMethod = builder.builderCreationMethod; + this.buildMethod = buildMethod; + this.name = builder.name; + } + + public String getBuilderCreationMethod() { + return builderCreationMethod; + } + + public String getBuildMethod() { + return buildMethod; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public static Builder wrongBuilder() { + return new Builder( "wrongBuilder" ); + } + + public static Builder builder() { + return new Builder( "builder" ); + } + + public static class Builder { + + private String name; + private String builderCreationMethod; + + protected Builder(String builderCreationMethod) { + this.builderCreationMethod = builderCreationMethod; + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Case create() { + return new Case( this, "create" ); + } + } +}