Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#1011 updates are not handled correctly for nested target mappings #1046

Merged
merged 2 commits into from Feb 1, 2017

Conversation

sjaakd
Copy link
Contributor

@sjaakd sjaakd commented Jan 21, 2017

This PR makes use of the new forged bean mapping (part of automapping) functionality. The commits need to be squashed but I left it intentionally for the time being. The idea is to make mapping options and in particular the mappings themselves, also be used by a forged method.

Its a bit rough around the edges but I wanted to share the solution for initial review.

As a reminder to myself: the documentation needs to be update to reflect the best approach for update method (nullvaluecheck strategy: always). Furthermore, I need to add some notes to the release, there is some (logical) changed behavior.

@sjaakd sjaakd self-assigned this Jan 21, 2017
@sjaakd sjaakd added the bug label Jan 21, 2017
@sjaakd sjaakd added this to the 1.2.0.Beta1 milestone Jan 21, 2017
@sjaakd sjaakd requested a review from filiphr January 21, 2017 09:34
@sjaakd
Copy link
Contributor Author

sjaakd commented Jan 21, 2017

@filiphr I added you as initial reviewer since it reflects your idea. @agudian , @gunnarmorling : by all means, if you want to review as well go ahead.

@sjaakd
Copy link
Contributor Author

sjaakd commented Jan 22, 2017

One thing I realized myself: the MappingOptions should have and equals and hashcode method at least considering the mappings (for the time being). Need a test case were I test 2 bean mapping methods that have the same type but a different cross section of target mappings.

Forge method should delegate its equals to its include MappingOptions

Copy link
Member

@filiphr filiphr left a comment

Choose a reason for hiding this comment

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

Overall it looks really great.

Some general stylistic comments:

  • I would also add some Javadoc to the different popX methods. They are well explained in the MappingOptions, but it would be nice if we have it per class where it is used.
  • The order of some imports is strange and some formatting issues.

This only works for nested target properties, and not source properties right? I think that it should work for mappers like: org.mapstruct.ap.test.nestedproperties.simple.SimpleMapper, org.mapstruct.ap.test.nestedsourceproperties.ArtistToChartEntry, org.mapstruct.ap.test.nestedsourceproperties.ArtistToChartEntryComposedReverse. Maybe this needs another PR, I don't know?

The reason why I did "Request changes" is because I think that there is a problem when we want to do normal mapping with nested target properties like in org.mapstruct.ap.test.nestedtargetproperties.ChartEntryToArtist#map(ChartEntry chartEntry). In the implementation method chartEntryToStudio(ChartEntry chartEntry, Studio mappingTarget) we have some null checks. I think that those checks there are wrong. Maybe we need to have one type of generated methods for update methods (with @MappingTarget) and one for normal mappings (with a return type).

