Skip to content

PR for issues #131, #2438, #366#2512

Merged
filiphr merged 31 commits into
mapstruct:masterfrom
rigd-loxia:feature/PR-131
Oct 19, 2021
Merged

PR for issues #131, #2438, #366#2512
filiphr merged 31 commits into
mapstruct:masterfrom
rigd-loxia:feature/PR-131

Conversation

@Zegveld

@Zegveld Zegveld commented Jul 11, 2021

Copy link
Copy Markdown
Contributor

#131 #2438 #366 : Added support for SubClassMapping annotations. This will allow for hierarchy mappings.

if a parent mapping method is annotated with @SubClassMapping it will now generate an instanceof check inside the parent mapping and generate the subclass mappings if they are not manually defined. See issues #131 #2438 and #366 for more information.

mapstruct#131, mapstruct#2438, mapstruct#366: Added support for SubClassMapping annotations. This will allow for hierarchy mappings.
@Zegveld Zegveld marked this pull request as draft July 11, 2021 13:19
@Zegveld Zegveld marked this pull request as ready for review July 12, 2021 06:31
@filiphr

filiphr commented Aug 14, 2021

Copy link
Copy Markdown
Member

Wow, thanks a lot for this PR @Zegveld. I haven't gotten around in reviewing this (picking the low hanging fruits first). However, we would look into this to make the 1.5.0.Beta2 release

@filiphr filiphr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just finished with the review on this one. Really nice work @Zegveld.

I've left some comments inline and I have some more general comments here.

I see that you are using the _bugs folder. We use this usually for actual bugs, not new features. I would suggest instead to use subclassmapping as top package and put your test there. You can then use different sub packages to show different use cases. In addition to that I am looking at the tests for #131 and #366. Correct me if I am wrong, but the only difference is the fact that in #131 Vehicle is not abstract, right?

Let's try to merge these things together and use the same source types and different target types. Perhaps you can also rename the source to VehicleEntity or somehow else, in order to avoid using full qualified names in the tests to make it more readable.

Can we also add some fixture tests, to see how the generated code looks like? As you know we strive to generate code which looks human readable 😄 .

Can we also add some erroneous tests to test the non happy paths for the new messages.

I am really excited in getting this in the upcoming 1.5.


Something that could be good, but maybe we need to do it in a separate PR is to validate that the SubClassMapping make sense. For example if the first sub class mapping does not hide the second one.

e.g.

interface Vehicle { }

interface Car extends Vehicle { }

class SUV implements Car { }

class Sedan implements Car { }

And then if you defined sub class mapping like:

@SubClassMapping(source = Vehicle.class, target = ...)
@SubClassMapping(source = Car.class, target = ...)

Those subclass mappings will generate code which will never be executed.

e.g.

if (source instanceof Vehicle) {
    // ...
} else if (source instanceof Car) {
    // ...
}

After having written all of this, I think it makes sense to tackle it in a different PR. Let's do it like that, once we have this merged.

