-
-
Notifications
You must be signed in to change notification settings - Fork 916
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
#674 Support for native Java 8 Optional mapping #3183
base: main
Are you sure you want to change the base?
Conversation
@@ -33,7 +33,7 @@ public class JavaFileAssert extends FileAssert { | |||
|
|||
private static final String FIRST_LINE_LICENSE_REGEX = ".*Copyright MapStruct Authors.*"; | |||
private static final String GENERATED_DATE_REGEX = "\\s+date = " + | |||
"\"\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\+\\d{4}\","; | |||
"\"\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{4}\","; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixing up some timezone bias :P
private Assignment forgeOptionalMapping(Type sourceType, Type targetType, SourceRHS source) { | ||
|
||
targetType = targetType.withoutBounds(); | ||
ForgedMethod methodRef = prepareForgedMethod( sourceType, targetType, source, "?" ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using ?
here to represent an Optional
type
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I need to see how this will look like in the error. I think that it would be something like customer?.address?.value
if customer and address are optional. I think that looks nice. Perhaps an example as a comment here to make it easy to understand 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have to admit, I'm not certain how to trigger an error that will print out this forge history. Would you have any guidance on the simplest way to do so? Once I can see what it looks like, I can add the comment.
<#-- TODO: this should return the default value for whatever the returnType is --> | ||
return null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure how to return the "default" type for when the resultType
is not Optional
(e.g. when it is a collection). Preferably mapping Optional<Collection> == null
=> Empty Collection
(though I do recognize that no one should be passing a null
to an Optional
, but we should prob still handle this case regardless.
Also Optional.<Collection>empty()
should probably also map to Empty Collection
when mapNullToDefault
is true, but that logic too is missing below.
} | ||
|
||
<#if sourceParameter.type.optionalType> | ||
return ${sourceParameter.name}.map( ${loopVariableName} -> <@includeModel object=elementAssignment/> )<#if !returnType.optionalType>.orElse( null )</#if>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Permission to rename loopVariableName
to containedElementName
?
0e3a5f4
to
0d2f599
Compare
Hi @ro0sterjam
to this
It would be useful for cases there Optional is a container for additional state (e.g. if an attribute was present at all in the patch request. so it maps like Optional.present -> value, Optional.empty -> null set, null -> undefined/not set in a request, this is supported OOB in jackson) |
Honestly that seems reasonable to me and was originally going to support this case if not for this comment here #674 (comment) That said, I do agree and think we should allow the user to decide what they want, similar with collections. If we did do this though, should we conflate it with
Which I would argue is less common of a desired outcome than always returning a non null for an Incidentally, similar to With that, I might propose that we introduce instead a config such as: Edit: |
@ro0sterjam just out of curiosity: is this going to change how we currently handle BTW: one of the things that I consider up for change in a 2.0 version is the way we've done all these |
One use case for Optional mapping is for Patch request body:
When we want to use for input to call an external RestApi:
It's possible to obtain this? |
We've been talking about these strategies for a while. I do agree with you that it might be a good idea to reiterate on them for 2.0. @sjaakd regarding the affect on current code. Won't the rule of MapStruct of using something which already exists be applied as well? I haven't reviewed the PR, so I am not sure to what you are referring to. @lysz210 I am really against in MapStruct treating |
@filiphr the main reason is the a representation of Json UNDEFINED fields that in Java it's not possible without Optional or something similar (https://www.baeldung.com/jackson-optional) to implement a patch in RestApi. |
@lysz210 I know about Json UNDEFINED fields. You can use a
A better solution for this is something like the |
I could be mistaken, but from my understanding/experience with MapStruct, custom mappings should supersede any default mapping behaviour (which this is). But I can verify this claim myself. Would you be able to provide an example of how you are currently mapping |
Re: I think it's worth looking at how Jackson approaches The difference in behaviour are, if
While if
I do agree with you @filiphr that setting Maybe a simple Where
And
The only difference being mapping from a Anyway, just my 2 cents on the matter. |
There are other Wrapper class and probably some custom classes too. |
And |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I finally managed to find the time to review this in detail. I really like the direction where this is going. Thanks for your work on it @ro0sterjam.
As a first step for how we treat Optional
I would say that we always assume that they are not null
. I would wait for initial feedback before trying to allow null
for them. I've heard of the Jackson strategies. I think we can look into this as a step 2 of this functionality. Perhaps users will need to opt in into telling us to do null checks on optionals, or they would need to provide their own mapping methods for this.
Having said that, it means that we don't really need the changes in the CommonMacros.ftl
. We can achieve the check by adapting PropertyMapping.PropertyMappingBuilder#getSourcePresenceCheckerRef
. We can provide our own custom PresenceCheck
for the optionals.
Apart from that, I would also not yet do a null check for the source parameter in the OptionalMappingMethod.ftl
if the source parameter is an optional. This also makes the question you had about handling the mapNullToDefault
obsolete, since we can never be in a situation like that. Although, this might mean that instead of doing optional.map
we need to do optional.orElse( null )
and then do the mapping. Otherwise, empty optional won't be affected by the RETURN_DEFAULT
strategy and I think it should.
private Assignment forgeOptionalMapping(Type sourceType, Type targetType, SourceRHS source) { | ||
|
||
targetType = targetType.withoutBounds(); | ||
ForgedMethod methodRef = prepareForgedMethod( sourceType, targetType, source, "?" ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I need to see how this will look like in the error. I think that it would be something like customer?.address?.value
if customer and address are optional. I think that looks nice. Perhaps an example as a comment here to make it easy to understand 😄
Hey @filiphr thanks for the suggestions. I'm trying to incorporate them into my changes, but hitting a bit of a road block in trying to comprehend what From what I can tell, it is first giving priority to any custom presence check provided from the But then after that, I'm not entirely following what the following snippet is doing?
Update: I think this is the, Finally we got the block:
From what I'm understanding, this is helping us build a nested presence checker. Which makes sense to me, but why is it only utilized when I am going about an approach that utilizes a new Any guidance (or really just clarification on what the code snippets above are doing) would be greatly appreciated! |
I checked out your branch a bit to test this out @ro0sterjam and I stumbled around the same thing. Yes the use of For the null check on the source properties, I am working on some refactoring to support #2610, and I believe that we would be able to benefit from it in this PR as well. |
@ro0sterjam I finally managed to do the refactoring that I promised. Was done in PR #3348 and it is now on main. Can you perhaps rebase on top of main and see if it fits better for you? |
Will rebase and take a look in the upcoming days, thanks! |
Is there anything blocking this PR from merging? Can we help on some additional tasks, like docs? |
Hey @lostiniceland sorry, work and life got the better of me and I stepped away from this project for a while. I'll pick this back up sometime this week. There is still some code clean up in the manner that @filiphr suggested. I would also like to support the following:
|
The flattened-feature would be really nice. Take your time. |
I'm taking a step back and going to clarify desired behaviour first, via the following tests: Specifically, some contentious behaviour that I want to finalize before continuing development.
|
0a51de3
to
290023d
Compare
@filiphr re: moving the I think this depends on the question: when do we want to perform an Three options and my thoughts:
|
290023d
to
56cf074
Compare
Pushed some new functionality:
Achieved this by introduction of a new model
Supported by use of special Other changes and outstanding items:
Removed this functionality; I believe this will be too radical a change from current behaviour, and this is already achievable with a custom
Still unsure how we want to approach this; From the comments in this thread I can see that some would prefer not to have e.g.
In this case, we only set That all said, the remaining "inner" mapping functions will always return One remaining caveat is when mapping from |
@@ -680,6 +690,15 @@ public ReadAccessor getReadAccessor(String propertyName, boolean allowedMapToBea | |||
.orElse( null ); | |||
return new MapValueAccessor( getMethod, typeParameters.get( 1 ).getTypeMirror(), propertyName ); | |||
} | |||
else if ( propertyName.equals( "?" ) && isOptionalType() ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I might consider just throwing a error here if we encounter a ?
but it is not an Optional
type.
public boolean hasAccessibleDefaultConstructor() { | ||
if ( hasAccessibleDefaultConstructor == null ) { | ||
hasAccessibleDefaultConstructor = false; | ||
List<ExecutableElement> constructors = ElementFilter.constructorsIn( typeElement.getEnclosedElements() ); | ||
for ( ExecutableElement constructor : constructors ) { | ||
if ( constructor.isDefault() && !constructor.getModifiers().contains( Modifier.PRIVATE ) ) { | ||
hasAccessibleDefaultConstructor = true; | ||
break; | ||
} | ||
} | ||
} | ||
return hasAccessibleDefaultConstructor; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is related to an existing strange behaviour with CommonMacros#initTargetObject
mapstruct/processor/src/main/resources/org/mapstruct/ap/internal/model/macro/CommonMacros.ftl
Line 173 in c9b551a
new <@includeModel object=ext.targetType/>() |
In that code, it is blindly calling new X()
even if X
does not have a default constructor. This is even called out in NullValuePropertyMappingStrategy, but in this case since this is a nested object, we cannot work around it with a defaultValue()
.
Thus this method was added so that null
can be returned when a "default value" cannot be constructed.
I'm considering just removing this logic and keeping it simple (i.e. let the user fix this issue by writing a custom mapper or create a default constructor).
@@ -21,7 +21,11 @@ | |||
<#if !entry.presenceChecker?? > | |||
<#if !entry.type.primitive> | |||
if ( ${entry.name} == null ) { | |||
return ${returnType.null}; | |||
<#if returnType.isOptionalType()> | |||
return Optional.empty(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should probably use InitDefaultValue
here instead of hardcoding Optional.empty()
, but this ModelElement is missing the required factoryMethod
.
Thanks for your work on this @ro0sterjam. I've been a bit busy. I'll try to carve out some time to look into this in detail. |
We're still enthusiastically waiting for this to make the cut, or NOT?... could you please give an update on what's the verdict? |
Unfortunately life has gotten in the way. I think there has been a lot of conversation here that is leading us in the right direction, but I am not able to actively work on this right now. So if anyone wants to pick this up, please be my guest. I can try to come back to it in about a month's time but I can't make any promises. |
First draft at native mapping support for Java 8 Optional types. I intend to clean up some logic, especially around the template once provided some preliminary feedback.
Models
Optional
as a unique case ofContainerMappingMethod
.Features (WIP):
Optional<TypeA>
->Optional<TypeB>
, (B)Optional<TypeA>
->TypeB
, and (C)TypeA
->Optional<TypeB>
)@BeforeMapping
and@AfterMapping
[ ] ImplementOptional.isPresent()
check in expected pre-mappingnull
checksRETURN_DEFAULT
configured (e.g. collection types)[ ] Implement defaultOptional.empty()
whenNullValueCheckStrategy.ALWAYS
setOptional.empty()
whenNullValuePropertyMappingStrategy.SET_TO_DEFAULT
setOptional
by mapping/flat-mapping all the way down (e.g.source = "optional1.optional2.value", target = "flattenedValue"
)