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.GetterWrapperForCollectionsAndMaps;
|
||||
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.common.Assignment;
|
||||
import org.mapstruct.ap.internal.model.common.FormattingParameters;
|
||||
@ -438,6 +439,10 @@ public class PropertyMapping extends ModelElement {
|
||||
if ( result.getSourceType().isCollectionType() ) {
|
||||
result = new AdderWrapper( result, method.getThrownTypes(), isFieldAssignment(), targetPropertyName );
|
||||
}
|
||||
else if ( result.getSourceType().isStreamType() ) {
|
||||
result = new StreamAdderWrapper(
|
||||
result, method.getThrownTypes(), isFieldAssignment(), targetPropertyName );
|
||||
}
|
||||
else {
|
||||
// Possibly adding null to a target collection. So should be surrounded by an null check.
|
||||
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.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
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.
|
||||
*/
|
||||
public Type getSourceTypeForMatching() {
|
||||
return useElementAsSourceTypeForMatching && sourceType.isCollectionType() ?
|
||||
first( sourceType.determineTypeArguments( Collection.class ) ) : sourceType;
|
||||
if ( useElementAsSourceTypeForMatching ) {
|
||||
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.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
import javax.lang.model.element.AnnotationMirror;
|
||||
import javax.lang.model.element.Element;
|
||||
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.Executables;
|
||||
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.accessor.Accessor;
|
||||
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) {
|
||||
|
||||
List<Accessor> candidates = new ArrayList<Accessor>();
|
||||
if ( collectionProperty.isCollectionType ) {
|
||||
List<Accessor> candidates;
|
||||
|
||||
// this is a collection, so this can be done always
|
||||
TypeMirror typeArg = first( collectionProperty.determineTypeArguments( Iterable.class ) ).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();
|
||||
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 );
|
||||
}
|
||||
}
|
||||
if ( collectionProperty.isCollectionType() ) {
|
||||
candidates = getAccessorCandidates( collectionProperty, Iterable.class );
|
||||
}
|
||||
else if ( collectionProperty.isStreamType() ) {
|
||||
candidates = getAccessorCandidates( collectionProperty, Stream.class );
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( candidates.isEmpty() ) {
|
||||
return null;
|
||||
}
|
||||
else if ( candidates.size() == 1 ) {
|
||||
|
||||
if ( candidates.size() == 1 ) {
|
||||
return candidates.get( 0 );
|
||||
}
|
||||
else {
|
||||
for ( Accessor candidate : candidates ) {
|
||||
String elementName = accessorNaming.getElementNameForAdder( candidate );
|
||||
if ( elementName != null && elementName.equals( Nouns.singularize( pluralPropertyName ) ) ) {
|
||||
return candidate;
|
||||
}
|
||||
|
||||
for ( Accessor candidate : candidates ) {
|
||||
String elementName = accessorNaming.getElementNameForAdder( candidate );
|
||||
if ( elementName != null && elementName.equals( Nouns.singularize( pluralPropertyName ) ) ) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
*
|
||||
@ -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.
|
||||
// (assuming it is initialized)
|
||||
for ( Accessor readAccessor : readAccessors ) {
|
||||
if ( isCollectionOrMap( readAccessor ) &&
|
||||
if ( isCollectionOrMapOrStream( readAccessor ) &&
|
||||
!correspondingSetterMethodExists( readAccessor, setterMethods ) ) {
|
||||
result.add( readAccessor );
|
||||
}
|
||||
@ -745,14 +768,21 @@ public class Type extends ModelElement implements Comparable<Type> {
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isCollectionOrMap(Accessor getterMethod) {
|
||||
return isCollection( getterMethod.getAccessedType() ) || isMap( getterMethod.getAccessedType() );
|
||||
private boolean isCollectionOrMapOrStream(Accessor getterMethod) {
|
||||
return isCollection( getterMethod.getAccessedType() ) || isMap( getterMethod.getAccessedType() ) ||
|
||||
isStream( getterMethod.getAccessedType() );
|
||||
}
|
||||
|
||||
private boolean isCollection(TypeMirror candidate) {
|
||||
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) {
|
||||
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