#1479 Add support for Builders with multiple build methods#1498
Conversation
|
@chris922 You also had a look at the issue, if you have time you can also review this PR. Thanks 😄 |
|
@filiphr I just had a quick look:
|
|
Thanks a lot @chris922.
|
sjaakd
left a comment
There was a problem hiding this comment.
I'm not sure whether this is the way to go.. Can't we provide a buildMethodName in the @Mapper and/or @MappingConfig? and have a decent default e.g. build? MapStruct should alleviate the user from too complicated stuff like SPI's ..
| * 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 `TooManyBuildMethodsException` would be thrown. |
There was a problem hiding this comment.
During compilation? Or is this part of the SPI? Normally we don't refer to exceptions in MapStruct. We just refer to the warnings / messages that occur during the compilation.. Perhaps its time to write a separate SPI guide..
| 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_TOO_MANY_BUILDER_CREATION_METHODS( "Too many builder creation methods for \"%s\". Found methods: \"%s\". Builder will not be used. Consider implementing a custom BuilderProvider SPI.", Diagnostic.Kind.WARNING ), |
There was a problem hiding this comment.
Too many == More than one.. What is 'too many'? 1, 2, 5?
| GENERAL_NO_SUITABLE_CONSTRUCTOR( "%s does not have an accessible parameterless constructor." ), | ||
|
|
||
| BUILDER_TOO_MANY_BUILDER_CREATION_METHODS( "Too many builder creation methods for \"%s\". Found methods: \"%s\". Builder will not be used. Consider implementing a custom BuilderProvider SPI.", Diagnostic.Kind.WARNING ), | ||
| BUILDER_TOO_MANY_BUILD_METHODS( "Too many build methods in \"%s\" for \"%s\". Found methods: \"%s\". Builder will not be used. Consider implementing a custom BuilderProvider SPI.", Diagnostic.Kind.WARNING ), |
| * | ||
| * @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 TooManyBuildMethodsException if the Builder type has more than one method that can build the type |
There was a problem hiding this comment.
MoreThanOneBuildMethodException..
I totally agree with this point. SPI should only be required for really complex stuff and not for basic stuff like deciding which Having a new parameter for |
|
@chris922 and @sjaakd I hear you 😄. I will try to find a better approach for this. I agree that we need to make it easier for the users. The only issue is that the As for the build method, I would propose that we introduce some new annotation like For example instead of doing Btw, I am travelling until Sunday so won't be able to work on this |
|
@sjaakd and @chris922 I've thought a bit about this one. I will add a new However, I am still struggling when there is more than one builder creation method. We need the type of the builder really early on in the compilation and we can use a method to get it as it would be too late. What is your suggestion about this? Is it OK to leave this for a custom SPI? |
Is there a way to automatically detect what kind of builder pattern is present given the builder technology used? I think we did this recently as well, right? |
By builder technology do you mean Immutables, FreeBuilder etc, or something else? The current mechanism works in the following way:
What we can actually work with is when there is more that one builder creation method to not use builders. However, if there is a single builder creation method and multiple build methods than we would assume the user has defined a |
or even protobuf (believe it uses a builder pattern as well, right?
So.. If i understand you correctly, in case of conflicts (multiple qualifying methods), you leave it to the user to point out the right "factory" for creation, and the right "finisher" so the user could delegate to the generated code, right? Could we also use the qualifier approach? Because we normally use that to resolve conflicts somehow..? |
Factory for creation would be to implement an SPI as we need the builder really early, even before we work with the methods. The finisher can be done with an annotation.
Not really, most of the time the builder code is generated, so there is no way for users to put the qualifiers. At least I don't see it, If you have some idea please do share it. Example for the more than one builder creation method: In the case of the builder creator for For the more than one build methods: In this case we can find the type of the builder ( With How does this sound? |
|
Ill check it out this evening..
… Op 3 jun. 2018 om 15:18 heeft Filip Hrisafov ***@***.***> het volgende geschreven:
So.. If i understand you correctly, in case of conflicts (multiple qualifying methods), you leave it to the user to point out the right "factory" for creation, and the right "finisher" so the user could delegate to the generated code, right?
Factory for creation would be to implement an SPI as we need the builder really early, even before we work with the methods. The finisher can be done with an annotation.
Could we also use the qualifier approach? Because we normally use that to resolve conflicts somehow..?
Not really, most of the time the builder code is generated, so there is no way for users to put the qualifiers. At least I don't see it, If you have some idea please do share it.
Example for the more than one builder creation method:
public Person {
public static PersonBuilder builder() {}
public static SecondPersonBuilder secondBuilder() {}
}
In the case of the builder creator for Person without an SPI we cannot figure out what is the type of the builder that we need, PersonBuilder or SecondPersonBuilder. If such case occurs the user will need to implement an SPI.
For the more than one build methods:
public class Vehicle {
public static VehicleBuilder builder(){}
public static class VehicleBuilder {
public Vehicle create() {}
public Vehicle otherCreate() {}
}
}
In this case we can find the type of the builder (VehicleBuilder). However, the user will need to define something like:
@Mapper
public interface MyMapper {
@BuilderFinisher
default Vehicle build(VehicleBuilder builder) {
return builder.otherCreate();
}
}
With @BuilderFinisher the users would be able to define their own way of invoking the builder.
How does this sound?
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or mute the thread.
|
|
@filiphr .. I understand your approach. However it would still take a considerable amount of work to do this for each method. If you only get 3 or 4 properties to map in a bean, you now need to implement possibly a creater and a finisher method. Do you know if the builders predominantly use a consistent naming convention that we could use? E.g. if its always |
34031f6 to
c15266e
Compare
|
@sjaakd, @chris922 I've changed the approach a bit. I took ideas from you, @sjaakd . There is now I still need to update the Javadoc and the reference documentation, but wanted to get your opinion first. I went with a standalone annotation as that opens the path to meta annotations for 2.0 (perhaps). WDYT? |
sjaakd
left a comment
There was a problem hiding this comment.
@filiphr. Your approach looks fine.
I do have doubts on the new BuilderMapping annotation (see comments), it comes down to:
- it doesn't make sense if the user applies it to for instance
MapMappingandIterableMapping(if you want to do it right, you need to check if that's the case and raise an error). - We abandon the hierarchy (no
MapperConfiglevel), right?
| } | ||
| else { | ||
| BuilderMappingPrism builderMapping = builderMappingPrism( method, ctx ); | ||
| String buildMethodPattern = "build"; |
There was a problem hiding this comment.
constant?.. E.g. DEFAULT_BUILD_METHOD_NAME?
| ElementType.TYPE, | ||
| ElementType.METHOD | ||
| }) | ||
| public @interface BuilderMapping { |
There was a problem hiding this comment.
I think the new meta annotation could be confusing for the user. If we do this, we should probably do it for all in 2.0.
Consider this: Would we 'normally' have made this a property of BeanMapping (we use a hierarchy
from @MapperConfig to @Mapper) ? In other words: does it make sense to use this mapping also on MapMapping, ValueMapping or StreamMapping methods?
Can you apply this BuilderMapping on MappingConfig level as well?
There was a problem hiding this comment.
You are right. It might not make sense to MapMapping. However, it will make sense during the computation of the intermediate methods for the keys / values mappings. In my opinion we need to change the MapperConfig and allow use of meta annotations in 2.0. The mapper config might even not be needed.
There was a problem hiding this comment.
In my opinion we need to change the MapperConfig and allow use of meta annotations in 2.0. The mapper config might even not be needed.
Ok.. I can live with that.. But.. shall we take it in 2.0 then? Now it looks like we mix two strategies.. So in 2.0 do a major overhaul of the way we set configuration/ handle annotations. Have MetaMapping definitions of property mapping as well (in stead of having the need to base class).
There was a problem hiding this comment.
Yes we do that in 2.0. I will apply the changes here.
| } | ||
| for ( ExecutableElement buildMethod : buildMethods ) { | ||
| String methodName = buildMethod.getSimpleName().toString(); | ||
| if ( methodName.matches( buildMethodPattern ) ) { |
There was a problem hiding this comment.
should we also check that the method has no args, has a return type?
There was a problem hiding this comment.
Yes we could. However, currently we are making sure that this can't happen in the DefaultBuilderProvider. We can even make that as part of the contract of the BuilderInfo.
| }) | ||
| public @interface BuilderMapping { | ||
|
|
||
| String builderFinisher() default "build"; |
There was a problem hiding this comment.
I thought the problem was 2 fold: builderFinisher and builderCreator.. where's the latter one?
There was a problem hiding this comment.
The builderCreator one is a more complex one. We need the type of the builder at the time of the creation of the Type itself. As we need the properties really early on, when we are constructing the Target and Source references. That's why that is not hear, and I am having hard time pulling that here.
There was a problem hiding this comment.
ok.. but then leave it open as a new issue / improvement.. From a user perspective, its strange, agreed?
There was a problem hiding this comment.
I'll create an issue for that. yes it might be a bit strange from a user perspective.
| if ( builderInfo == null ) { | ||
| return null; | ||
| } | ||
| ExecutableElement buildMethod = builderInfo.getBuildMethod(); |
There was a problem hiding this comment.
Probably this check belongs to the BuilderFinisherMethodResolver now. As we can check the method return type there. Although this can't happen with the default builder provider as we have this check there.
|
@sjaakd it might actually make sense to apply it on
How should I proceed with this one? |
I would prefer to have a consists meta annotation interface in 2.0 and (a consistent one now) 😄 |
| //If we reach this method this should never happen | ||
| return null; | ||
| } | ||
| else if ( buildMethods.size() == 1 ) { |
There was a problem hiding this comment.
If a @BuilderMapping was defined with an explicit name for the builderFinisher, but only a different method was found, the wrong method will be used. The user would not notice this.
In case he entered a builderFinisher I would expect a warning or even better an error in case the only method found has a different name. The issue is most likely how we should know that he explicitly entered a name, because you defined build as a default name in the annotation. So we will always have a builderFinisher as soon as the @BuilderMapping annotation (or whatever it will be at the end) will be used.
The only way I see to solve this issue is to use an empty string as a default annotation attribute value and in this case use build as a constant value here in the code (the DEFAULT_BUILD_METHOD_NAME).
And switch the if-else ordering.. if a builderFinisher was defined try to use a method with exactly this name, if not possible -> error. If nothing was defined fallback to the way you are doing it right now (size == 1, then methodName equals build).
| 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_MORE_THAN_ONE_BUILD_METHOD_DEFAULT_PATTERN( "More than one build method in \"%s\" for \"%s\". Found methods: \"%s\". Using default \"build\" method pattern. Consider to add @BuilderMapping in order to select the correct build method.", Diagnostic.Kind.ERROR ), |
There was a problem hiding this comment.
This "Using default .. method pattern" sounds confusing for me. Just after reading this message I thought this is an INFO that he found more than one possible build method, but uses at the end the one with the name build. Maybe something like:
More than one build method in \"%s\" for \"%s\". Found methods: \"%s\", but no method is named \"build\". Consider to add @BuilderMapping in order to select the correct build method.
|
|
||
| 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_MORE_THAN_ONE_BUILD_METHOD_DEFAULT_PATTERN( "More than one build method in \"%s\" for \"%s\". Found methods: \"%s\". Using default \"build\" method pattern. Consider to add @BuilderMapping in order to select the correct build method.", Diagnostic.Kind.ERROR ), | ||
| BUILDER_MORE_THAN_ONE_BUILD_METHOD_DEFINED_PATTERN( "More than one build method in \"%s\" for \"%s\". Found methods: \"%s\". Using defined \"%s\" method pattern.", Diagnostic.Kind.ERROR ), |
There was a problem hiding this comment.
Same as above, maybe:
More than one build method in \"%s\" for \"%s\". Found methods: \"%s\", but no method is named \"%s\" as defined in @BuilderMapping#builderFinisher.
c15266e to
d46e7b8
Compare
|
I've finally found the time to go over the changes in this PR 😄. Thank you @chris922 and @sjaakd for the reviews. I've applied both of your comments.
If you have time let me know what you think. |
d46e7b8 to
1800a00
Compare
|
Hey Filip.. Looks more consistent. I'm still a bit struggling whether we want to have annotations within annotations to solve this problem. I know you are considering some kind of meta annotation mechanism for 2.0. However, It would be more usable (and step-up to 2.0) if you could define an annotation as a user (a bit like we do with qualifiers), grouping specific So: @Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
@BuilderMapping(builderFinisher = "create")
public @interface BuilderMappingForFrameworkX { }@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
@BuilderMapping(builderFinisher = "wrongCreate")
public @interface BuilderMappingForFrameworkY { }You should then also be able to use the @Mapper(builder = @BuilderMappingForFrameworkX)
public interface BuilderMappingDefinedMapper {
BuilderMappingDefinedMapper INSTANCE = Mappers.getMapper( BuilderMappingDefinedMapper.class );
Process map(Source source);
@BeanMapping(builder = @BuilderMappingForFrameworkY)
Process wrongMap(Source source);
}If this is possible, we could:
Note: Having only one property in Shall we discuss this in our Tuesday meeting? EDIT:
EDIT II:
|
| import java.lang.annotation.Target; | ||
|
|
||
| /** | ||
| * Annotation that can be used to provide a custom finisher for the builders in case of multiple build methods. |
There was a problem hiding this comment.
Let's not repeat that it's an annotation, that's clear from its definition:
Configuration of builders, e.g. the name of the final build method.
| */ | ||
| @Retention(RetentionPolicy.CLASS) | ||
| @Target({}) | ||
| public @interface BuilderMapping { |
There was a problem hiding this comment.
Should it be marked as @Incubating or are we sure that it won't be altered incompatibly down the road?
There was a problem hiding this comment.
We have @Experimental I think we can use that instead
|
Rename |
* 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`
1800a00 to
2c7e102
Compare
|
I've squashed all commits, did the rename, update the commit message to:
I also updated the documentation. Once the build is done, I'll do the merge |
| /** | ||
| * 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 |
| * 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. | ||
| * <p> | ||
| * If the builder is defined and there is a single method that does not match the name of the finisher than |
There was a problem hiding this comment.
the name of the build method as we don't use finisher anymore?
| * build method, then that method would be used. | ||
| * <p> | ||
| * 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 | ||
| */ |
| 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 |
|
Thanks for the final checks @chris922. I'll commit those directly on master 😄 |
buildand if found use itDefaulting to a method named
buildwill make sure that a correct method is selected for:buildandbuildPartialgetDefaultInstanceForType,buildandbuildPartialFixes #1479
Edit: changed text to match the final commit