From 721288140aa1c1210de222224b44f23e82be0c53 Mon Sep 17 00:00:00 2001 From: Zegveld <41897697+Zegveld@users.noreply.github.com> Date: Fri, 4 Aug 2023 10:14:53 +0200 Subject: [PATCH] Feature/2663 (#3007) #2663 Fix for 2-step mapping with generics. --------- Co-authored-by: Ben Zegveld --- .../ap/internal/model/common/Type.java | 116 +++++++++++++++++- .../creation/MappingResolverImpl.java | 2 +- .../ap/test/bugs/_2663/Issue2663Mapper.java | 31 +++++ .../ap/test/bugs/_2663/Issue2663Test.java | 47 +++++++ .../ap/test/bugs/_2663/JsonNullable.java | 44 +++++++ .../ap/test/bugs/_2663/Nullable.java | 45 +++++++ .../ap/test/bugs/_2663/NullableHelper.java | 23 ++++ .../mapstruct/ap/test/bugs/_2663/Request.java | 44 +++++++ .../ap/test/bugs/_2663/RequestDto.java | 44 +++++++ 9 files changed, 390 insertions(+), 6 deletions(-) create mode 100644 processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/Issue2663Mapper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/Issue2663Test.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/JsonNullable.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/Nullable.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/NullableHelper.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/Request.java create mode 100644 processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/RequestDto.java diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/common/Type.java b/processor/src/main/java/org/mapstruct/ap/internal/model/common/Type.java index a6d942748..c18574fcf 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/common/Type.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/common/Type.java @@ -605,6 +605,21 @@ public class Type extends ModelElement implements Comparable { ); } + 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" * as well. @@ -1377,9 +1392,9 @@ public class Type extends ModelElement implements Comparable { /** * 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. - * - * For example: + * the provided parameterized type where this typeVar type is used.
+ *
+ * For example:
      * {@code
      * this: T
      * declaredType: JAXBElement
@@ -1392,12 +1407,13 @@ public class Type extends ModelElement implements Comparable {
      * parameterizedType: Callable
      * return: BigDecimal
      * }
+     * 
* * @param declared the 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) - * - the matching parameter in the parameterized type when this is a type var when found + * @return - the same type when this is not a type var in the broadest sense (T, T[], or ? extends T)
+ * - the matching parameter in the parameterized type when this is a type var when found
* - null in all other cases */ public ResolvedPair resolveParameterToType(Type declared, Type parameterized) { @@ -1408,6 +1424,96 @@ public class Type extends ModelElement implements Comparable { return new ResolvedPair( this, this ); } + /** + * Resolves generic types using the declared and parameterized types as input.
+ *
+ * For example: + *
+     * {@code
+     * this: T
+     * declaredType: JAXBElement
+     * parameterizedType: JAXBElement
+     * result: Integer
+     *
+     * this: List
+     * declaredType: JAXBElement
+     * parameterizedType: JAXBElement
+     * result: List
+     *
+     * this: List
+     * declaredType: JAXBElement
+     * parameterizedType: JAXBElement
+     * result: List
+     *
+     * this: List>
+     * declaredType: JAXBElement
+     * parameterizedType: JAXBElement
+     * result: List>
+     * }
+     * 
+ * It also works for partial matching.
+ *
+ * For example: + *
+     * {@code
+     * this: Map
+     * declaredType: JAXBElement
+     * parameterizedType: JAXBElement
+     * result: Map
+     * }
+     * 
+ * It also works with multiple parameters at both sides.
+ *
+ * For example when reversing Key/Value for a Map: + *
+     * {@code
+     * this: Map
+     * declaredType: HashMap
+     * parameterizedType: HashMap
+     * result: Map
+     * }
+     * 
+ * + * Mismatch result examples: + *
+     * {@code
+     * this: T
+     * declaredType: JAXBElement
+     * parameterizedType: JAXBElement
+     * result: null
+     *
+     * this: List
+     * declaredType: JAXBElement
+     * parameterizedType: JAXBElement
+     * result: List
+     * }
+     * 
+ * + * @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.
+ * - the type but then with the matching type parameters replaced.
+ * - 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() { return ( hasExtendsBound() || hasSuperBound() ) && getTypeBound().isTypeVar(); } diff --git a/processor/src/main/java/org/mapstruct/ap/internal/processor/creation/MappingResolverImpl.java b/processor/src/main/java/org/mapstruct/ap/internal/processor/creation/MappingResolverImpl.java index 586465d5f..32fa1af93 100755 --- a/processor/src/main/java/org/mapstruct/ap/internal/processor/creation/MappingResolverImpl.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/processor/creation/MappingResolverImpl.java @@ -825,7 +825,7 @@ public class MappingResolverImpl implements MappingResolver { for ( T2 yCandidate : yMethods ) { Type ySourceType = yCandidate.getMappingSourceType(); - ySourceType = ySourceType.resolveParameterToType( targetType, yCandidate.getResultType() ).getMatch(); + ySourceType = ySourceType.resolveGenericTypeParameters( targetType, yCandidate.getResultType() ); Type yTargetType = yCandidate.getResultType(); if ( ySourceType == null || !yTargetType.isRawAssignableTo( targetType ) diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/Issue2663Mapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/Issue2663Mapper.java new file mode 100644 index 000000000..0ac236372 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/Issue2663Mapper.java @@ -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 mapJsonNullableChildren(JsonNullable dtos) { + if ( dtos.isPresent() ) { + return JsonNullable.of( mapChild( dtos.get() ) ); + } + else { + return JsonNullable.undefined(); + } + } + + Request.ChildRequest mapChild(RequestDto.ChildRequestDto dto); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/Issue2663Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/Issue2663Test.java new file mode 100644 index 000000000..efd14b1d3 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/Issue2663Test.java @@ -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 ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/JsonNullable.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/JsonNullable.java new file mode 100644 index 000000000..4794aff94 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/JsonNullable.java @@ -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 { + + 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 JsonNullable undefined() { + return (JsonNullable) UNDEFINED; + } + + public static JsonNullable of(T value) { + return new JsonNullable<>( value, true ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/Nullable.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/Nullable.java new file mode 100644 index 000000000..f6309be1c --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/Nullable.java @@ -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 { + + @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 Nullable of(T value) { + return new Nullable<>( value, true ); + } + + @SuppressWarnings("unchecked") + public static Nullable undefined() { + return (Nullable) UNDEFINED; + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/NullableHelper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/NullableHelper.java new file mode 100644 index 000000000..ae68300e6 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/NullableHelper.java @@ -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 Nullable jsonNullableToNullable(JsonNullable jsonNullable) { + if ( jsonNullable.isPresent() ) { + return Nullable.of( jsonNullable.get() ); + } + return Nullable.undefined(); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/Request.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/Request.java new file mode 100644 index 000000000..8b58e2e0b --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/Request.java @@ -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 name = Nullable.undefined(); + private Nullable child = Nullable.undefined(); + + public Nullable getName() { + return name; + } + + public void setName(Nullable name) { + this.name = name; + } + + public Nullable getChild() { + return child; + } + + public void setChild(Nullable child) { + this.child = child; + } + + public static class ChildRequest { + + private Nullable name = Nullable.undefined(); + + public Nullable getName() { + return name; + } + + public void setName(Nullable name) { + this.name = name; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/RequestDto.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/RequestDto.java new file mode 100644 index 000000000..d1a93a9bd --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_2663/RequestDto.java @@ -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 name = JsonNullable.undefined(); + private JsonNullable child = JsonNullable.undefined(); + + public JsonNullable getName() { + return name; + } + + public void setName(JsonNullable name) { + this.name = name; + } + + public JsonNullable getChild() { + return child; + } + + public void setChild(JsonNullable child) { + this.child = child; + } + + public static class ChildRequestDto { + + private JsonNullable name = JsonNullable.undefined(); + + public JsonNullable getName() { + return name; + } + + public void setName(JsonNullable name) { + this.name = name; + } + } +}