*/
public Map<PropertyEntry, MappingOptions> popTargetReferences() {

// sort all mappings based on the top level name before popping
Copy link
Member

Choose a reason for hiding this comment

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

Don't you mean group here? Also for the other sort comments.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yep.. I'll change this into group.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

well, its actually also a kind of pop (like pop from a stack). groupByPoppedPropertyEntry good enough?

Copy link
Member

Choose a reason for hiding this comment

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

The name of the method is completely fine. What I meant are the comments sort all mappings based on the top level name before popping. What you mean (I think) is group all mappings by the top level name before popping

public boolean hasNestedTargetReferences() {
boolean found = false;
Iterator<List<Mapping>> mapIterator = mappings.values().iterator();
while ( mapIterator.hasNext() && !found ) {
Copy link
Member

Choose a reason for hiding this comment

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

Why don't you use 2 for iterators like in the other methods?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not so fond of breaking a loop. The found condition breaks the loop. Now it is in the loop check.

Copy link
Member

Choose a reason for hiding this comment

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

Fine by me. I just needed some more time to understand the loops, nothing else 😄

Copy link
Member

Choose a reason for hiding this comment

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

I have to say, I had to read that method for a while as well to understand that it's just a two step a iteration...

public boolean hasNestedTargetReferences() {
    for (List<Mapping> mappings : mappings.values()) {
        for (Mapping mapping : mappings) {
            TargetReference targetReference = mapping.getTargetReference();
            if (targetReference.isValid() && targetReference.getPropertyEntries().size() > 1) {
                return true;
            }
        }
    }
    return false;
}

... we could then much easier transform it into a chain of stream / lambdas when we're full on Java 8 at some point... 😁

return result;
}

private Map<String, List<Mapping>> sortByTargetName( List<Mapping> mappingList ) {
Copy link
Member

Choose a reason for hiding this comment

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

I think it should be groupByTargetName. At least that is what I get from the method implementation 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

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

that's right.. I'll change.

Copy link
Member

Choose a reason for hiding this comment

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

This is still here

return result;
}

public void reInit( Parameter sourceParameter ) {
Copy link
Member

Choose a reason for hiding this comment

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

Maybe a small Javadoc what this exactly means, also for the Mapping#reInit

Copy link
Contributor Author

Choose a reason for hiding this comment

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

we place the Mappings on top of a new method. So we need to associate those with the new source parameter in this method. There can be only one source parameter in generated forged methods... Something along those lines?

Copy link
Member

Choose a reason for hiding this comment

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

Yes something along those lines

Copy link
Member

Choose a reason for hiding this comment

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

Without having read all the code, it sounds like we could also just create fresh instance for the new usage, eg with a properly named factory method? Then we wouldn't have to make sure that we don't use the instance for the previous method after it has been reInit-ed for the next method?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@agudian: it originates form init which happens after the construction of the Mapping as well. Point is: I already have a MappingOption. Just freshly created (but an another spot). I don't want to drag around the mappings themselves (I see a need for having the other mappings as well). So what about plain init?

Copy link
Member

Choose a reason for hiding this comment

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

Sounds good, init wouldn't confuse me here... 🙂

@@ -115,4 +107,39 @@ public String getFullName() {
return Strings.join( Arrays.asList( fullName ), "." );
}

public PropertyEntry pop() {
Copy link
Member

Choose a reason for hiding this comment

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

Maybe a Javadoc to explain that a new PropertyEntry is created and how.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll add..

return errorOccured;
}

private boolean reportErrorOnTargetObject(Mapping mapping) {
Copy link
Member

Choose a reason for hiding this comment

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

This won't show the entire nested path if some of the middle nested properties is abstract class / interface, right? Maybe we need to add a test for this situation 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Lets keep this separated. I'll add an issue.

Copy link
Member

Choose a reason for hiding this comment

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

Agreed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See #1049


ForgedMethod forgedMethod = (ForgedMethod) this.method;
ForgedMethod forgedMethod = (ForgedMethod) this.method;
if ( forgedMethod.isAutoMapping() ) {
Copy link
Member

Choose a reason for hiding this comment

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

Why don't you add this if to the one bellow and have if ( forgedMethod.isAutoMapping() && ( targetProperties.isEmpty() || !unprocessedTargetProperties.isEmpty() ) ) instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok.. I'll change

@@ -37,6 +38,7 @@
@RunWith(AnnotationProcessorTestRunner.class)
@WithClasses(ErroneousIssue1029Mapper.class)
@IssueKey("1029")
@WithSingleCompiler(org.mapstruct.ap.testutil.runner.Compiler.JDK)
Copy link
Member

Choose a reason for hiding this comment

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

What is the problem with the Eclipse compiler? On my computer there was no problem when running for both compilers.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

none.. its a left over of testing.. I'll remove that.. Good point.

@filiphr
Copy link
Member

filiphr commented Jan 22, 2017

I know we mentioned it once, but a lot of the new implementation is begging for Java 8 😄

@sjaakd
Copy link
Contributor Author

sjaakd commented Jan 22, 2017

This only works for nested target properties, and not source properties right? I think that it should work for mappers like: org.mapstruct.ap.test.nestedproperties.simple.SimpleMapper, org.mapstruct.ap.test.nestedsourceproperties.ArtistToChartEntry, org.mapstruct.ap.test.nestedsourceproperties.ArtistToChartEntryComposedReverse. Maybe this needs another PR, I don't know?

you mean the combination: in one mapping nested source / nested target..? I've got no reason to believe it does not work.. I don't touch the sourceReferences. For source properties we already forge methods and that's not not changed. But perhaps I do understand you wrong?

@sjaakd
Copy link
Contributor Author

sjaakd commented Jan 22, 2017

I know we mentioned it once, but a lot of the new implementation is begging for Java 8 😄

I know.. But I know that a lot of companies are more conservative in their java update plan. Its only 5 years ago a rolled out a platform on Java5..

@sjaakd
Copy link
Contributor Author

sjaakd commented Jan 22, 2017

The reason why I did "Request changes" is because I think that there is a problem when we want to do normal mapping with nested target properties like in org.mapstruct.ap.test.nestedtargetproperties.ChartEntryToArtist#map(ChartEntry chartEntry). In the implementation method chartEntryToStudio(ChartEntry chartEntry, Studio mappingTarget) we have some null checks. I think that those checks there are wrong. Maybe we need to have one type of generated methods for update methods (with @MappingTarget) and one for normal mappings (with a return type).

I think they originate from:

@Mapper( nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS )
public abstract class ChartEntryToArtist {
....

That is what I meant with I probably have to update the documentation 😄. I added those to make the composite test case work out of the box (it assumes that the first argument changes are not override by the second argument changes). Makes sense?

@filiphr
Copy link
Member

filiphr commented Jan 22, 2017

you mean the combination: in one mapping nested source / nested target..? I've got no reason to believe it does not work.. I don't touch the sourceReferences. For source properties we already forge methods and that's not not changed. But perhaps I do understand you wrong?

I know that we forge methods, I just thought that we are going to group those methods as well. Now for the TargetReference we group and forge new methods now, and for the SourceReference we don't (For each @Mapping we create a new forged method). This can go in a new issue.

That is what I meant with I probably have to update the documentation 😄. I added those to make the composite test case work out of the box (it assumes that the first argument changes are not override by the second argument changes). Makes sense?

Yes it makes sense if all the fields are null when instantiating the object. If for some reason someone sets a default value for field, then we won't take the changes from the first argument into consideration (this is not an update, but a normal mapping). I think that this is a big backwards incompatibility (users will have to change the NullValueCheckStrategy in order composite mapping to work). Maybe we need to create new methods for each composite parameter (for each different mapping group)?

@sjaakd
Copy link
Contributor Author

sjaakd commented Jan 22, 2017

I know that we forge methods, I just thought that we are going to group those methods as well. Now for the TargetReference we group and forge new methods now, and for the SourceReference we don't (For each @mapping we create a new forged method). This can go in a new issue.

Ok.. So what you mean is that there are too many source mappings generated than is strictly needed. I think this is indeed a new issue. But we should be careful here. Introduction of grouping here could lead to a can of worms (think about dependsOn stuff) etc. So I'm actually quite happy with the current implementation.

@filiphr
Copy link
Member

filiphr commented Jan 22, 2017

Ok.. So what you mean is that there are too many source mappings generated than is strictly needed. I think this is indeed a new issue. But we should be careful here. Introduction of grouping here could lead to a can of worms (think about dependsOn stuff) etc. So I'm actually quite happy with the current implementation.

Indeed it is a new issue. I created #1048 for this reason.

@sjaakd
Copy link
Contributor Author

sjaakd commented Jan 22, 2017

Yes it makes sense if all the fields are null when instantiating the object. If for some reason someone sets a default value for field, then we won't take the changes from the first argument into consideration (this is not an update, but a normal mapping). I think that this is a big backwards incompatibility (users will have to change the NullValueCheckStrategy in order composite mapping to work). Maybe we need to create new methods for each composite parameter (for each different mapping group)?

Hmm. I agreed it an issue for the migration notes. But I do think we did not think on this composite scenario properly in the past: NestedTargetPropertiesTest#shouldMapNestedComposedTarget behaved as an update method between arg1 and arg2. Which is exactly what you pointed out above: this is not an update, but a normal mapping.

Having said this: which arg wins in case of a normal mapping? I believe according we cannot assume ordering in the source parameters, right? Moreover: if we do use the default NullValueCheckStrategy.ON_IMPLICIT_CONVERSION null is just a value, for a String, just as "" or "blah". My point being: we have to fall back on the user, and the dependsOn functionality to point out which one goes first in a composite case.

So, perhaps I should add a testcase to check whether the possibility to control the order is indeed kept in-tact by my scribbling and adding a dependsOn testcase. And of course: add documentation on how to do a merge between two objects and migration notes. One thing though: I think that mapping nested properties was experimental. And I think we should keep it that way until we gain more experience. WDYT? Would that alleviate your concerns?

@filiphr
Copy link
Member

filiphr commented Jan 22, 2017

I think that the test NestedTargetPropertiesTest#shouldMapNestedComposedTarget behaves as a merge method for 2 different chart entries.

Having said this: which arg wins in case of a normal mapping? I believe according we cannot assume ordering in the source parameters, right? Moreover: if we do use the default NullValueCheckStrategy.ON_IMPLICIT_CONVERSION null is just a value, for a String, just as "" or "blah". My point being: we have to fall back on the user, and the dependsOn functionality to point out which one goes first in a composite case.

The thing is that there is no question which arg wins in case of a normal mapping?. We were not assuming anything. The test NestedTargetPropertiesTest#shouldMapNestedComposedTarget uses the ChartEntryToArtist mapper which has the following defined:

@Mapper( nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS )
public abstract class ChartEntryToArtist {
    @Mappings({
        @Mapping(target = "type", ignore = true),
        @Mapping(target = "name", source = "chartEntry2.chartName"),
        @Mapping(target = "song.title", source = "chartEntry1.songTitle" ),
        @Mapping(target = "song.artist.name", source = "chartEntry1.artistName" ),
        @Mapping(target = "song.artist.label.studio.name", source = "chartEntry1.recordedAt"),
        @Mapping(target = "song.artist.label.studio.city", source = "chartEntry1.city" ),
        @Mapping(target = "song.positions", source = "chartEntry2.position" )
    })
    public abstract Chart map(ChartEntry chartEntry1, ChartEntry chartEntry2);

//There are other mappings as well
}

As you can see there is clear distinction which property is taken from which argument in the @Mapping. And in my opinion when we use NullValueCheckStrategy.ON_IMPLICIT_CONVERSION we shouldn't be doing a null check on the studio.getName(). I don't think that this PR should lead to use having to use NullValueCheckStrategy.ALWAYS for composite mapping to work.

Adding a testcase with dependsOn sounds good.

One thing though: I think that mapping nested properties was experimental. And I think we should keep it that way until we gain more experience. WDYT? Would that alleviate your concerns?

I couldn't find anything about nested properties being experimental in our documentation. If they are I agree with you we can keep them, until we gain more experience. However, I don't think that we need to use NullValueCheckStrategy.ALWAYS for doing a composite merge. I think that the problem is that we generate only one ForgedMethod, instead of 3 for the different mappings.

public Map<Parameter, MappingOptions> splitOnSourceParameterType() {

Set<Parameter> parameters = new HashSet<Parameter>();
Map<Type, List<Mapping>> mappingsKeyedByParameterType = new HashMap<Type, List<Mapping>>();
Copy link
Member

@filiphr filiphr Jan 22, 2017

Choose a reason for hiding this comment

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

If we do this, then if 2 parameters have the same type like in the ChartEntryToArtist then we are going to return the same MappingOptions for chartEntry1 and chartEntry2. Is this expected?

@sjaakd
Copy link
Contributor Author

sjaakd commented Jan 22, 2017

The thing is that there is no question which arg wins in case of a normal mapping?. We were not assuming anything. The test NestedTargetPropertiesTest#shouldMapNestedComposedTarget uses the ChartEntryToArtist mapper which has the following defined:

Of course.. you are right.. I need to rethink. Perhaps we should generate also methods having more source parameters (as long as that is relevant). WDYT?

@sjaakd
Copy link
Contributor Author

sjaakd commented Jan 22, 2017

couldn't find anything about nested properties being experimental in our documentation.

I thought I noticed it somewhere..

@filiphr
Copy link
Member

filiphr commented Jan 22, 2017

Of course.. you are right.. I need to rethink. Perhaps we should generate also methods having more source parameters (as long as that is relevant). WDYT?

That's an interesting idea.

However, I don't think that that is the problem. Have a look at PropertyMapping#L634 and MappingBuilderContext#L184, the ctx.getNamesOfMappingsToGenerate() returns only the names of the methods that we need to generate. Maybe we need to store the names of the forged methods as well. Then we would generate 3 new trees of forged methods.

Additionally see my comment on MappingOptions#splitOnSourceParameterType. I think that here we need to take the parameter name and type into consideration.

Edit: The line for the property mapping was wrong.

@sjaakd
Copy link
Contributor Author

sjaakd commented Jan 22, 2017

Additionally see my comment on MappingOptions#splitOnSourceParameterType. I think that here we need to take the parameter name and type into consideration.

I initially did so (had MappingOptions#splitOnSourceParameterType only). But than an incomplete mapping method was generated and than reused for the second parameter. But if 2 methods would be forged that would solve the problem. Bringing me back to the fact that I need to implement equals for MappingOptions. The ctx.getMappingsToGenerate().contains( mappingMethod ) in PropertyMapping should be able to distinguish also based on MappingOptions..

@filiphr
Copy link
Member

filiphr commented Jan 22, 2017

But if 2 methods would be forged that would solve the problem. Bringing me back to the fact that I need to implement equals for MappingOptions. The ctx.getMappingsToGenerate().contains( mappingMethod ) in PropertyMapping should be able to distinguish also based on MappingOptions

The ForgedMethod(s) are not added to the ctx.getMappingsToGenerate(). Maybe we need to add them as well?

@sjaakd
Copy link
Contributor Author

sjaakd commented Jan 22, 2017

The ForgedMethod(s) are not added to the ctx.getMappingsToGenerate(). Maybe we need to add them as well?

If we want to achieve re-use (and avoid conflicts) we need to. Just as implementing the equals and hashcode operations to make a proper distinction. And they are for forged Map, Iterable and Stream mappings, right? I'll limit myself to my case..

@filiphr
Copy link
Member

filiphr commented Jan 22, 2017

If we want to achieve re-use (and avoid conflicts) we need to. Just as implementing the equals and hashcode operations to make a proper distinction. And they are for forged Map, Iterable and Stream mappings, right? I'll limit myself to my case..

I just had some idea 😄. Maybe the name ForgedMethod is wrong. I think that we should look at the ForgedMethods as ForgedMappings, or maybe even rename (just to have a better understanding of what they mean) them. Then:

  1. We make sure that the MappingOptions are taken into consideration for the equals and hashCode methods
  2. In the MapperCreationProcessor we make sure that when we collect the ForgedMethod(s) we make sure that their names are changed

@sjaakd if you want I can create a new issue for this and do it in a separate PR, so we keep things more contained 😄

@filiphr
Copy link
Member

filiphr commented Jan 26, 2017

I think we need an integral strategy for this. An target="prop.*" ignore = "true" will have the benefit dat stuff is harmonized. And it will offer a solution for switching of automapping selectively.And it looks easy to realize.

Using wildcards like this is something interesting. I only think that @gunnarmorling is not a big fan of wildcards 😄

@sjaakd
Copy link
Contributor Author

sjaakd commented Jan 26, 2017

Using wildcards like this is something interesting. I only think that @gunnarmorling is not a big fan of wildcards 😄

I know.. But we need to away to stop auto-mapping, but also name-based mapping. I now write entire mappers with unmappedTargetPolicy = ReportingPolicy.IGNORE while often only want to do this for certain bean-mapping-methods: map certain properties and ignore the rest.

@sjaakd
Copy link
Contributor Author

sjaakd commented Jan 26, 2017

I didn't have time for it, yet - can it wait for the weekend? I certainly won't be able to read through it tonight... 😕

No problem at all..

@agudian
Copy link
Member

agudian commented Jan 27, 2017

Okay, I think I get the gist of what the first and second commit are about to do and I'd only have some minor comments there (I'm going over it later).

The last commit "exclude other mappings (name and parameter base) from nesting" puzzles me a bit more... 😉. The test yields the same result with and without the code changes in the processor of that commit: in both cases, the test is green, but it shows that something is not right here, as you've written in the comment.

Without knowing too much about MapStruct, I would probably expect that fish.name would be mapped as it is and not silently ignored. It would be mapped if it the type/kind config was on top-level, so I don't see how we could explain that it wouldn't be mapped on a lower level.

More explicitly, I would expect that the following two declarations would create the same results:

    @Mapping(target = "fish.kind", source = "fish.type")
    FishTankDto toFishTankDto(FishTank source);

is basically just a short-cut to this:

    FishTankDto toFishTankDto(FishTank source);

    @Mapping(target = "kind", source = "type")
    FishDto toFishDto(Fish fish);

In the first example, don't we pass on the mapping options from toFishTankDto to the (forged) toFishDto without the fish. property path prefixes? Then it should behave exactly as the second one.

Without thinking too much about it, I would guess that it's what a user would expect as well. We usually require users to only describe the exceptions and let the rest be handled implicitly. With the auto-mapping out, I actually expect that users will use the path expressions in @Mapping annotations on their topmost method...

The behaviour of the previous combination of nested source and nested target is a bit odd and I don't think that users really have a need for exactly that....Because what if I want to stop mapping at a certain branch? I'd just ignore that one, e.g. target = "path.to.waterPlant", ignore = true.

That's mostly the reason why I finally worked on the nested-property code completion for source and target in the Eclipse plugin... (mapstruct/mapstruct-eclipse#40, in case someone want's to review or give it a try).

Copy link
Member

@agudian agudian left a comment

Choose a reason for hiding this comment

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

Some more minor comments on the code...

public boolean hasNestedTargetReferences() {
boolean found = false;
Iterator<List<Mapping>> mapIterator = mappings.values().iterator();
while ( mapIterator.hasNext() && !found ) {
Copy link
Member

Choose a reason for hiding this comment

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

I have to say, I had to read that method for a while as well to understand that it's just a two step a iteration...

public boolean hasNestedTargetReferences() {
    for (List<Mapping> mappings : mappings.values()) {
        for (Mapping mapping : mappings) {
            TargetReference targetReference = mapping.getTargetReference();
            if (targetReference.isValid() && targetReference.getPropertyEntries().size() > 1) {
                return true;
            }
        }
    }
    return false;
}

... we could then much easier transform it into a chain of stream / lambdas when we're full on Java 8 at some point... 😁

return nestedDependsOn;
}


Copy link
Member

Choose a reason for hiding this comment

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

double empty lines

@@ -115,4 +107,39 @@ public String getFullName() {
return Strings.join( Arrays.asList( fullName ), "." );
}

public PropertyEntry pop() {
if ( fullName.length > 1 ) {
String[] newFullName = Arrays.copyOfRange( fullName, 1, fullName.length );
Copy link
Member

Choose a reason for hiding this comment

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

Why didn't we make fullName a List<String>? We could have used List.subList(...) here instead then, avoiding the array-copy,

}

@Test
@WithClasses({FishTankMapper.class})
Copy link
Member

Choose a reason for hiding this comment

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

You know my CPU isn't that powerful, so moving all mappers to the top of the class would really help... For debugging purposes, I usually remove those mappers that I'm not interested in from the WithClasses... 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I know.. But I'm not doing it to tease you 😄. The reason is indeed debugging (and my sloppiness: I forget to move stuff back).. But I can of course do that.

@sjaakd
Copy link
Contributor Author

sjaakd commented Jan 28, 2017

@agudian thanks for your comments. I'll fix those (won't change the array at this point in time though, but we can do this later.

The behaviour of the previous combination of nested source and nested target is a bit odd and I don't think that users really have a need for exactly that....Because what if I want to stop mapping at a certain branch? I'd just ignore that one, e.g. target = "path.to.waterPlant", ignore = true.

I can agree with that. But stil, we introduce a backward incompatibility. And we should be cautious with those. At lest the (co)issuer of #1011 had an issue here, hence my latest commit.

My biggest concern is that we end up in ignoring a lot of nested target properties. That's why I propose to have a shorthand:

@Mapper
public interface FishTankMapper {

    FishTankMapper INSTANCE = Mappers.getMapper( FishTankMapper.class );

    @Mapping(target = "fish.kind", source = "fish.type")
    @Mapping(target = "fish.*", ignore=true) // will ignore all others
    FishTankDto map( FishTank source  );
}

So, what I can do is remove the latest commit from this PR, extend on it by the above wild card idea and move it to a new PR for stopping auto-mapping. Just as we discussed in #1001.

Would this PR than be fit to be merged? We can continue the discussion on stopping automapping in the new PR.

That's mostly the reason why I finally worked on the nested-property code completion for source and target in the Eclipse plugin... (mapstruct/mapstruct-eclipse#40, in case someone want's to review or give it a try).

Ok.. I'm not so familiar with Eclipse here, hence I did not look at it, I can give it a try. But Not everyone is relying on Eclipse for solving the problem above. So I'm still in favor of some kind of shorthand.

@sjaakd
Copy link
Contributor Author

sjaakd commented Jan 28, 2017

So, what I can do is remove the latest commit from this PR, extend on it by the above wild card idea and move it to a new PR for stopping auto-mapping. Just as we discussed in #1001.

And add 'normal' (non wildcard) ignore on nesting levels to this PR (reuse the fishtanks test for that).

@agudian
Copy link
Member

agudian commented Jan 28, 2017

Agreed, let's leave out the last part and discuss the potential need for a wildcard ignore or anything like that in a separate issue.

@sjaakd
Copy link
Contributor Author

sjaakd commented Jan 28, 2017

@agudian .. Solved most of your comments (not the array though, I've got a stash for that, but it still fails and I need to investigate why).

I myself did not see the problem correctly with was found in #1011 by @snowe2010 after giving him a solution only containing the first 2 commits.

The problem is in the code that is generated:

    protected void fishTankToFishDto(FishTank fishTank, FishDto mappingTarget) {
        if ( fishTank == null ) {
            return;
        }

        String type = fishTankFishType( fishTank );
        if ( type != null ) {
            mappingTarget.setKind( type );
        }
    }

the current solution stops mapping a property name in FishTank and not in Fish to FishDto.

The composite test shows that there are 2 mapping methods generated for each source property. @filiphr and myself discussed this above. However, auto-mapping currently is not applied to already handled targets on root nesting level.

I reckon we need to address that as well. It should also be handled in another separate mapping method ignoring the properties already mapped. I need to think about this some more. I'll open up a separate issue / PR for that one as well. This works as it used to, solving the issue described in #1011.

@sjaakd
Copy link
Contributor Author

sjaakd commented Jan 28, 2017

The reproducer in 23fd42a actually does its job now.

@gunnarmorling
Copy link
Member

Wow, what a debate :) Where are we with it, any further discussion needed? I guess something for tonight :)

@sjaakd
Copy link
Contributor Author

sjaakd commented Jan 31, 2017

Wow, what a debate :) Where are we with it, any further discussion needed? I guess something for tonight :)

Not particularly on this one.. But this one got me thinking about how to handle ignore's in automapping. I send 2 mails on that one (off line). But I think we can also put that in a separate issue.

The only think stopping me from merging is @agudian 's verdict on the commit: 23fd42a and 3f84707 and the merge of #1053 . I've got a rebase locally standing by.

Copy link
Member

@agudian agudian left a comment

Choose a reason for hiding this comment

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

I'm happy with it, given that we'll further discuss how we really want this FishTank example to be handled... because the current generated result smells fishy - sorry, had to write that. 🐟

@filiphr
Copy link
Member

filiphr commented Jan 31, 2017

I can also add a fixture for this so we can immediately pick up when the generated source changes 😄

@sjaakd
Copy link
Contributor Author

sjaakd commented Jan 31, 2017

I'm happy with it, given that we'll further discuss how we really want this FishTank example to be handled... because the current generated result smells fishy - sorry, had to write that.

guess I was watching a very intelligent program 😄 with an half eye called "Tanked" on the Discovery Channel

@sjaakd
Copy link
Contributor Author

sjaakd commented Feb 1, 2017

@gunnarmorling : same here.. If you're ok I'll continue and merge.

@sjaakd
Copy link
Contributor Author

sjaakd commented Feb 1, 2017

I'm happy with it, given that we'll further discuss how we really want this FishTank example to be handled... because the current generated result smells fishy - sorry, had to write that. 🐟

@agudian : Let me know were you think the problems are. So we can take that as input for the discussion.

@sjaakd
Copy link
Contributor Author

sjaakd commented Feb 1, 2017

I'll go ahead and merge.

@sjaakd sjaakd merged commit 1406c0b into mapstruct:master Feb 1, 2017
@gunnarmorling
Copy link
Member

Let me know were you think the problems are. So we can take that as input for the discussion.

Indeed something looks weird there. Why does it generate a fishTankToFishDto() method? Two questions I see really:

  • why does it try to map fish tank to fish DTO (the elements are on different levels in the tree)?
  • why is this an update method instead of a regular mapping method with a return value?

@gunnarmorling
Copy link
Member

I've opened #1060 for this. It's definitely something we'll need to look into.

@sjaakd
Copy link
Contributor Author

sjaakd commented Feb 1, 2017

why does it try to map fish tank to fish DTO (the elements are on different levels in the tree)

Nested mappings were never limited to a symmetrical tree. It works exactly as before.

why is this an update method instead of a regular mapping method with a return value?

Checkout: NestedTargetPropertiesTest#shouldMapNestedComposedTarget. I need update methods because the mapping for the first source parameter would influence the mapping for the next source parameter.


The generated code works exactly as before (even before the auto-mapping). I only added the FishTankMapper test case to show a problem in my initial solution, which was shown by tests of @snowe2010. See issue #1011. I only make use of the auto-mapping do simplify the code and to solve the problem in #1011.

For more info, see #1057 for more info.

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

Successfully merging this pull request may close these issues.

None yet

4 participants