mirror of
https://github.com/mapstruct/mapstruct.git
synced 2025-07-12 00:00:08 +08:00
Feature/2663 (#3007)
#2663 Fix for 2-step mapping with generics. --------- Co-authored-by: Ben Zegveld <Ben.Zegveld@gmail.com>
This commit is contained in:
parent
812faeef51
commit
721288140a
@ -605,6 +605,21 @@ public class Type extends ModelElement implements Comparable<Type> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Type replaceGeneric(Type oldGenericType, Type newType) {
|
||||||
|
if ( !typeParameters.contains( oldGenericType ) || newType == null ) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
newType = newType.getBoxedEquivalent();
|
||||||
|
TypeMirror[] replacedTypeMirrors = new TypeMirror[typeParameters.size()];
|
||||||
|
for ( int i = 0; i < typeParameters.size(); i++ ) {
|
||||||
|
Type typeParameter = typeParameters.get( i );
|
||||||
|
replacedTypeMirrors[i] =
|
||||||
|
typeParameter.equals( oldGenericType ) ? newType.typeMirror : typeParameter.typeMirror;
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeFactory.getType( typeUtils.getDeclaredType( typeElement, replacedTypeMirrors ) );
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether this type is assignable to the given other type, considering the "extends / upper bounds"
|
* Whether this type is assignable to the given other type, considering the "extends / upper bounds"
|
||||||
* as well.
|
* as well.
|
||||||
@ -1377,9 +1392,9 @@ public class Type extends ModelElement implements Comparable<Type> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Steps through the declaredType in order to find a match for this typeVar Type. It aligns with
|
* Steps through the declaredType in order to find a match for this typeVar Type. It aligns with
|
||||||
* the provided parameterized type where this typeVar type is used.
|
* the provided parameterized type where this typeVar type is used.<br>
|
||||||
*
|
* <br>
|
||||||
* For example:
|
* For example:<pre>
|
||||||
* {@code
|
* {@code
|
||||||
* this: T
|
* this: T
|
||||||
* declaredType: JAXBElement<String>
|
* declaredType: JAXBElement<String>
|
||||||
@ -1392,12 +1407,13 @@ public class Type extends ModelElement implements Comparable<Type> {
|
|||||||
* parameterizedType: Callable<BigDecimal>
|
* parameterizedType: Callable<BigDecimal>
|
||||||
* return: BigDecimal
|
* return: BigDecimal
|
||||||
* }
|
* }
|
||||||
|
* </pre>
|
||||||
*
|
*
|
||||||
* @param declared the type
|
* @param declared the type
|
||||||
* @param parameterized the parameterized type
|
* @param parameterized the parameterized type
|
||||||
*
|
*
|
||||||
* @return - the same type when this is not a type var in the broadest sense (T, T[], or ? extends T)
|
* @return - the same type when this is not a type var in the broadest sense (T, T[], or ? extends T)<br>
|
||||||
* - the matching parameter in the parameterized type when this is a type var when found
|
* - the matching parameter in the parameterized type when this is a type var when found<br>
|
||||||
* - null in all other cases
|
* - null in all other cases
|
||||||
*/
|
*/
|
||||||
public ResolvedPair resolveParameterToType(Type declared, Type parameterized) {
|
public ResolvedPair resolveParameterToType(Type declared, Type parameterized) {
|
||||||
@ -1408,6 +1424,96 @@ public class Type extends ModelElement implements Comparable<Type> {
|
|||||||
return new ResolvedPair( this, this );
|
return new ResolvedPair( this, this );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves generic types using the declared and parameterized types as input.<br>
|
||||||
|
* <br>
|
||||||
|
* For example:
|
||||||
|
* <pre>
|
||||||
|
* {@code
|
||||||
|
* this: T
|
||||||
|
* declaredType: JAXBElement<T>
|
||||||
|
* parameterizedType: JAXBElement<Integer>
|
||||||
|
* result: Integer
|
||||||
|
*
|
||||||
|
* this: List<T>
|
||||||
|
* declaredType: JAXBElement<T>
|
||||||
|
* parameterizedType: JAXBElement<Integer>
|
||||||
|
* result: List<Integer>
|
||||||
|
*
|
||||||
|
* this: List<? extends T>
|
||||||
|
* declaredType: JAXBElement<? extends T>
|
||||||
|
* parameterizedType: JAXBElement<BigDecimal>
|
||||||
|
* result: List<BigDecimal>
|
||||||
|
*
|
||||||
|
* this: List<Optional<T>>
|
||||||
|
* declaredType: JAXBElement<T>
|
||||||
|
* parameterizedType: JAXBElement<BigDecimal>
|
||||||
|
* result: List<Optional<BigDecimal>>
|
||||||
|
* }
|
||||||
|
* </pre>
|
||||||
|
* It also works for partial matching.<br>
|
||||||
|
* <br>
|
||||||
|
* For example:
|
||||||
|
* <pre>
|
||||||
|
* {@code
|
||||||
|
* this: Map<K, V>
|
||||||
|
* declaredType: JAXBElement<K>
|
||||||
|
* parameterizedType: JAXBElement<BigDecimal>
|
||||||
|
* result: Map<BigDecimal, V>
|
||||||
|
* }
|
||||||
|
* </pre>
|
||||||
|
* It also works with multiple parameters at both sides.<br>
|
||||||
|
* <br>
|
||||||
|
* For example when reversing Key/Value for a Map:
|
||||||
|
* <pre>
|
||||||
|
* {@code
|
||||||
|
* this: Map<KEY, VALUE>
|
||||||
|
* declaredType: HashMap<VALUE, KEY>
|
||||||
|
* parameterizedType: HashMap<BigDecimal, String>
|
||||||
|
* result: Map<String, BigDecimal>
|
||||||
|
* }
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* Mismatch result examples:
|
||||||
|
* <pre>
|
||||||
|
* {@code
|
||||||
|
* this: T
|
||||||
|
* declaredType: JAXBElement<Y>
|
||||||
|
* parameterizedType: JAXBElement<Integer>
|
||||||
|
* result: null
|
||||||
|
*
|
||||||
|
* this: List<T>
|
||||||
|
* declaredType: JAXBElement<Y>
|
||||||
|
* parameterizedType: JAXBElement<Integer>
|
||||||
|
* result: List<T>
|
||||||
|
* }
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @param declared the type
|
||||||
|
* @param parameterized the parameterized type
|
||||||
|
*
|
||||||
|
* @return - the result of {@link #resolveParameterToType(Type, Type)} when this type itself is a type var.<br>
|
||||||
|
* - the type but then with the matching type parameters replaced.<br>
|
||||||
|
* - the same type when this type does not contain matching type parameters.
|
||||||
|
*/
|
||||||
|
public Type resolveGenericTypeParameters(Type declared, Type parameterized) {
|
||||||
|
if ( isTypeVar() || isArrayTypeVar() || isWildCardBoundByTypeVar() ) {
|
||||||
|
return resolveParameterToType( declared, parameterized ).getMatch();
|
||||||
|
}
|
||||||
|
Type resultType = this;
|
||||||
|
for ( Type generic : getTypeParameters() ) {
|
||||||
|
if ( generic.isTypeVar() || generic.isArrayTypeVar() || generic.isWildCardBoundByTypeVar() ) {
|
||||||
|
ResolvedPair resolveParameterToType = generic.resolveParameterToType( declared, parameterized );
|
||||||
|
resultType = resultType.replaceGeneric( generic, resolveParameterToType.getMatch() );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Type replacementType = generic.resolveParameterToType( declared, parameterized ).getMatch();
|
||||||
|
resultType = resultType.replaceGeneric( generic, replacementType );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resultType;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isWildCardBoundByTypeVar() {
|
public boolean isWildCardBoundByTypeVar() {
|
||||||
return ( hasExtendsBound() || hasSuperBound() ) && getTypeBound().isTypeVar();
|
return ( hasExtendsBound() || hasSuperBound() ) && getTypeBound().isTypeVar();
|
||||||
}
|
}
|
||||||
|
@ -825,7 +825,7 @@ public class MappingResolverImpl implements MappingResolver {
|
|||||||
|
|
||||||
for ( T2 yCandidate : yMethods ) {
|
for ( T2 yCandidate : yMethods ) {
|
||||||
Type ySourceType = yCandidate.getMappingSourceType();
|
Type ySourceType = yCandidate.getMappingSourceType();
|
||||||
ySourceType = ySourceType.resolveParameterToType( targetType, yCandidate.getResultType() ).getMatch();
|
ySourceType = ySourceType.resolveGenericTypeParameters( targetType, yCandidate.getResultType() );
|
||||||
Type yTargetType = yCandidate.getResultType();
|
Type yTargetType = yCandidate.getResultType();
|
||||||
if ( ySourceType == null
|
if ( ySourceType == null
|
||||||
|| !yTargetType.isRawAssignableTo( targetType )
|
|| !yTargetType.isRawAssignableTo( targetType )
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* 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.bugs._2663;
|
||||||
|
|
||||||
|
import org.mapstruct.Mapper;
|
||||||
|
import org.mapstruct.factory.Mappers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Filip Hrisafov
|
||||||
|
*/
|
||||||
|
@Mapper( uses = NullableHelper.class )
|
||||||
|
public interface Issue2663Mapper {
|
||||||
|
|
||||||
|
Issue2663Mapper INSTANCE = Mappers.getMapper( Issue2663Mapper.class );
|
||||||
|
|
||||||
|
Request map(RequestDto dto);
|
||||||
|
|
||||||
|
default JsonNullable<Request.ChildRequest> mapJsonNullableChildren(JsonNullable<RequestDto.ChildRequestDto> dtos) {
|
||||||
|
if ( dtos.isPresent() ) {
|
||||||
|
return JsonNullable.of( mapChild( dtos.get() ) );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return JsonNullable.undefined();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Request.ChildRequest mapChild(RequestDto.ChildRequestDto dto);
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* 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.bugs._2663;
|
||||||
|
|
||||||
|
import org.mapstruct.ap.testutil.IssueKey;
|
||||||
|
import org.mapstruct.ap.testutil.ProcessorTest;
|
||||||
|
import org.mapstruct.ap.testutil.WithClasses;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Filip Hrisafov
|
||||||
|
*/
|
||||||
|
@IssueKey( "2663" )
|
||||||
|
@WithClasses({
|
||||||
|
Issue2663Mapper.class,
|
||||||
|
JsonNullable.class,
|
||||||
|
Nullable.class,
|
||||||
|
NullableHelper.class,
|
||||||
|
Request.class,
|
||||||
|
RequestDto.class
|
||||||
|
})
|
||||||
|
public class Issue2663Test {
|
||||||
|
|
||||||
|
@ProcessorTest
|
||||||
|
public void shouldUnpackGenericsCorrectly() {
|
||||||
|
RequestDto dto = new RequestDto();
|
||||||
|
dto.setName( JsonNullable.of( "Tester" ) );
|
||||||
|
|
||||||
|
Request request = Issue2663Mapper.INSTANCE.map( dto );
|
||||||
|
|
||||||
|
assertThat( request.getName() )
|
||||||
|
.extracting( Nullable::get )
|
||||||
|
.isEqualTo( "Tester" );
|
||||||
|
|
||||||
|
dto.setName( JsonNullable.undefined() );
|
||||||
|
|
||||||
|
request = Issue2663Mapper.INSTANCE.map( dto );
|
||||||
|
|
||||||
|
assertThat( request.getName() )
|
||||||
|
.extracting( Nullable::isPresent )
|
||||||
|
.isEqualTo( false );
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* 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.bugs._2663;
|
||||||
|
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Filip Hrisafov
|
||||||
|
*/
|
||||||
|
public class JsonNullable<T> {
|
||||||
|
|
||||||
|
private static final JsonNullable<?> UNDEFINED = new JsonNullable<>( null, false );
|
||||||
|
|
||||||
|
private final T value;
|
||||||
|
private final boolean present;
|
||||||
|
|
||||||
|
private JsonNullable(T value, boolean present) {
|
||||||
|
this.value = value;
|
||||||
|
this.present = present;
|
||||||
|
}
|
||||||
|
|
||||||
|
public T get() {
|
||||||
|
if (!present) {
|
||||||
|
throw new NoSuchElementException("Value is undefined");
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPresent() {
|
||||||
|
return present;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static <T> JsonNullable<T> undefined() {
|
||||||
|
return (JsonNullable<T>) UNDEFINED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> JsonNullable<T> of(T value) {
|
||||||
|
return new JsonNullable<>( value, true );
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* 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.bugs._2663;
|
||||||
|
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Filip Hrisafov
|
||||||
|
*/
|
||||||
|
public class Nullable<T> {
|
||||||
|
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
|
private static final Nullable UNDEFINED = new Nullable<>( null, false );
|
||||||
|
|
||||||
|
private final T value;
|
||||||
|
private final boolean present;
|
||||||
|
|
||||||
|
private Nullable(T value, boolean present) {
|
||||||
|
this.value = value;
|
||||||
|
this.present = present;
|
||||||
|
}
|
||||||
|
|
||||||
|
public T get() {
|
||||||
|
if (!present) {
|
||||||
|
throw new NoSuchElementException("Value is undefined");
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPresent() {
|
||||||
|
return present;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> Nullable<T> of(T value) {
|
||||||
|
return new Nullable<>( value, true );
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static <T> Nullable<T> undefined() {
|
||||||
|
return (Nullable<T>) UNDEFINED;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* 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.bugs._2663;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Filip Hrisafov
|
||||||
|
*/
|
||||||
|
public class NullableHelper {
|
||||||
|
|
||||||
|
private NullableHelper() {
|
||||||
|
// Helper class
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> Nullable<T> jsonNullableToNullable(JsonNullable<T> jsonNullable) {
|
||||||
|
if ( jsonNullable.isPresent() ) {
|
||||||
|
return Nullable.of( jsonNullable.get() );
|
||||||
|
}
|
||||||
|
return Nullable.undefined();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* 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.bugs._2663;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Filip Hrisafov
|
||||||
|
*/
|
||||||
|
public class Request {
|
||||||
|
|
||||||
|
private Nullable<String> name = Nullable.undefined();
|
||||||
|
private Nullable<ChildRequest> child = Nullable.undefined();
|
||||||
|
|
||||||
|
public Nullable<String> getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(Nullable<String> name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Nullable<ChildRequest> getChild() {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChild(Nullable<ChildRequest> child) {
|
||||||
|
this.child = child;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ChildRequest {
|
||||||
|
|
||||||
|
private Nullable<String> name = Nullable.undefined();
|
||||||
|
|
||||||
|
public Nullable<String> getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(Nullable<String> name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* 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.bugs._2663;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Filip Hrisafov
|
||||||
|
*/
|
||||||
|
public class RequestDto {
|
||||||
|
|
||||||
|
private JsonNullable<String> name = JsonNullable.undefined();
|
||||||
|
private JsonNullable<ChildRequestDto> child = JsonNullable.undefined();
|
||||||
|
|
||||||
|
public JsonNullable<String> getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(JsonNullable<String> name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonNullable<ChildRequestDto> getChild() {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChild(JsonNullable<ChildRequestDto> child) {
|
||||||
|
this.child = child;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ChildRequestDto {
|
||||||
|
|
||||||
|
private JsonNullable<String> name = JsonNullable.undefined();
|
||||||
|
|
||||||
|
public JsonNullable<String> getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(JsonNullable<String> name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user