mirror of
https://github.com/mapstruct/mapstruct.git
synced 2025-07-12 00:00:08 +08:00
#1561 add support for adders in combination with streams
- Extended Type#getAlternativeTargetAccessors to recognize stream read accessors for which no corresponding setter exists (there is only an add method) - Extended SourceRHS#getSourceTypeForMatching to return the correct source type for streams too - Add StreamAdderWrapper to map Stream -> Adder - Extended PropertyMapping$PropertyMappingBuilder#assignToPlainViaAdder to return StreamAdderWrapper if source type is stream
This commit is contained in:
parent
5a4990c474
commit
30c2dadec7
@ -25,6 +25,7 @@ import org.mapstruct.ap.internal.model.assignment.ArrayCopyWrapper;
|
|||||||
import org.mapstruct.ap.internal.model.assignment.EnumConstantWrapper;
|
import org.mapstruct.ap.internal.model.assignment.EnumConstantWrapper;
|
||||||
import org.mapstruct.ap.internal.model.assignment.GetterWrapperForCollectionsAndMaps;
|
import org.mapstruct.ap.internal.model.assignment.GetterWrapperForCollectionsAndMaps;
|
||||||
import org.mapstruct.ap.internal.model.assignment.SetterWrapper;
|
import org.mapstruct.ap.internal.model.assignment.SetterWrapper;
|
||||||
|
import org.mapstruct.ap.internal.model.assignment.StreamAdderWrapper;
|
||||||
import org.mapstruct.ap.internal.model.assignment.UpdateWrapper;
|
import org.mapstruct.ap.internal.model.assignment.UpdateWrapper;
|
||||||
import org.mapstruct.ap.internal.model.common.Assignment;
|
import org.mapstruct.ap.internal.model.common.Assignment;
|
||||||
import org.mapstruct.ap.internal.model.common.FormattingParameters;
|
import org.mapstruct.ap.internal.model.common.FormattingParameters;
|
||||||
@ -438,6 +439,10 @@ public class PropertyMapping extends ModelElement {
|
|||||||
if ( result.getSourceType().isCollectionType() ) {
|
if ( result.getSourceType().isCollectionType() ) {
|
||||||
result = new AdderWrapper( result, method.getThrownTypes(), isFieldAssignment(), targetPropertyName );
|
result = new AdderWrapper( result, method.getThrownTypes(), isFieldAssignment(), targetPropertyName );
|
||||||
}
|
}
|
||||||
|
else if ( result.getSourceType().isStreamType() ) {
|
||||||
|
result = new StreamAdderWrapper(
|
||||||
|
result, method.getThrownTypes(), isFieldAssignment(), targetPropertyName );
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
// Possibly adding null to a target collection. So should be surrounded by an null check.
|
// Possibly adding null to a target collection. So should be surrounded by an null check.
|
||||||
result = new SetterWrapper( result, method.getThrownTypes(), ALWAYS, isFieldAssignment(), targetType );
|
result = new SetterWrapper( result, method.getThrownTypes(), ALWAYS, isFieldAssignment(), targetType );
|
||||||
|
@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* 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.internal.model.assignment;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import org.mapstruct.ap.internal.model.common.Assignment;
|
||||||
|
import org.mapstruct.ap.internal.model.common.Type;
|
||||||
|
import org.mapstruct.ap.internal.util.Nouns;
|
||||||
|
|
||||||
|
import static org.mapstruct.ap.internal.util.Collections.first;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps the assignment in a target setter.
|
||||||
|
*
|
||||||
|
* @author Sebastian Haberey
|
||||||
|
*/
|
||||||
|
public class StreamAdderWrapper extends AssignmentWrapper {
|
||||||
|
|
||||||
|
private final List<Type> thrownTypesToExclude;
|
||||||
|
private final Type adderType;
|
||||||
|
|
||||||
|
public StreamAdderWrapper(Assignment rhs,
|
||||||
|
List<Type> thrownTypesToExclude,
|
||||||
|
boolean fieldAssignment,
|
||||||
|
String targetPropertyName ) {
|
||||||
|
super( rhs, fieldAssignment );
|
||||||
|
this.thrownTypesToExclude = thrownTypesToExclude;
|
||||||
|
String desiredName = Nouns.singularize( targetPropertyName );
|
||||||
|
rhs.setSourceLocalVarName( rhs.createLocalVarName( desiredName ) );
|
||||||
|
adderType = first( getSourceType().determineTypeArguments( Stream.class ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Type> getThrownTypes() {
|
||||||
|
List<Type> parentThrownTypes = super.getThrownTypes();
|
||||||
|
List<Type> result = new ArrayList<Type>( parentThrownTypes );
|
||||||
|
for ( Type thrownTypeToExclude : thrownTypesToExclude ) {
|
||||||
|
for ( Type parentExceptionType : parentThrownTypes ) {
|
||||||
|
if ( parentExceptionType.isAssignableTo( thrownTypeToExclude ) ) {
|
||||||
|
result.remove( parentExceptionType );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type getAdderType() {
|
||||||
|
return adderType;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<Type> getImportTypes() {
|
||||||
|
Set<Type> imported = new HashSet<Type>();
|
||||||
|
imported.addAll( super.getImportTypes() );
|
||||||
|
imported.add( adderType.getTypeBound() );
|
||||||
|
return imported;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -9,6 +9,7 @@ import java.util.Collection;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import org.mapstruct.ap.internal.util.Strings;
|
import org.mapstruct.ap.internal.util.Strings;
|
||||||
|
|
||||||
@ -124,8 +125,15 @@ public class SourceRHS extends ModelElement implements Assignment {
|
|||||||
* @return the source type to be used in the matching process.
|
* @return the source type to be used in the matching process.
|
||||||
*/
|
*/
|
||||||
public Type getSourceTypeForMatching() {
|
public Type getSourceTypeForMatching() {
|
||||||
return useElementAsSourceTypeForMatching && sourceType.isCollectionType() ?
|
if ( useElementAsSourceTypeForMatching ) {
|
||||||
first( sourceType.determineTypeArguments( Collection.class ) ) : sourceType;
|
if ( sourceType.isCollectionType() ) {
|
||||||
|
return first( sourceType.determineTypeArguments( Collection.class ) );
|
||||||
|
}
|
||||||
|
else if ( sourceType.isStreamType() ) {
|
||||||
|
return first( sourceType.determineTypeArguments( Stream.class ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sourceType;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -13,6 +13,7 @@ import java.util.LinkedHashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.stream.Stream;
|
||||||
import javax.lang.model.element.AnnotationMirror;
|
import javax.lang.model.element.AnnotationMirror;
|
||||||
import javax.lang.model.element.Element;
|
import javax.lang.model.element.Element;
|
||||||
import javax.lang.model.element.ElementKind;
|
import javax.lang.model.element.ElementKind;
|
||||||
@ -33,6 +34,7 @@ import org.mapstruct.ap.internal.prism.CollectionMappingStrategyPrism;
|
|||||||
import org.mapstruct.ap.internal.util.AccessorNamingUtils;
|
import org.mapstruct.ap.internal.util.AccessorNamingUtils;
|
||||||
import org.mapstruct.ap.internal.util.Executables;
|
import org.mapstruct.ap.internal.util.Executables;
|
||||||
import org.mapstruct.ap.internal.util.Filters;
|
import org.mapstruct.ap.internal.util.Filters;
|
||||||
|
import org.mapstruct.ap.internal.util.JavaStreamConstants;
|
||||||
import org.mapstruct.ap.internal.util.Nouns;
|
import org.mapstruct.ap.internal.util.Nouns;
|
||||||
import org.mapstruct.ap.internal.util.accessor.Accessor;
|
import org.mapstruct.ap.internal.util.accessor.Accessor;
|
||||||
import org.mapstruct.ap.internal.util.accessor.ExecutableElementAccessor;
|
import org.mapstruct.ap.internal.util.accessor.ExecutableElementAccessor;
|
||||||
@ -625,46 +627,67 @@ public class Type extends ModelElement implements Comparable<Type> {
|
|||||||
*/
|
*/
|
||||||
private Accessor getAdderForType(Type collectionProperty, String pluralPropertyName) {
|
private Accessor getAdderForType(Type collectionProperty, String pluralPropertyName) {
|
||||||
|
|
||||||
List<Accessor> candidates = new ArrayList<Accessor>();
|
List<Accessor> candidates;
|
||||||
if ( collectionProperty.isCollectionType ) {
|
|
||||||
|
|
||||||
// this is a collection, so this can be done always
|
if ( collectionProperty.isCollectionType() ) {
|
||||||
TypeMirror typeArg = first( collectionProperty.determineTypeArguments( Iterable.class ) ).getTypeBound()
|
candidates = getAccessorCandidates( collectionProperty, Iterable.class );
|
||||||
.getTypeMirror();
|
|
||||||
// now, look for a method that
|
|
||||||
// 1) starts with add,
|
|
||||||
// 2) and has typeArg as one and only arg
|
|
||||||
List<Accessor> adderList = getAdders();
|
|
||||||
for ( Accessor adder : adderList ) {
|
|
||||||
ExecutableElement executable = adder.getExecutable();
|
|
||||||
if ( executable == null ) {
|
|
||||||
// it should not be null, but to be safe
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
VariableElement arg = executable.getParameters().get( 0 );
|
|
||||||
if ( typeUtils.isSameType( arg.asType(), typeArg ) ) {
|
|
||||||
candidates.add( adder );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
else if ( collectionProperty.isStreamType() ) {
|
||||||
|
candidates = getAccessorCandidates( collectionProperty, Stream.class );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if ( candidates.isEmpty() ) {
|
if ( candidates.isEmpty() ) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
else if ( candidates.size() == 1 ) {
|
|
||||||
|
if ( candidates.size() == 1 ) {
|
||||||
return candidates.get( 0 );
|
return candidates.get( 0 );
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
for ( Accessor candidate : candidates ) {
|
for ( Accessor candidate : candidates ) {
|
||||||
String elementName = accessorNaming.getElementNameForAdder( candidate );
|
String elementName = accessorNaming.getElementNameForAdder( candidate );
|
||||||
if ( elementName != null && elementName.equals( Nouns.singularize( pluralPropertyName ) ) ) {
|
if ( elementName != null && elementName.equals( Nouns.singularize( pluralPropertyName ) ) ) {
|
||||||
return candidate;
|
return candidate;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all accessor candidates that start with "add" and have exactly one argument
|
||||||
|
* whose type matches the collection or stream property's type argument.
|
||||||
|
*
|
||||||
|
* @param property the collection or stream property
|
||||||
|
* @param superclass the superclass to use for type argument lookup
|
||||||
|
*
|
||||||
|
* @return accessor candidates
|
||||||
|
*/
|
||||||
|
private List<Accessor> getAccessorCandidates(Type property, Class<?> superclass) {
|
||||||
|
TypeMirror typeArg = first( property.determineTypeArguments( superclass ) ).getTypeBound()
|
||||||
|
.getTypeMirror();
|
||||||
|
// now, look for a method that
|
||||||
|
// 1) starts with add,
|
||||||
|
// 2) and has typeArg as one and only arg
|
||||||
|
List<Accessor> adderList = getAdders();
|
||||||
|
List<Accessor> candidateList = new ArrayList<Accessor>();
|
||||||
|
for ( Accessor adder : adderList ) {
|
||||||
|
ExecutableElement executable = adder.getExecutable();
|
||||||
|
if ( executable == null ) {
|
||||||
|
// it should not be null, but to be safe
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
VariableElement arg = executable.getParameters().get( 0 );
|
||||||
|
if ( typeUtils.isSameType( arg.asType(), typeArg ) ) {
|
||||||
|
candidateList.add( adder );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return candidateList;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* getSetters
|
* getSetters
|
||||||
*
|
*
|
||||||
@ -716,7 +739,7 @@ public class Type extends ModelElement implements Comparable<Type> {
|
|||||||
// an accessor could substitute the setter in that case and act as setter.
|
// an accessor could substitute the setter in that case and act as setter.
|
||||||
// (assuming it is initialized)
|
// (assuming it is initialized)
|
||||||
for ( Accessor readAccessor : readAccessors ) {
|
for ( Accessor readAccessor : readAccessors ) {
|
||||||
if ( isCollectionOrMap( readAccessor ) &&
|
if ( isCollectionOrMapOrStream( readAccessor ) &&
|
||||||
!correspondingSetterMethodExists( readAccessor, setterMethods ) ) {
|
!correspondingSetterMethodExists( readAccessor, setterMethods ) ) {
|
||||||
result.add( readAccessor );
|
result.add( readAccessor );
|
||||||
}
|
}
|
||||||
@ -745,14 +768,21 @@ public class Type extends ModelElement implements Comparable<Type> {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isCollectionOrMap(Accessor getterMethod) {
|
private boolean isCollectionOrMapOrStream(Accessor getterMethod) {
|
||||||
return isCollection( getterMethod.getAccessedType() ) || isMap( getterMethod.getAccessedType() );
|
return isCollection( getterMethod.getAccessedType() ) || isMap( getterMethod.getAccessedType() ) ||
|
||||||
|
isStream( getterMethod.getAccessedType() );
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isCollection(TypeMirror candidate) {
|
private boolean isCollection(TypeMirror candidate) {
|
||||||
return isSubType( candidate, Collection.class );
|
return isSubType( candidate, Collection.class );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isStream(TypeMirror candidate) {
|
||||||
|
TypeElement streamTypeElement = elementUtils.getTypeElement( JavaStreamConstants.STREAM_FQN );
|
||||||
|
TypeMirror streamType = streamTypeElement == null ? null : typeUtils.erasure( streamTypeElement.asType() );
|
||||||
|
return streamType != null && typeUtils.isSubtype( candidate, streamType );
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isMap(TypeMirror candidate) {
|
private boolean isMap(TypeMirror candidate) {
|
||||||
return isSubType( candidate, Map.class );
|
return isSubType( candidate, Map.class );
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
<#--
|
||||||
|
|
||||||
|
Copyright MapStruct Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
-->
|
||||||
|
<#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.assignment.StreamAdderWrapper" -->
|
||||||
|
<#import "../macro/CommonMacros.ftl" as lib>
|
||||||
|
<@lib.handleExceptions>
|
||||||
|
if ( ${sourceReference} != null ) {
|
||||||
|
${sourceReference}.forEach( ${ext.targetBeanName}::${ext.targetWriteAccessorName} );
|
||||||
|
}
|
||||||
|
</@lib.handleExceptions>
|
@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* 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._1561.java8;
|
||||||
|
|
||||||
|
import org.mapstruct.CollectionMappingStrategy;
|
||||||
|
import org.mapstruct.Mapper;
|
||||||
|
import org.mapstruct.factory.Mappers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Sebastian Haberey
|
||||||
|
*/
|
||||||
|
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED)
|
||||||
|
public interface Issue1561Mapper {
|
||||||
|
|
||||||
|
Issue1561Mapper
|
||||||
|
INSTANCE = Mappers.getMapper( Issue1561Mapper.class );
|
||||||
|
|
||||||
|
Target map(Source source);
|
||||||
|
|
||||||
|
Source map(Target target);
|
||||||
|
}
|
@ -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._1561.java8;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.mapstruct.ap.testutil.IssueKey;
|
||||||
|
import org.mapstruct.ap.testutil.WithClasses;
|
||||||
|
import org.mapstruct.ap.testutil.runner.AnnotationProcessorTestRunner;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Sebastian Haberey
|
||||||
|
*/
|
||||||
|
@RunWith(AnnotationProcessorTestRunner.class)
|
||||||
|
@IssueKey("1561")
|
||||||
|
@WithClasses({
|
||||||
|
Issue1561Mapper.class,
|
||||||
|
Source.class,
|
||||||
|
Target.class
|
||||||
|
})
|
||||||
|
public class Issue1561Test {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldCorrectlyUseAdder() {
|
||||||
|
|
||||||
|
Source source = new Source();
|
||||||
|
source.addProperty( "first" );
|
||||||
|
source.addProperty( "second" );
|
||||||
|
|
||||||
|
Target target = Issue1561Mapper.INSTANCE.map( source );
|
||||||
|
|
||||||
|
assertThat( target.getProperties() )
|
||||||
|
.containsExactly( "first", "second" );
|
||||||
|
|
||||||
|
Source mapped = Issue1561Mapper.INSTANCE.map( target );
|
||||||
|
|
||||||
|
assertThat( mapped.getProperties() )
|
||||||
|
.containsExactly( "first", "second" );
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* 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._1561.java8;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Sebastian Haberey
|
||||||
|
*/
|
||||||
|
public class Source {
|
||||||
|
|
||||||
|
private List<String> properties = new ArrayList<String>();
|
||||||
|
|
||||||
|
public List<String> getProperties() {
|
||||||
|
return properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addProperty(String property) {
|
||||||
|
properties.add( property );
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* 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._1561.java8;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Sebastian Haberey
|
||||||
|
*/
|
||||||
|
public class Target {
|
||||||
|
|
||||||
|
private List<String> properties = new ArrayList<String>();
|
||||||
|
|
||||||
|
public Stream<String> getProperties() {
|
||||||
|
return properties.stream();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addProperty(String property) {
|
||||||
|
properties.add( property );
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user