Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork984
#3303 Mapping from Map to Map and specify source and target keys of type String#3367
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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
base:main
Are you sure you want to change the base?
Uh oh!
There was an error while loading.Please reload this page.
Conversation
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.
Really nice PR@thunderhook. Thanks for your work on this.
I've left some comments in the PR review, still need to play with it to see the actual generated code.
Here are some comments for your notes:
The variable containing key mappings is not built safe like in ...
I've left a review comment for this. If we do it like I suggest there, I think that we can use theSupportingField#getSafeField
Only
@Mapping
annotations with setsource
andtarget
are considered, the rest is ignored, likeexpression
orignore
(I felt that it is not worth to throw an error in that case)
If we are going to start something that we didn't support before then in my opinion we should be as strict as possible. This means that it is fine to only supportsource
andtarget
. However, if someone sets something else then we should have a compile error.
Alternatively we can also go with a new annotation for this since for thissource
is mandatory, but nottarget
. I do see people asking for things like we have inBeanMapping#ignoreByDefault
and maybe expanding more on how value for a particular key should be mapped using thequalifedByName
and stuff like this. Having said all of this I think it would be better for us if we go with a new annotation e.g.@MapKeyMapping
(open to other name suggestions as well)
I am not saying that we should add all this customizations in this PR, but I would like to anticipate things and make sure that we are implementing this right. We do have#3293 where something else around map mapping is being requested.
This PR tackles only mapping of maps with keys of type string (
Map<String, ?>
). Any other type does not feel like a valid use-case (well,CharSequence
eventually?)
That is completely OK. However, see my comment for the validations. We also need to add tests for mappings fromMap<X, Y>
toMap<String, ?>
. We do support convertingX
toString
through some kind of a mapping or if it is some of the out of the box mappings fromX
toString
. Checkorg.mapstruct.ap.test.collection.map.SourceTargetMapper
as an example.
Map<String,Long>map(Map<Long,Long>source);
I found no easy solution to get a type of
Map<String, String>
with theTypeFactory
. If you have a solution for this, please let me know. I could then remove theMapKeyMappingField.ftl
and use a typedField.ftl
You can get the right map by doing something like:
In theTypeFactory
add:
publicTypegetMapType(StringkeyCanonicalName,StringvalueCanonicalName) {TypeElementmapTypeElement =getTypeElement(Map.class.getCanonicalName() );TypeElementkeyTypeElement =getTypeElement(keyCanonicalName );TypeElementvalueTypeElement =getTypeElement(valueCanonicalName );DeclaredTypemapType =typeUtils.getDeclaredType(mapTypeElement,keyTypeElement.asType(),valueTypeElement.asType() );returngetType(mapType ); }
ThegetTypeElement
looks like:
privateTypeElementgetTypeElement(StringcanonicalName) {TypeElementtypeElement =elementUtils.getTypeElement(canonicalName );if (typeElement ==null ) {thrownewAnnotationProcessingException("Couldn't find type " +canonicalName +". Are you missing a dependency on your classpath?" ); }returntypeElement; }
This is extracted fromprivate Type getType(String canonicalName, boolean isLiteral)
@@ -39,6 +41,8 @@ public class MapMappingMethod extends NormalTypeMappingMethod { | |||
private final Assignment valueAssignment; | |||
private final Parameter sourceParameter; | |||
private final PresenceCheck sourceParameterPresenceCheck; | |||
private final MapKeyMappingConstructorFragment mapKeyMappingConstructorFragment; |
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.
We only need this here because it is used in theMapperCreationProcessor
. However, if you look at what we do there we do
Set<Field>supportingFieldSet =newLinkedHashSet<>(mappingContext.getUsedSupportedFields());
which means that we can easily add the supported fields in the mapping context in the builder for theMapMappingMethod
. doing it there would also making it possible to create the fields usingSupportingField#getSafeField
For the constructor fragments. We can add aSet<ConstructorFragment>
and do it in a similar way as we are doing for the supported fields.
* | ||
* @author Oliver Erhart | ||
*/ | ||
public class MapKeyMappingConstructor extends ModelElement implements Constructor { |
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.
Where is this used?
--> | ||
<#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.MapKeyMappingConstructorFragment" --> | ||
this.${field.variableName} = new java.util.HashMap<>(); |
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.
For this you can then use anIterableCreation
in theMapKeyMappingConstructorFragment
. Most likely we'll need a new method inIterableCreation
e.g.
publicstaticIterableCreationcreate(TyperesultType) {returnnewIterableCreation(resultType,null,null ); }
I think that the new line will then be
this.${field.variableName} = <@includeModel object=iterableCreation useSizeIfPossible=false/>;
assertThat( target ).isNotNull() | ||
.hasSize( 2 ) |
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.
isNotNull
andhasSize
are obsolete since we are doingisEqualTo
already
assertThat( target ).isNotNull() | ||
.hasSize( 2 ) | ||
.contains( | ||
entry( "targetKey", "value" ), | ||
entry( "unmappedKey", "otherValue" ) | ||
); |
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.
we can removeisNotNull
andhasSize
if we usecontainsOnly
instead ofcontains
final GeneratedSource generatedSource = new GeneratedSource(); | ||
@ProcessorTest | ||
public void shouldContainTwoKeyMappingMapsInConstructor() { |
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 would prefer if we have a fixture for this and we test the contents. Makes it a bit easier to see what gets generated as well
CustomNumberMapper.class, | ||
Source.class, | ||
Target.class, | ||
ImportedType.class |
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.
Why are these needed? I guess it is a copy paste thing. Let's remove it if that is the case
Thank you for your detailed and thorough review.
Agreed with a new annotation (didn't think of the I guess this should also be pre JDK-8 compatible? So there will be
Yeah. I will start with the "basic" key mapping feature via Thank you for the detailled comments (especially about the type(s)), I will incorporate them as I refactor the code. |
The need of Side note, can you please reach out to me on my email? You can find it in any of my commits. |
@filiphr I made good progress but then I confused myself with the conversion of
Scenario 1 Mapping Long to String without custom mapping methodLet's say that company 1 and 2 need to be switched. With the original implementation that would've looked like... @MapKeyMapping(target ="1L",source ="2L" )@MapKeyMapping(target ="2L",source ="1L" )Map<String,Long>mapLongKeyMapToStringKeyMap(Map<Long,Long>source);// GENERATED CODEprivatefinalMap<String,String>mapLongKeyMapToStringKeyMapKeyMappings;publicMapKeyMappingCommonConversionMapperImpl() {this.mapLongKeyMapToStringKeyMapKeyMappings =newLinkedHashMap<String,String>();this.mapLongKeyMapToStringKeyMapKeyMappings.put("1L","2L" );this.mapLongKeyMapToStringKeyMapKeyMappings.put("2L","1L" ); }publicMap<String,Long>mapLongKeyMapToStringKeyMap(Map<Long,Long>source) {Map<String,Long>map =newLinkedHashMap<String,Long>();for (java.util.Map.Entry<Long,Long>entry :source.entrySet() ) {Stringkey =String.valueOf(entry.getKey() );// generates "1" instead of "1L"Longvalue =entry.getValue();map.put(this.mapLongKeyMapToStringKeyMapKeyMappings.getOrDefault(key,key ),value );// "1" != "1L" }returnmap; } Ifthere is no custom mapping method from Long to String, Scenario 2: Mapping Long to String with custom mappingLet's say we have a custom mapping method (which is not qualified, since Stringmap(Longvalue) {return"Company" +value;} How should map key mappings be defined?
// Option 1@MapKeyMapping(target ="Company1",source ="2L" )@MapKeyMapping(target ="Company2",source ="1L" )Map<String,Long>option1(Map<Long,Long>source);// Option 2@MapKeyMapping(target ="Company1",source ="Company2" )@MapKeyMapping(target ="Company2",source ="Company1" )Map<String,Long>option2(Map<Long,Long>source);// Option 3@MapKeyMapping(target ="1L",source ="2L" )@MapKeyMapping(target ="2L",source ="1L" )Map<String,Long>option3(Map<Long,Long>source);// GENERATED CODEprivatefinalMap<Long,String>option1KeyMappings;privatefinalMap<String,String>option2KeyMappings;privatefinalMap<Long,Long>option3KeyMappings;publicMapKeyMappingCommonConversionMapperImpl() {this.option1KeyMappings =newLinkedHashMap<Long,String>();this.option1KeyMappings.put( (long)1L,"Company2" );this.option1KeyMappings.put( (long)2L,"Company1" );this.option2KeyMappings =newLinkedHashMap<Long,String>();this.option2KeyMappings.put("Company1","Company2" );this.option2KeyMappings.put("Company2","Company1" );this.option3KeyMappings =newLinkedHashMap<Long,Long>();this.option3KeyMappings.put( (long)1L, (long)2L );this.option3KeyMappings.put( (long)2L, (long)1L ); } Option 1Using a diffrent type between source and target needs a different access inside the mapping method: // GENERATED CODE@OverridepublicMap<String,Long>option1(Map<Long,Long>source) {if (source ==null ) {returnnull; }Map<String,Long>map =newLinkedHashMap<String,Long>(Math.max( (int) (source.size() /.75f ) +1,16 ) );for (java.util.Map.Entry<Long,Long>entry :source.entrySet() ) {Stringkey =String.valueOf(entry.getKey() );Longvalue =entry.getValue();LongsourceMappingKey =entry.getKey();map.put(this.option1KeyMappings.getOrDefault(sourceMappingKey,key ),value ); }returnmap; } Implementation-wise I am certain that we should go with option 1. @MapKeyMapping(target ="Company1",source ="NOT_A_LONG")// cannot generate mapperMap<String,Long>mapLongKeyMapToStringKeyMap(Map<Long,Long>source); Cons: seems unintuitive to define different types Remember the problem from Scenario 1 with Option 2I think the second option is most understandable from a users perspective. Or is it? I am not sure. However this can lead to wrong (and missed) mappings. @MapKeyMapping(target ="Company1",source ="CompanyThatNeverHits")Map<String,Long>mapLongKeyMapToStringKeyMap(Map<Long,Long>source); This is just not mapped and the user doesn't get informed. Option 3I don't think that option 3 is really an option. Just using the source key type is hard to understand, right? Scenario 3 Map to (or even from) an enumI reuse the code to get the expression of source/target like it is handled with the Let's say we have a map with month and employees count, and all employees from @MapKeyMapping(target ="FEBRUARY",source ="MARCH" )@MapKeyMapping(target ="MARCH",source ="FEBRUARY" )Map<String,Long>map(Map<java.util.Month,Long>source); This works. It does not matter if the key mapping map is backed with a String or Month map. Let's add a custom enum conversion method: StringmapMonth(Monthvalue) {returnvalue.name().substring(0,3);} How about the mapping? Must it be changed to the following? This example should help to chose for an option from above // option 1@MapKeyMapping(target ="FEB",source ="MARCH" )@MapKeyMapping(target ="MAR",source ="FEBRUARY" )// option 2@MapKeyMapping(target ="FEB",source ="MAR" )@MapKeyMapping(target ="MAR",source ="FEB" )// option 3@MapKeyMapping(target ="FEBRUARY",source ="MARCH" )@MapKeyMapping(target ="MARCH",source ="FEBRUARY" )Map<String,Long>map(Map<java.util.Month,Long>source); Scenario 4 (bonus) Mapping from Long to Long (or: same source key type as target key type)I can see no reason why this should not work. And this will work when the key mapping map supports different types than String. This is currently implemented and working as expected. @MapKeyMapping(taraget ="1L",source ="2L")@MapKeyMapping(taraget ="2L",source ="1L")Map<Long,Long>mapLongsToLongs(Map<Long,Long>source); ConclusionDuring writing this wall of text (took me like an hour...) I could think about it more and analyze it. I want to know your opinion about the possible options, and what would you choose? Thanks in advance. Take your time to answer it. I would love to push my current code, but it is really mixed up with those three options. |
Uh oh!
There was an error while loading.Please reload this page.
Tackling#3303 map mapping with source and target keys.
Things to note:
org.mapstruct.ap.internal.model.SupportingField#getSafeField
. This would need a bigger change in theMapperCreationProcessor
. However, a collision with a field named likemethod.getName() + "KeyMappings"
is very unlikely.@Mapping
annotations with setsource
andtarget
are considered, the rest is ignored, likeexpression
orignore
(I felt that it is not worth to throw an error in that case)Map<String, ?>
). Any other type does not feel like a valid use-case (well,CharSequence
eventually?)Map<String, String>
in the FreeMarker template.Map<String, String>
with theTypeFactory
. If you have a solution for this, please let me know. I could then remove theMapKeyMappingField.ftl
and use a typedField.ftl