#2250, #2324 Do not throw error when qualifier is missing on iterable mapping

With this change we are relaxing the error handling when qualifiers are used in `@Mapping`
and we allow defining qualifiers for collections / array mapping to mean the same as if it was defined on `@IterableMapping`
i.e. apply the qualifier on the element mapping
This commit is contained in:
Filip Hrisafov 2021-01-16 19:43:22 +01:00
parent 8478a5455b
commit 0d8bbacc53
10 changed files with 203 additions and 20 deletions

View File

@ -22,6 +22,8 @@ import org.mapstruct.ap.internal.util.Strings;
import static org.mapstruct.ap.internal.util.Collections.first;
import javax.lang.model.element.AnnotationMirror;
/**
* Builder that can be used to build {@link ContainerMappingMethod}(s).
*
@ -37,6 +39,7 @@ public abstract class ContainerMappingMethodBuilder<B extends ContainerMappingMe
private FormattingParameters formattingParameters;
private String errorMessagePart;
private String callingContextTargetPropertyName;
private AnnotationMirror positionHint;
ContainerMappingMethodBuilder(Class<B> selfType, String errorMessagePart) {
super( selfType );
@ -58,6 +61,11 @@ public abstract class ContainerMappingMethodBuilder<B extends ContainerMappingMe
return myself;
}
public B positionHint(AnnotationMirror positionHint) {
this.positionHint = positionHint;
return myself;
}
@Override
public final M build() {
Type sourceParameterType = first( method.getSourceParameters() ).getType();
@ -88,7 +96,7 @@ public abstract class ContainerMappingMethodBuilder<B extends ContainerMappingMe
formattingParameters,
criteria,
sourceRHS,
null,
positionHint,
() -> forge( sourceRHS, sourceElementType, targetElementType )
);

View File

@ -656,6 +656,7 @@ public class PropertyMapping extends ModelElement {
.method( methodRef )
.selectionParameters( selectionParameters )
.callingContextTargetPropertyName( targetPropertyName )
.positionHint( positionHint )
.build();
return createForgedAssignment( source, methodRef, iterableMappingMethod );

View File

@ -298,8 +298,14 @@ public class MappingResolverImpl implements MappingResolver {
}
if ( hasQualfiers() ) {
if ((sourceType.isCollectionType() || sourceType.isArrayType()) && targetType.isIterableType()) {
// Allow forging iterable mapping when no iterable mapping already found
return forger.get();
}
else {
printQualifierMessage( selectionCriteria );
}
}
else if ( allowMappingMethod() ) {
// only forge if we would allow mapping method
return forger.get();

View File

@ -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.selection.qualifier.errors;
import java.util.Collection;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper
public interface ErroneousMessageByNamedWithIterableMapper {
@Mapping(target = "elements", qualifiedByName = "SelectMe")
Target map(Source source);
// CHECKSTYLE:OFF
class Source {
public Collection<String> elements;
}
class Target {
public Collection<String> elements;
}
// CHECKSTYLE ON
}

View File

@ -87,4 +87,27 @@ public class QualfierMessageTest {
)
public void testNoQualifyingMethodByAnnotationAndNamedFound() {
}
@Test
@WithClasses(ErroneousMessageByNamedWithIterableMapper.class)
@ExpectedCompilationOutcome(
value = CompilationResult.FAILED,
diagnostics = {
@Diagnostic(
type = ErroneousMessageByNamedWithIterableMapper.class,
kind = ERROR,
line = 16,
message = "Qualifier error. No method found annotated with @Named#value: [ SelectMe ]. " +
"See https://mapstruct.org/faq/#qualifier for more info."),
@Diagnostic(
type = ErroneousMessageByNamedWithIterableMapper.class,
kind = ERROR,
line = 16,
messageRegExp = "Can't map property.*"),
}
)
public void testNoQualifyingMethodByNamedForForgedIterableFound() {
}
}

View File

@ -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.selection.qualifier.iterable;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.mapstruct.Qualifier;
/**
* @author Filip Hrisafov
*/
@Qualifier
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface Cities {
}

View File

