Skip to content

#3678 Prevent duplicate @BeforeMapping and @AfterMapping calls on classes that use the Builder pattern.#3680

Closed
thunderhook wants to merge 4 commits intomainfrom
3678
Closed

#3678 Prevent duplicate @BeforeMapping and @AfterMapping calls on classes that use the Builder pattern.#3680
thunderhook wants to merge 4 commits intomainfrom
3678

Conversation

@thunderhook
Copy link
Contributor

Fixes #3678

@@ -43,13 +48,15 @@ public void lifecycleMethodsShouldBeInvoked() {
assertThat( context.getInvokedMethods() )
.contains(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@filiphr
At first I tried to validate the fix by using containsExactly(...). But it soon became too complex and hard to understand due to the nested nature of this test.

So I added an additional fixture test that covers this.

Do you think this should be in a separate Issue3678Mapper or is it ok to test it here?

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 removed the fixture, added the before/afterMapping only with source for the sake of completeness here.

But i provided a separate test that explicitly checks if before and after mapping methods are not called more than once.

Copy link
Member

Choose a reason for hiding this comment

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

If we have a dedicated test do we even need changes in this particular test here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not necessariliy. It was however easier to debug the internals of the method resolution due to multiple lifecycle callback methods. I will remove it after implementing the fix.

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.

I'm not sure the fix is correct.

How will this behave for something like:

@Mapper
public interface CustomMapper {

    Target map(SourceA sourceA, SourceB sourceB);

    @BeforeMapping
    default void beforeWithA(SourceA sourceA) {
        // ...
    }


    @BeforeMapping
    default void beforeWithB(SourceB sourceB) {
        // ...
    }

}

import org.mapstruct.Mapper;
import org.mapstruct.ReportingPolicy;

@Mapper(unmappedSourcePolicy = ReportingPolicy.ERROR)
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 not needed for our tests as it'll anyways fails if there is a compiler warning

@@ -43,13 +48,15 @@ public void lifecycleMethodsShouldBeInvoked() {
assertThat( context.getInvokedMethods() )
.contains(
Copy link
Member

Choose a reason for hiding this comment

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

If we have a dedicated test do we even need changes in this particular test here?

Comment on lines 30 to 31
.containsExactly( "beforeMapping", "afterMapping" )
.doesNotHaveDuplicates();
Copy link
Member

Choose a reason for hiding this comment

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

doesNotHaveDuplicates is obsolete because we are using containsExactly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I just wanted to express the intent of this test more clearly. We can of course remove this.

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 containsExactly expresses the intent clearly. It says I'm expecting exactly this and nothing more. If there are duplicates it would clearly fail

@filiphr
Copy link
Member

filiphr commented Aug 24, 2024

Looking at the issue I feel like the fix should be happening on a different level, perhaps when getting the lifecycle methods. Most likely the selection criteria needs to be changed

@thunderhook
Copy link
Contributor Author

I'm not sure the fix is correct.

How will this behave for something like:

@Mapper
public interface CustomMapper {

    Target map(SourceA sourceA, SourceB sourceB);

    @BeforeMapping
    default void beforeWithA(SourceA sourceA) {
        // ...
    }


    @BeforeMapping
    default void beforeWithB(SourceB sourceB) {
        // ...
    }

}

Good catch. I will have a look at that again.

Comment on lines -390 to -392
if ( !super.equals( obj ) ) {
return false;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This evaluated against Object#equals and therefore was always false. This might be a relic from legacy inheritance structures or never worked as intended.

Comment on lines +400 to +402
if ( !Objects.equals( name, other.name ) ) {
return false;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not 100% sure if the name (and already been there: declaringMapper + providingParameter) is enough to differ between multiple method references. All unit tests are green.

@thunderhook
Copy link
Contributor Author

So, i tried to adapt the method retrieval process. It became confusing very quickly and hard to differ between callback lifecycle methods that belong to the builder object and the finalized object.

The initial idea to remove the already invoked methods came from here:

So, I had an idea how to improve this. Why not remove the methods via removeAll(...)? I had to generate equals/hashCode but now this looks more clean. There was a bug in the equals method anyway, I've fixed that (see the other comment)

@filiphr WDYT? Is this good enough?

Problems that arised during the attempt rewriting the selectors and criteria:

  • Should there be a different SelectionCriteria.Type.LIFECYCLE_CALLBACK_WHAT_SO_EVER?
  • The chaining from BeanMappingMethod > LifecycleMethodResolver > SelectionContext was somewhat cumbersome
  • Should it be a blacklist like collect everything except the methods from Set<...>
    • If so, what model should be used for that blacklist? MethodReference or SelectedMethod?


@Mapping( target = "name", source = "sourceA.name")
@Mapping( target = "description", source = "sourceB.description")
abstract Target mapTwoSources(SourceA sourceA, SourceB sourceB);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added a specific test with two sources to be sure.

@filiphr
Copy link
Member

filiphr commented Aug 25, 2024

Thanks @thunderhook. I'll need some time to think about this. I'm not too fond of the removals. I would prefer if we can select the right types when we need them. The SelectionContext already has mappingTargetType and returnType, so if we can somehow support multiple target types here we would be able to collect all in one go

@thunderhook
Copy link
Contributor Author

Thanks @thunderhook. I'll need some time to think about this. I'm not too fond of the removals. I would prefer if we can select the right types when we need them. The SelectionContext already has mappingTargetType and returnType, so if we can somehow support multiple target types here we would be able to collect all in one go

Don't spend time on this yet, I think I get what you mean. I'll revisit this and hopefully come up with the solution you suggested. If there are any problems we can further discuss this here. 👍

@thunderhook
Copy link
Contributor Author

@filiphr Short heads up: Unfortunately, I won't be able to do this any time soon. What do you think about merging the fix with this variant and creating a refactoring issue? Or is it too dangerous for you that the PR could have side effects?

@filiphr
Copy link
Member

filiphr commented Sep 13, 2024

That's fine @thunderhook. I'll have a go at it. I'm not sure what kind of side effects this might have and I would rather spend the time on finding the right solution then rush into something

@filiphr
Copy link
Member

filiphr commented Sep 15, 2024

@thunderhook I found a different solution for this. Have a look at #3716. I'll close this PR in favour of that one

@filiphr filiphr closed this Sep 15, 2024
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.

@AfterMapping methods are called twice when target uses Lombok @Builder annotation

2 participants