Comment thread processor/src/test/java/org/mapstruct/ap/test/bugs/_366/Issue366Test.java Outdated
Comment thread processor/src/test/java/org/mapstruct/ap/test/bugs/_366/Issue366Mapper.java Outdated
Comment thread processor/src/test/java/org/mapstruct/ap/test/bugs/_2438/Issue2438Test.java Outdated
Comment thread processor/src/main/java/org/mapstruct/ap/internal/model/ForgedMethod.java Outdated
Comment on lines +304 to +311
for ( SourceMethod method : methods ) {
for ( SubClassMappingOptions subClassMapping : method.getOptions().getSubClassMappings() ) {
Type type = typeFactory.getType( subClassMapping.getTargetClass() );
extraImports.add( type );
type = typeFactory.getType( subClassMapping.getSourceClass() );
extraImports.add( type );
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think that this is needed. The SubClassMapping already implements the getImportTypes, so you only need to call that for each subClassMapping in the BeanMappingMethod#getImportTypes

Comment on lines +1762 to +1765
public List<SubClassMapping> constructSubClassMappings(List<Parameter> parameters) {
subClasses.forEach( subClass -> subClass.updateWithParameters( parameters ) );
return subClasses;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We strive for the models to be immutable if possible. This code is invoked from the BeanMappingMethod.ftl. Can we do this in a different place and only get the subClassMappings here?

ENUMMAPPING_ILLEGAL_TRANSFORMATION( "Illegal transformation for '%s' EnumTransformationStrategy. Error: '%s'." ),

SUBCLASSMAPPING_ILLEGAL_SUBCLASS("Class '%s' is not a subclass of '%s'."),
SUBCLASSMAPPING_METHOD_SIGNATURE_NOT_SUPPORTED("SubClassMapping annotation can not be used on Suppliers and Runnables."),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think that this error message is clear. Having void return type means that the user is using update mappings, most likely we do not support that for now, so I would change the message to something like that.

By the way, you can do update mappings with a return type e.g. Target update(@MappingTarget Target target, Source source)

@Zegveld Zegveld Sep 5, 2021

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So something like: @SubClassMapping do not support update mappings.?
Open for suggestions to make this message better, because the one above doesn't feel quite right either.

Edit: Probably still needs more work but something for the second review round.

@filiphr

filiphr commented Aug 29, 2021

Copy link
Copy Markdown
Member

By the way @Zegveld in #131 (comment) you are saying that you are not happy with the implementation route you took? Can you explain why? I personally thought that it is quite neat 😄 .

@Zegveld

Zegveld commented Aug 29, 2021

Copy link
Copy Markdown
Contributor Author

By the way @Zegveld in #131 (comment) you are saying that you are not happy with the implementation route you took? Can you explain why? I personally thought that it is quite neat .

Well that was before I made a lot more changes where in I made it quite neat ;)
Fixed it in this commit: rigd-loxia@98c3d8e

@Zegveld

Zegveld commented Aug 29, 2021

Copy link
Copy Markdown
Contributor Author

Only done the bits so far where I've added a thumbs up.
Still got a few review items to go, but that will have to wait till when I have time again later this week.

Comment thread core/src/main/java/org/mapstruct/SubClassMapping.java Outdated
Comment thread documentation/src/main/asciidoc/chapter-10-advanced-mapping-options.asciidoc Outdated
@filiphr

filiphr commented Aug 30, 2021

Copy link
Copy Markdown
Member

Only done the bits so far where I've added a thumbs up.
Still got a few review items to go, but that will have to wait till when I have time again later this week.

No problem @Zegveld, take as much time as you need. Please ping me once the PR is fully ready for the next iteration of review.

@Zegveld

Zegveld commented Sep 5, 2021

Copy link
Copy Markdown
Contributor Author

@filiphr , Think I've done all the adjustments. So that part is ready for a next iteration of review. Hopefully I haven't missed anything.

@Zegveld

Zegveld commented Sep 11, 2021

Copy link
Copy Markdown
Contributor Author

Just read the first review again and noticed the bit about:
Can we also add some fixture tests, to see how the generated code looks like? As you know we strive to generate code which looks human readable smile .
Where can I find an example of fixture tests so that I can add them for the subclass mappings as well? Edit: Think I've found examples at other tests. Took me a while to understand where to look for the classes with which the result is compared.

Also added a warning for Something that could be good, but maybe we need to do it in a separate PR is to validate that the SubClassMapping make sense. For example if the first sub class mapping does not hide the second one.

@Zegveld

Zegveld commented Sep 12, 2021

Copy link
Copy Markdown
Contributor Author

I've added fixture tests. Made these separate so that the main variants are covered and not impacted by changes within the general tests.
@filiphr, hope to hear from you soon for the 2 remaining issues, and also what you think of my solution for the annotation handling order one.

@sjaakd sjaakd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First of all: what a tremendous feat of work. Thanks!. When discussing this with @filiphr I was amazed by the effort done on this.

I did not check the complete implementation. I recon that @filiphr did that already. So I focussed on the API extensions and the implications there.

Now: we do like to go more into meta-annotations in 2.0. So, isolating functionality (sourceClass and targetClass) into a new annotation is a good idea. I've one consideration here though.

MapStruct recognises a few major classes of mappings: mapping bean-to-bean, mapping iterable-to-iterable, map-to-map and value-to-value. BeanMapping obviously the most important one. Also, each comes with its own annotation for doing configuration: @BeanMapping, @IterableMapping, @MapMapping, @ValueMapping.

There is check functionality that guarantees that a MapMapping is not accidentally applied to @BeanMapping. In fact, we do have a small problem with the just released map-to-bean-mapping, which under some conditions gets mixed up with bean mapping.

Having said that: would it not be better to make @SubclassMapping a property of @BeanMapping? Although more verbose, it would keep functionality together and make sure @SubclassMapping is not accidentally applied to another form of mapping.

In 2.0 we could give those mappings a more " independent life", perhaps making even use of the fact that they are part of a @BeanMapping. But lets do that step-by-step.

Next, I was thinking how this functionality would interplay with the new MapToBean functionality. Do we have an idea 'bout that?

anyway @Zegveld : what a nice piece of work on an issue which has a long history. Really looking forward to work with you on more issues in the future.

@filiphr

filiphr commented Sep 19, 2021

Copy link
Copy Markdown
Member

Thanks for the review @sjaakd. However, I have to say that I do not quite agree with moving @SubclassMapping to the BeanMapping. I believe that @SubclassMapping is on the same level as @ValueMapping and @Mapping.

Adding it to the @BeanMapping to only remove it later on doesn't bring much value I think. @Zegveld also did a really nice refactoring which makes supporting meta annotations work out of the box already.

and also what you think of my solution for the annotation handling order one.

@Zegveld you mean the refactoring you did, right? I found it really great, already said that in #2512 (comment) 😉 .


@Zegveld I've been a bit busy, so I haven't gotten around in finishing the review with your last changes. I haven't forgotten about this one yet.

@Zegveld

Zegveld commented Sep 19, 2021

Copy link
Copy Markdown
Contributor Author

First of all: what a tremendous feat of work. Thanks!. When discussing this with @filiphr I was amazed by the effort done on this.

I did not check the complete implementation. I recon that @filiphr did that already. So I focussed on the API extensions and the implications there.

Now: we do like to go more into meta-annotations in 2.0. So, isolating functionality (sourceClass and targetClass) into a new annotation is a good idea. I've one consideration here though.

MapStruct recognises a few major classes of mappings: mapping bean-to-bean, mapping iterable-to-iterable, map-to-map and value-to-value. BeanMapping obviously the most important one. Also, each comes with its own annotation for doing configuration: @BeanMapping, @IterableMapping, @MapMapping, @ValueMapping.

There is check functionality that guarantees that a MapMapping is not accidentally applied to @BeanMapping. In fact, we do have a small problem with the just released map-to-bean-mapping, which under some conditions gets mixed up with bean mapping.

Having said that: would it not be better to make @SubclassMapping a property of @BeanMapping? Although more verbose, it would keep functionality together and make sure @SubclassMapping is not accidentally applied to another form of mapping.

I didn't think about this initially, but it would be possible to do submappings for iterables or maps as well. This would be useful if the mapping is reused multiple times, but the implementation collection varies. And you could even support implementations of the Collection framework that you do not know exist. (e.g. project specific ones)
For @ValueMappings I'm not that certain, if this is only enums then no. But if this is also static fields than the same thing could apply. Because static fields can refer to subclassed instances of the source.
It would require some more work to support the @IterableMapping, @MapMapping and @ValueMapping annotations with subclasses but I do believe that would be possible. Not certain as to how often it would be used if at all though, because the above is pure theoretical.

In 2.0 we could give those mappings a more " independent life", perhaps making even use of the fact that they are part of a @BeanMapping. But lets do that step-by-step.

Next, I was thinking how this functionality would interplay with the new MapToBean functionality. Do we have an idea 'bout that?

I think that could turn out to be an adjusted variant of the @SubclassMapping as is, but instead of supplying a source class you would supply a sourceCondition. If this sourceCondition would result in a true value a more specific MapToBean method would be called. Something for in another PR but here is the gist of it:

@SubclassMapping( sourceCondition="java(input.containsKey('subBeanSpecificField'))", target=SubBean.class )
Bean mapToBean(Map<String, String> input);

@Mapping(/*specific mapping for this condition*/
SubBean mapToSubBean(Map<String, String> input);

which could result in something like:

Bean mapToBean(Map<String, String> input) {
    if (input.containsKey('subBeanSpecificField')) {
        return mapToSubBean(input);
    }
    // default mapToBean behaviour
}

SubBean mapToSubBean(Map<String, String> input) {
    // mapToSubBean behaviour
}

anyway @Zegveld : what a nice piece of work on an issue which has a long history. Really looking forward to work with you on more issues in the future.

Likewise.

and also what you think of my solution for the annotation handling order one.

@Zegveld you mean the refactoring you did, right? I found it really great, already said that in #2512 (comment) wink .

@filiphr, no I did a refactor which changes several Set's into specifically LinkedHashSet's. Was talking about that one.

@Zegveld I've been a bit busy, so I haven't gotten around in finishing the review with your last changes. I haven't forgotten about this one yet.

No problem I've got plenty of waiting time. Currently implementing just a lot of instanceof checks manually when I'm using it instead of generating them. :)

@sjaakd

sjaakd commented Sep 19, 2021

Copy link
Copy Markdown
Contributor

I didn't think about this initially, but it would be possible to do submappings for iterables or maps as well. This would be useful if the mapping is reused multiple times, but the implementation collection varies. And you could even support implementations of the Collection framework that you do not know exist. (e.g. project specific ones)

You have a point here. Did not consider that. I think @filiphr and I were discussing the change of the default implementation (don't remember if it was Map or List) a while ago. And this would open up changing it back or having your own preferred default implementation.

For @ValueMappings I'm not that certain, if this is only enums then no. But if this is also static fields than the same thing could apply. Because static fields can refer to subclassed instances of the source.

That would be really a niche feature.

I was discussing mis-use of annotations with @filiphr just earlier and he pointed out that we don't have (presumably) checks for @Mapping either. So we're a bit half-hearted with checks here anyway.

Finally, applying it to the new map-to-bean would indeed qualify as a follow up PR.

So, as far as I'm concerned: go ahead.

@filiphr

filiphr commented Oct 16, 2021

Copy link
Copy Markdown
Member

I checked out the PR locally today and did some changes in it. I've pushed the changes to https://github.com/filiphr/mapstruct/tree/feature/PR-131 (I couldn't push it to your branch for some reason, most likely you have not allowed maintainers to push changes to it). Can you please have a look at them and let me know whether you agree with them @Zegveld? I tried to do them atomically in order to show my thought process for some of them, everything will be merged into one commit in the end.

@filiphr, no I did a refactor which changes several Set's into specifically LinkedHashSet's. Was talking about that one.

I saw that one, and I didn't really like it. I do not like having collection implementation types as fields of classes or parameters. I reverted some of that change in a separate PR.

Some other more notable changes I did are:

  • We do not support all update methods, not just the ones that are void. Otherwise it is too confusing
  • Small Refactoring in the RepeatableMappingAnnotations to reduce the complexity a bit
  • Remove obsolete changes in MapperCreationProcessor

Some final things to address before merging:

  • I am not sure that the throwing of the IllegalArgumentException is a good approach. The reason for that is that if you remove the SubclassMapping(s) we will have a compile error, otherwise we throw a runtime error. Can we keep this behaviour consistent? In a follow up PR we can add support for throwing the exception if the user wants it by explicitly asking for it via @BeanMapping and in another PR to support sealed classes, i.e. we can know whether the SubclassMapping(s) have all classes for the source or not.

I think that this would be the last change I request, together with my changes the PR would be ready for merging and doing a beta release shortly after that. It would be a great addition to the 1.5 release 😄


Side note: Since your email address is private, can you please reach out to me somehow? You could use Twitter or my email address (it is in my commits). I'd like to discuss something with you.

* getImportType() is already implemented in the parent
* getThrownTypes() is needed, since it is possible that an already existing method is used and that one throws an exception
@Zegveld

Zegveld commented Oct 16, 2021

Copy link
Copy Markdown
Contributor Author

I've looked through the changes and they are indeed an improvement as to what I did.
The bit about the Set we could discuss, but that shouldn't hinder this PR.

Concerning the IllegalStateException:
So if I understand it correctly, since this is generated code it is better to throw an UnsupportedSubclassError that extends Error (Edit: or perhaps a NotImplementedError?) instead of an IllegalStateException?
Did some reading up on the definition of Error vs RuntimeException. I've never used error before but then again this is my first time interacting at compiler-level.
I think that would indeed be an improvement here.

side note: I've sent you an e-mail.

@filiphr

filiphr commented Oct 16, 2021

Copy link
Copy Markdown
Member

Concerning the IllegalStateException:
So if I understand it correctly, since this is generated code it is better to throw an UnsupportedSubclassError that extends Error (Edit: or perhaps a NotImplementedError?) instead of an IllegalStateException?
Did some reading up on the definition of Error vs RuntimeException. I've never used error before but then again this is my first time interacting at compiler-level.
I think that would indeed be an improvement here.

That's not what I had in mind. All of that is runtime behaviour and not compile time behaviour. From a runtime point of view the runtime the IllegalArgumentException is OK. However, from a point of view of MapStruct, the way our users use it, I do not think that we need to have such behaviour on the runtime. We are more towards compile time safety and telling the user that they have made a mistake.

What I had in mind is that we do not do some of the changes you did in the BeanMappingMethod such as the hasSubclassMappingsAndIsEitherAbstractOrCanBeConstructed and we keep everything as it was. That way the user will get a compile error saying something like

  • Fruit does not have an accessible constructor.
  • The return type Fruit is an abstract class or interface. Provide a non abstract / non interface result type or a factory method.

Once we have support for sealed types we can avoid that part if all of them have been covered.

In order to see what I mean you can remove the @SubclassMapping for the AbstractSuperClassTest, SubclassFixtureTest and SubclassMappingTest and see what happens. I think that we need to keep that behaviour consistent. We could offer a possibility to throw the exception that is now thrown through something on the BeanMapping.

side note: I've sent you an e-mail.

Thanks for reaching out to me.

@Zegveld

Zegveld commented Oct 16, 2021

Copy link
Copy Markdown
Contributor Author

I did take that route under consideration, but the use case that I had which caused me to write this enhancement was one where I never have an implementation for general usage. In this use case the classes are generated from two xsd's and I need to transfer information between the two models.

The best solution I can think of usage side without having the exception would be to manually create an implementation/extension of the base class which then would throw an IllegalStateException in it's constructor because it should not be created. That one would then be the default implementation that mapstruct falls back to. This does not feel that good as a solution for this problem.

What if we add another annotation @SubclassMappingStrategy in which you can configure whether you get an exception or compile error. Hmmm maybe with a field/property named .... can't think of a good name for the property atm. But that property would then influence the behavior. Just like how the other MappingStrategy annotations do.

@filiphr

filiphr commented Oct 16, 2021

Copy link
Copy Markdown
Member

What if we add another annotation @SubclassMappingStrategy in which you can configure whether you get an exception or compile error. Hmmm maybe with a field/property named .... can't think of a good name for the property atm. But that property would then influence the behavior. Just like how the other MappingStrategy annotations do.

That's what I head in mind when I was saying about the BeanMapping, but I couldn't come up with a good name 😄 . Your naming is really good. I think that for now we can expose subclassMappingStrategy or subclassExhaustiveStrategy and there we can have COMPILE_ERROR (as default) and RUNTIME_EXCEPTION as enums. This can be exposed to @MapperConfig, @Mapper and @BeanMapping, same as the other strategies. The exhaustive comes from the Java Sealed Classes JEP 409. I think that this would also solve your use case, by you configuring your mappers to have SubclassExhaustiveStrategy.RUNTIME_EXCEPTION.

We have ideas for the future (2.0) to have a different approach for our APIs and have a single @SubclassMappingStrategy as an annotation instead, but let's not do that now.

…rt from the default error handling behavior.
@Zegveld

Zegveld commented Oct 16, 2021

Copy link
Copy Markdown
Contributor Author

@filiphr , I've introduced the SubclassExhaustiveStrategy and did some more renaming/refactoring. Also updated the documentation to have it mentioned.
Hope you have time to review the new changes.

@filiphr filiphr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks great. One final comment in the javadoc of the SubclassExhaustiveStrategy and we are done 😄

Comment thread core/src/main/java/org/mapstruct/SubclassExhaustiveStrategy.java Outdated
@filiphr filiphr merged commit 5df6b7a into mapstruct:master Oct 19, 2021
@filiphr

filiphr commented Oct 19, 2021

Copy link
Copy Markdown
Member

And done one of our oldest feature request has been implemented. Thanks a lot for the huge effort you put in this @Zegveld.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants