#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:
Sebastian 2018-09-24 22:23:34 +02:00 committed by Filip Hrisafov
parent 5a4990c474
commit 30c2dadec7
9 changed files with 276 additions and 32 deletions

View File

@ -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 );

View File

@ -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;
}
}

View File

@ -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;
}
/**

View File

@ -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 );
}

View File

@ -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>

View File

@ -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);
}

View File

@ -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" );
}
}

View File

@ -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 );
}
}

View File

@ -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 );
}
}