#2553 Support source property paths for maps

This commit is contained in:
Filip Hrisafov 2021-11-20 08:48:08 +01:00 committed by GitHub
parent 29008e12bf
commit 13bc0c023c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 162 additions and 90 deletions

View File

@ -504,7 +504,7 @@ public class BeanMappingMethod extends NormalTypeMappingMethod {
.build();
Accessor targetPropertyReadAccessor =
method.getResultType().getPropertyReadAccessors().get( propertyName );
method.getResultType().getReadAccessor( propertyName );
MappingReferences mappingRefs = extractMappingReferences( propertyName, true );
PropertyMapping propertyMapping = new PropertyMappingBuilder()
.mappingContext( ctx )
@ -1047,7 +1047,7 @@ public class BeanMappingMethod extends NormalTypeMappingMethod {
}
Accessor targetWriteAccessor = unprocessedTargetProperties.get( targetPropertyName );
Accessor targetReadAccessor = resultTypeToMap.getPropertyReadAccessors().get( targetPropertyName );
Accessor targetReadAccessor = resultTypeToMap.getReadAccessor( targetPropertyName );
if ( targetWriteAccessor == null ) {
if ( targetReadAccessor == null ) {
@ -1389,7 +1389,7 @@ public class BeanMappingMethod extends NormalTypeMappingMethod {
}
Accessor targetPropertyReadAccessor =
method.getResultType().getPropertyReadAccessors().get( targetPropertyName );
method.getResultType().getReadAccessor( targetPropertyName );
MappingReferences mappingRefs = extractMappingReferences( targetPropertyName, false );
PropertyMapping propertyMapping = new PropertyMappingBuilder().mappingContext( ctx )
.sourceMethod( method )
@ -1432,7 +1432,7 @@ public class BeanMappingMethod extends NormalTypeMappingMethod {
.build();
Accessor targetPropertyReadAccessor =
method.getResultType().getPropertyReadAccessors().get( targetProperty.getKey() );
method.getResultType().getReadAccessor( targetProperty.getKey() );
MappingReferences mappingRefs = extractMappingReferences( targetProperty.getKey(), false );
PropertyMapping propertyMapping = new PropertyMappingBuilder()
.mappingContext( ctx )
@ -1473,22 +1473,11 @@ public class BeanMappingMethod extends NormalTypeMappingMethod {
return sourceRef;
}
if ( sourceParameter.getType().isMapType() ) {
List<Type> typeParameters = sourceParameter.getType().getTypeParameters();
if ( typeParameters.size() == 2 && typeParameters.get( 0 ).isString() ) {
return SourceReference.fromMapSource(
new String[] { targetPropertyName },
sourceParameter
);
}
}
Accessor sourceReadAccessor =
sourceParameter.getType().getPropertyReadAccessors().get( targetPropertyName );
Accessor sourceReadAccessor = sourceParameter.getType().getReadAccessor( targetPropertyName );
if ( sourceReadAccessor != null ) {
// property mapping
Accessor sourcePresenceChecker =
sourceParameter.getType().getPropertyPresenceCheckers().get( targetPropertyName );
sourceParameter.getType().getPresenceChecker( targetPropertyName );
DeclaredType declaredSourceType = (DeclaredType) sourceParameter.getType().getTypeMirror();
Type returnType = ctx.getTypeFactory().getReturnType( declaredSourceType, sourceReadAccessor );

View File

@ -14,9 +14,12 @@ import org.mapstruct.ap.internal.model.common.Parameter;
import org.mapstruct.ap.internal.model.common.PresenceCheck;
import org.mapstruct.ap.internal.model.common.Type;
import org.mapstruct.ap.internal.model.beanmapping.PropertyEntry;
import org.mapstruct.ap.internal.model.presence.SourceReferenceContainsKeyPresenceCheck;
import org.mapstruct.ap.internal.model.presence.SourceReferenceMethodPresenceCheck;
import org.mapstruct.ap.internal.util.Strings;
import org.mapstruct.ap.internal.util.ValueProvider;
import org.mapstruct.ap.internal.util.accessor.Accessor;
import org.mapstruct.ap.internal.util.accessor.AccessorType;
/**
* This method is used to convert the nested properties as listed in propertyEntries into a method
@ -165,11 +168,20 @@ public class NestedPropertyMappingMethod extends MappingMethod {
public SafePropertyEntry(PropertyEntry entry, String safeName, String previousPropertyName) {
this.safeName = safeName;
this.readAccessorName = ValueProvider.of( entry.getReadAccessor() ).getValue();
if ( entry.getPresenceChecker() != null ) {
this.presenceChecker = new SourceReferenceMethodPresenceCheck(
previousPropertyName,
entry.getPresenceChecker().getSimpleName()
);
Accessor presenceChecker = entry.getPresenceChecker();
if ( presenceChecker != null ) {
if ( presenceChecker.getAccessorType() == AccessorType.MAP_CONTAINS ) {
this.presenceChecker = new SourceReferenceContainsKeyPresenceCheck(
previousPropertyName,
presenceChecker.getSimpleName()
);
}
else {
this.presenceChecker = new SourceReferenceMethodPresenceCheck(
previousPropertyName,
presenceChecker.getSimpleName()
);
}
}
else {
this.presenceChecker = null;

View File

@ -642,7 +642,7 @@ public class NestedTargetPropertyMappingHolder {
boolean forceUpdateMethod) {
Accessor targetWriteAccessor = targetPropertiesWriteAccessors.get( targetPropertyName );
Accessor targetReadAccessor = targetType.getPropertyReadAccessors().get( targetPropertyName );
Accessor targetReadAccessor = targetType.getReadAccessor( targetPropertyName );
if ( targetWriteAccessor == null ) {
Set<String> readAccessors = targetType.getPropertyReadAccessors().keySet();
String mostSimilarProperty = Strings.getMostSimilarWord( targetPropertyName, readAccessors );

View File

@ -7,16 +7,13 @@ package org.mapstruct.ap.internal.model.beanmapping;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import org.mapstruct.ap.internal.model.common.Parameter;
import org.mapstruct.ap.internal.model.common.Type;
@ -27,8 +24,6 @@ import org.mapstruct.ap.internal.util.FormattingMessager;
import org.mapstruct.ap.internal.util.Message;
import org.mapstruct.ap.internal.util.Strings;
import org.mapstruct.ap.internal.util.accessor.Accessor;
import org.mapstruct.ap.internal.util.accessor.MapValueAccessor;
import org.mapstruct.ap.internal.util.accessor.MapValuePresenceChecker;
import static org.mapstruct.ap.internal.model.beanmapping.PropertyEntry.forSourceReference;
import static org.mapstruct.ap.internal.util.Collections.last;
@ -51,32 +46,13 @@ import static org.mapstruct.ap.internal.util.Collections.last;
* <li>{@code propertyEntries[1]} will describe {@code propB}</li>
* </ul>
*
* After building, {@link #isValid()} will return true when when no problems are detected during building.
* After building, {@link #isValid()} will return true when no problems are detected during building.
*
* @author Sjaak Derksen
* @author Filip Hrisafov
*/
public class SourceReference extends AbstractReference {
public static SourceReference fromMapSource(String[] segments, Parameter parameter) {
Type parameterType = parameter.getType();
Type valueType = parameterType.getTypeParameters().get( 1 );
TypeElement typeElement = parameterType.getTypeElement();
TypeMirror typeMirror = valueType.getTypeMirror();
String simpleName = String.join( ".", segments );
MapValueAccessor mapValueAccessor = new MapValueAccessor( typeElement, typeMirror, simpleName );
MapValuePresenceChecker mapValuePresenceChecker = new MapValuePresenceChecker(
typeElement,
typeMirror,
simpleName
);
List<PropertyEntry> entries = Collections.singletonList(
PropertyEntry.forSourceReference( segments, mapValueAccessor, mapValuePresenceChecker, valueType )
);
return new SourceReference( parameter, entries, true );
}
/**
* Builds a {@link SourceReference} from an {@code @Mappping}.
*/
@ -173,11 +149,6 @@ public class SourceReference extends AbstractReference {
* @return the source reference
*/
private SourceReference buildFromSingleSourceParameters(String[] segments, Parameter parameter) {
if ( canBeTreatedAsMapSourceType( parameter.getType() ) ) {
return fromMapSource( segments, parameter );
}
boolean foundEntryMatch;
String[] propertyNames = segments;
@ -214,14 +185,6 @@ public class SourceReference extends AbstractReference {
*/
private SourceReference buildFromMultipleSourceParameters(String[] segments, Parameter parameter) {
if (parameter != null && canBeTreatedAsMapSourceType( parameter.getType() )) {
String[] propertyNames = new String[0];
if ( segments.length > 1 ) {
propertyNames = Arrays.copyOfRange( segments, 1, segments.length );
}
return fromMapSource( propertyNames, parameter );
}
boolean foundEntryMatch;
String[] propertyNames = new String[0];
@ -244,17 +207,8 @@ public class SourceReference extends AbstractReference {
return new SourceReference( parameter, entries, foundEntryMatch );
}
private boolean canBeTreatedAsMapSourceType(Type type) {
if ( !type.isMapType() ) {
return false;
}
List<Type> typeParameters = type.getTypeParameters();
return typeParameters.size() == 2 && typeParameters.get( 0 ).isString();
}
/**
* When there are more than one source parameters, the first segment name of the propery
* When there are more than one source parameters, the first segment name of the property
* needs to match the parameter name to avoid ambiguity
*
* consider: {@code Target map( Source1 source1, Source2 source2 )}
@ -356,24 +310,20 @@ public class SourceReference extends AbstractReference {
Type newType = type;
for ( int i = 0; i < entryNames.length; i++ ) {
boolean matchFound = false;
Map<String, Accessor> sourceReadAccessors = newType.getPropertyReadAccessors();
Map<String, Accessor> sourcePresenceCheckers = newType.getPropertyPresenceCheckers();
for ( Map.Entry<String, Accessor> getter : sourceReadAccessors.entrySet() ) {
if ( getter.getKey().equals( entryNames[i] ) ) {
newType = typeFactory.getReturnType(
(DeclaredType) newType.getTypeMirror(),
getter.getValue()
);
sourceEntries.add( forSourceReference(
Arrays.copyOf( entryNames, i + 1 ),
getter.getValue(),
sourcePresenceCheckers.get( entryNames[i] ),
newType
) );
matchFound = true;
break;
}
Accessor readAccessor = newType.getReadAccessor( entryNames[i] );
if ( readAccessor != null ) {
Accessor presenceChecker = newType.getPresenceChecker( entryNames[i] );
newType = typeFactory.getReturnType(
(DeclaredType) newType.getTypeMirror(),
readAccessor
);
sourceEntries.add( forSourceReference(
Arrays.copyOf( entryNames, i + 1 ),
readAccessor,
presenceChecker,
newType
) );
matchFound = true;
}
if ( !matchFound ) {
break;

View File

@ -48,6 +48,8 @@ import org.mapstruct.ap.internal.util.Nouns;
import org.mapstruct.ap.internal.util.TypeUtils;
import org.mapstruct.ap.internal.util.accessor.Accessor;
import org.mapstruct.ap.internal.util.accessor.AccessorType;
import org.mapstruct.ap.internal.util.accessor.MapValueAccessor;
import org.mapstruct.ap.internal.util.accessor.MapValuePresenceChecker;
import static org.mapstruct.ap.internal.util.Collections.first;
@ -60,6 +62,7 @@ import static org.mapstruct.ap.internal.util.Collections.first;
* through {@link TypeFactory}.
*
* @author Gunnar Morling
* @author Filip Hrisafov
*/
public class Type extends ModelElement implements Comparable<Type> {
@ -308,6 +311,17 @@ public class Type extends ModelElement implements Comparable<Type> {
return isMapType;
}
private boolean hasStringMapSignature() {
if ( isMapType() ) {
List<Type> typeParameters = getTypeParameters();
if ( typeParameters.size() == 2 && typeParameters.get( 0 ).isString() ) {
return true;
}
}
return false;
}
public boolean isCollectionOrMapType() {
return isCollectionType || isMapType;
}
@ -597,6 +611,42 @@ public class Type extends ModelElement implements Comparable<Type> {
}
}
public Accessor getReadAccessor(String propertyName) {
if ( hasStringMapSignature() ) {
ExecutableElement getMethod = getAllMethods()
.stream()
.filter( m -> m.getSimpleName().contentEquals( "get" ) )
.filter( m -> m.getParameters().size() == 1 )
.findAny()
.orElse( null );
return new MapValueAccessor( getMethod, typeParameters.get( 1 ).getTypeMirror(), propertyName );
}
Map<String, Accessor> readAccessors = getPropertyReadAccessors();
return readAccessors.get( propertyName );
}
public Accessor getPresenceChecker(String propertyName) {
if ( hasStringMapSignature() ) {
ExecutableElement containsKeyMethod = getAllMethods()
.stream()
.filter( m -> m.getSimpleName().contentEquals( "containsKey" ) )
.filter( m -> m.getParameters().size() == 1 )
.findAny()
.orElse( null );
return new MapValuePresenceChecker(
containsKeyMethod,
typeParameters.get( 1 ).getTypeMirror(),
propertyName
);
}
Map<String, Accessor> presenceCheckers = getPropertyPresenceCheckers();
return presenceCheckers.get( propertyName );
}
/**
* getPropertyReadAccessors
*

View File

@ -261,6 +261,20 @@ class FromMapMappingTest {
assertThat( target.getNestedTarget().getStringFromNestedMap() ).isEqualTo( "valueFromNestedMap" );
}
@IssueKey("2553")
@ProcessorTest
@WithClasses(MapToBeanFromMapAndNestedMapWithDefinedMapping.class)
void shouldMapFromNestedMapWithDefinedMapping() {
MapToBeanFromMapAndNestedMapWithDefinedMapping.Source source =
new MapToBeanFromMapAndNestedMapWithDefinedMapping.Source();
MapToBeanFromMapAndNestedMapWithDefinedMapping.Target target =
MapToBeanFromMapAndNestedMapWithDefinedMapping.INSTANCE.toTarget( source );
assertThat( target ).isNotNull();
assertThat( target.getNested() ).isEqualTo( "valueFromNestedMap" );
}
@ProcessorTest
@WithClasses(ObjectMapToBeanWithQualifierMapper.class)
void shouldUseObjectQualifiedMethod() {

View File

@ -0,0 +1,57 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.ap.test.frommap;
import java.util.HashMap;
import java.util.Map;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
/**
* @author Filip Hrisafov
*/
@Mapper
public interface MapToBeanFromMapAndNestedMapWithDefinedMapping {
MapToBeanFromMapAndNestedMapWithDefinedMapping INSTANCE = Mappers.getMapper(
MapToBeanFromMapAndNestedMapWithDefinedMapping.class );
@Mapping(target = "nested", source = "nestedTarget.nested")
Target toTarget(Source source);
class Source {
private Map<String, String> nestedTarget = new HashMap<>();
public Map<String, String> getNestedTarget() {
return nestedTarget;
}
public void setNestedTarget(Map<String, String> nestedTarget) {
this.nestedTarget = nestedTarget;
}
public Source() {
nestedTarget.put( "nested", "valueFromNestedMap" );
}
}
class Target {
private String nested;
public String getNested() {
return nested;
}
public void setNested(String nested) {
this.nested = nested;
}
}
}