@ -15,6 +15,7 @@ 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 org.mapstruct.factory.Mappers;
/**
*
@ -22,20 +23,22 @@ import org.mapstruct.ap.testutil.runner.AnnotationProcessorTestRunner;
*/
@IssueKey( "707" )
@WithClasses( {
Cities.class,
CityDto.class,
CityEntity.class,
RiverDto.class,
RiverEntity.class,
Rivers.class,
TopologyDto.class,
TopologyEntity.class,
TopologyFeatureDto.class,
TopologyFeatureEntity.class,
TopologyMapper.class
} )
@RunWith( AnnotationProcessorTestRunner.class )
public class IterableAndQualifiersTest {
@Test
@WithClasses(TopologyMapper.class)
public void testGenerationBasedOnQualifier() {
TopologyDto topologyDto1 = new TopologyDto();
@ -67,4 +70,39 @@ public class IterableAndQualifiersTest {
assertThat( ( (CityEntity) result2.getTopologyFeatures().get( 0 ) ).getPopulation() ).isEqualTo( 800000 );
}
@Test
@WithClasses(TopologyWithoutIterableMappingMapper.class)
public void testIterableGeneratorBasedOnQualifier() {
TopologyWithoutIterableMappingMapper mapper = Mappers.getMapper( TopologyWithoutIterableMappingMapper.class );
TopologyDto riverTopologyDto = new TopologyDto();
List<TopologyFeatureDto> topologyFeatures1 = new ArrayList<>();
RiverDto riverDto = new RiverDto();
riverDto.setName( "Rhine" );
riverDto.setLength( 5 );
topologyFeatures1.add( riverDto );
riverTopologyDto.setTopologyFeatures( topologyFeatures1 );
TopologyEntity riverTopology = mapper.mapTopologyAsRiver( riverTopologyDto );
assertThat( riverTopology.getTopologyFeatures() ).hasSize( 1 );
assertThat( riverTopology.getTopologyFeatures().get( 0 ).getName() ).isEqualTo( "Rhine" );
assertThat( riverTopology.getTopologyFeatures().get( 0 ) ).isInstanceOf( RiverEntity.class );
assertThat( ( (RiverEntity) riverTopology.getTopologyFeatures().get( 0 ) ).getLength() ).isEqualTo( 5 );
TopologyDto cityTopologyDto = new TopologyDto();
List<TopologyFeatureDto> topologyFeatures2 = new ArrayList<>();
CityDto cityDto = new CityDto();
cityDto.setName( "Amsterdam" );
cityDto.setPopulation( 800000 );
topologyFeatures2.add( cityDto );
cityTopologyDto.setTopologyFeatures( topologyFeatures2 );
TopologyEntity cityTopology = mapper.mapTopologyAsCity( cityTopologyDto );
assertThat( cityTopology.getTopologyFeatures() ).hasSize( 1 );
assertThat( cityTopology.getTopologyFeatures().get( 0 ).getName() ).isEqualTo( "Amsterdam" );
assertThat( cityTopology.getTopologyFeatures().get( 0 ) ).isInstanceOf( CityEntity.class );
assertThat( ( (CityEntity) cityTopology.getTopologyFeatures().get( 0 ) ).getPopulation() ).isEqualTo( 800000 );
}
}

View File

@ -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.selection.qualifier.iterable;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.mapstruct.Qualifier;
/**
* @author Filip Hrisafov
*/
@Qualifier
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface Rivers {
}

View File

@ -5,16 +5,11 @@
*/
package org.mapstruct.ap.test.selection.qualifier.iterable;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.List;
import org.mapstruct.IterableMapping;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Qualifier;
import org.mapstruct.factory.Mappers;
/**
@ -64,16 +59,4 @@ public abstract class TopologyMapper {
return topologyFeatureEntity;
}
@Qualifier
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface Rivers {
}
@Qualifier
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface Cities {
}
}

View File

@ -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.selection.qualifier.iterable;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
/**
* @author Filip Hrisafov
*/
@Mapper
public interface TopologyWithoutIterableMappingMapper {
@Mapping( target = "topologyFeatures", qualifiedBy = Rivers.class )
TopologyEntity mapTopologyAsRiver(TopologyDto dto);
@Mapping( target = "topologyFeatures", qualifiedBy = Cities.class )
TopologyEntity mapTopologyAsCity(TopologyDto dto);
@Rivers
default TopologyFeatureEntity mapRiver( TopologyFeatureDto dto ) {
TopologyFeatureEntity topologyFeatureEntity = null;
if ( dto instanceof RiverDto ) {
RiverEntity riverEntity = new RiverEntity();
riverEntity.setLength( ( (RiverDto) dto ).getLength() );
topologyFeatureEntity = riverEntity;
topologyFeatureEntity.setName( dto.getName() );
}
return topologyFeatureEntity;
}
@Cities
default TopologyFeatureEntity mapCity( TopologyFeatureDto dto ) {
TopologyFeatureEntity topologyFeatureEntity = null;
if ( dto instanceof CityDto ) {
CityEntity cityEntity = new CityEntity();
cityEntity.setPopulation( ( (CityDto) dto ).getPopulation() );
topologyFeatureEntity = cityEntity;
topologyFeatureEntity.setName( dto.getName() );
}
return topologyFeatureEntity;
}
}