#3591 Fix duplicate method generation with recursive auto mapping

This commit is contained in:
Stefan Simon 2024-07-20 16:19:59 +02:00 committed by GitHub
parent df49ce5ff9
commit 3047760fd0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 416 additions and 24 deletions

View File

@ -5,6 +5,7 @@
*/
package org.mapstruct.ap.internal.model;
import java.util.function.Supplier;
import javax.lang.model.element.AnnotationMirror;
import org.mapstruct.ap.internal.model.common.Assignment;
@ -74,16 +75,9 @@ class AbstractBaseBuilder<B extends AbstractBaseBuilder<B>> {
*/
Assignment createForgedAssignment(SourceRHS sourceRHS, BuilderType builderType, ForgedMethod forgedMethod) {
if ( ctx.getForgedMethodsUnderCreation().containsKey( forgedMethod ) ) {
return createAssignment( sourceRHS, ctx.getForgedMethodsUnderCreation().get( forgedMethod ) );
}
else {
ctx.getForgedMethodsUnderCreation().put( forgedMethod, forgedMethod );
}
MappingMethod forgedMappingMethod;
Supplier<MappingMethod> forgedMappingMethodCreator;
if ( MappingMethodUtils.isEnumMapping( forgedMethod ) ) {
forgedMappingMethod = new ValueMappingMethod.Builder()
forgedMappingMethodCreator = () -> new ValueMappingMethod.Builder()
.method( forgedMethod )
.valueMappings( forgedMethod.getOptions().getValueMappings() )
.enumMapping( forgedMethod.getOptions().getEnumMappingOptions() )
@ -91,15 +85,31 @@ class AbstractBaseBuilder<B extends AbstractBaseBuilder<B>> {
.build();
}
else {
forgedMappingMethod = new BeanMappingMethod.Builder()
forgedMappingMethodCreator = () -> new BeanMappingMethod.Builder()
.forgedMethod( forgedMethod )
.returnTypeBuilder( builderType )
.mappingContext( ctx )
.build();
}
return getOrCreateForgedAssignment( sourceRHS, forgedMethod, forgedMappingMethodCreator );
}
Assignment getOrCreateForgedAssignment(SourceRHS sourceRHS, ForgedMethod forgedMethod,
Supplier<MappingMethod> mappingMethodCreator) {
if ( ctx.getForgedMethodsUnderCreation().containsKey( forgedMethod ) ) {
return createAssignment( sourceRHS, ctx.getForgedMethodsUnderCreation().get( forgedMethod ) );
}
else {
ctx.getForgedMethodsUnderCreation().put( forgedMethod, forgedMethod );
}
MappingMethod forgedMappingMethod = mappingMethodCreator.get();
Assignment forgedAssignment = createForgedAssignment( sourceRHS, forgedMethod, forgedMappingMethod );
ctx.getForgedMethodsUnderCreation().remove( forgedMethod );
return forgedAssignment;
}

View File

@ -46,7 +46,8 @@ public abstract class ContainerMappingMethod extends NormalTypeMappingMethod {
afterMappingReferences );
this.elementAssignment = parameterAssignment;
this.loopVariableName = loopVariableName;
this.selectionParameters = selectionParameters;
this.selectionParameters = selectionParameters != null ? selectionParameters : SelectionParameters.empty();
this.index1Name = Strings.getSafeVariableName( "i", existingVariables );
this.index2Name = Strings.getSafeVariableName( "j", existingVariables );

View File

@ -11,6 +11,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.Supplier;
import javax.lang.model.element.AnnotationMirror;
import org.mapstruct.ap.internal.gem.BuilderGem;
@ -746,7 +747,7 @@ public class PropertyMapping extends ModelElement {
targetType = targetType.withoutBounds();
ForgedMethod methodRef = prepareForgedMethod( sourceType, targetType, source, "[]" );
ContainerMappingMethod iterableMappingMethod = builder
Supplier<MappingMethod> mappingMethodCreator = () -> builder
.mappingContext( ctx )
.method( methodRef )
.selectionParameters( selectionParameters )
@ -754,7 +755,7 @@ public class PropertyMapping extends ModelElement {
.positionHint( positionHint )
.build();
return createForgedAssignment( source, methodRef, iterableMappingMethod );
return getOrCreateForgedAssignment( source, methodRef, mappingMethodCreator );
}
private ForgedMethod prepareForgedMethod(Type sourceType, Type targetType, SourceRHS source, String suffix) {
@ -772,12 +773,12 @@ public class PropertyMapping extends ModelElement {
ForgedMethod methodRef = prepareForgedMethod( sourceType, targetType, source, "{}" );
MapMappingMethod.Builder builder = new MapMappingMethod.Builder();
MapMappingMethod mapMappingMethod = builder
Supplier<MappingMethod> mapMappingMethodCreator = () -> builder
.mappingContext( ctx )
.method( methodRef )
.build();
return createForgedAssignment( source, methodRef, mapMappingMethod );
return getOrCreateForgedAssignment( source, methodRef, mapMappingMethodCreator );
}
private Assignment forgeMapping(SourceRHS sourceRHS) {

View File

@ -58,7 +58,7 @@ public class BeanMappingOptions extends DelegatingOptions {
public static BeanMappingOptions forForgedMethods(BeanMappingOptions beanMapping) {
BeanMappingOptions options = new BeanMappingOptions(
beanMapping.selectionParameters != null ?
SelectionParameters.withoutResultType( beanMapping.selectionParameters ) : null,
SelectionParameters.withoutResultType( beanMapping.selectionParameters ) : SelectionParameters.empty(),
Collections.emptyList(),
beanMapping.beanMapping,
beanMapping
@ -78,7 +78,7 @@ public class BeanMappingOptions extends DelegatingOptions {
}
public static BeanMappingOptions empty(DelegatingOptions delegatingOptions) {
return new BeanMappingOptions( null, Collections.emptyList(), null, delegatingOptions );
return new BeanMappingOptions( SelectionParameters.empty(), Collections.emptyList(), null, delegatingOptions );
}
public static BeanMappingOptions getInstanceOn(BeanMappingGem beanMapping, MapperOptions mapperOptions,

View File

@ -8,14 +8,14 @@ package org.mapstruct.ap.internal.model.source;
import java.util.Optional;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.ExecutableElement;
import org.mapstruct.ap.internal.util.ElementUtils;
import org.mapstruct.ap.internal.util.TypeUtils;
import org.mapstruct.ap.internal.model.common.FormattingParameters;
import org.mapstruct.ap.internal.gem.IterableMappingGem;
import org.mapstruct.ap.internal.gem.NullValueMappingStrategyGem;
import org.mapstruct.ap.internal.model.common.FormattingParameters;
import org.mapstruct.ap.internal.util.ElementUtils;
import org.mapstruct.ap.internal.util.FormattingMessager;
import org.mapstruct.ap.internal.util.Message;
import org.mapstruct.ap.internal.util.TypeUtils;
import org.mapstruct.tools.gem.GemValue;
/**
@ -34,7 +34,12 @@ public class IterableMappingOptions extends DelegatingOptions {
FormattingMessager messager, TypeUtils typeUtils) {
if ( iterableMapping == null || !isConsistent( iterableMapping, method, messager ) ) {
IterableMappingOptions options = new IterableMappingOptions( null, null, null, mapperOptions );
IterableMappingOptions options = new IterableMappingOptions(
null,
SelectionParameters.empty(),
null,
mapperOptions
);
return options;
}

View File

@ -38,9 +38,9 @@ public class MapMappingOptions extends DelegatingOptions {
if ( mapMapping == null || !isConsistent( mapMapping, method, messager ) ) {
MapMappingOptions options = new MapMappingOptions(
null,
SelectionParameters.empty(),
null,
null,
null,
SelectionParameters.empty(),
null,
mapperOptions
);

View File

@ -182,7 +182,7 @@ public class MappingOptions extends DelegatingOptions {
null,
true,
null,
null,
SelectionParameters.empty(),
Collections.emptySet(),
null,
null,

View File

@ -21,6 +21,16 @@ import org.mapstruct.ap.internal.model.common.SourceRHS;
*/
public class SelectionParameters {
private static final SelectionParameters EMPTY = new SelectionParameters(
Collections.emptyList(),
Collections.emptyList(),
Collections.emptyList(),
Collections.emptyList(),
null,
null,
null
);
private final List<TypeMirror> qualifiers;
private final List<String> qualifyingNames;
private final List<TypeMirror> conditionQualifiers;
@ -225,4 +235,9 @@ public class SelectionParameters {
sourceRHS
);
}
public static SelectionParameters empty() {
return EMPTY;
}
}

View File

@ -0,0 +1,36 @@
/*
* 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._3591;
import java.util.List;
public class Bean {
private List<Bean> beans;
private String value;
public Bean() {
}
public Bean(String value) {
this.value = value;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public List<Bean> getBeans() {
return beans;
}
public void setBeans(List<Bean> beans) {
this.beans = beans;
}
}

View File

@ -0,0 +1,30 @@
/*
* 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._3591;
import java.util.List;
public class BeanDto {
private List<BeanDto> beans;
private String value;
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public List<BeanDto> getBeans() {
return beans;
}
public void setBeans(List<BeanDto> beans) {
this.beans = beans;
}
}

View File

@ -0,0 +1,21 @@
/*
* 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._3591;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import org.mapstruct.factory.Mappers;
@Mapper
public interface BeanMapper {
BeanMapper INSTANCE = Mappers.getMapper( BeanMapper.class );
@Mapping(source = "beans", target = "beans")
BeanDto map(Bean bean, @MappingTarget BeanDto beanDto);
}

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.bugs._3591;
import java.util.Map;
import java.util.stream.Stream;
public class ContainerBean {
private String value;
private Map<String, ContainerBean> beanMap;
private Stream<ContainerBean> beanStream;
public ContainerBean() {
}
public ContainerBean(String value) {
this.value = value;
}
public Map<String, ContainerBean> getBeanMap() {
return beanMap;
}
public void setBeanMap(Map<String, ContainerBean> beanMap) {
this.beanMap = beanMap;
}
public Stream<ContainerBean> getBeanStream() {
return beanStream;
}
public void setBeanStream(Stream<ContainerBean> beanStream) {
this.beanStream = beanStream;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}

View File

@ -0,0 +1,40 @@
/*
* 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._3591;
import java.util.Map;
import java.util.stream.Stream;
public class ContainerBeanDto {
private String value;
private Map<String, ContainerBeanDto> beanMap;
private Stream<ContainerBeanDto> beanStream;
public Map<String, ContainerBeanDto> getBeanMap() {
return beanMap;
}
public void setBeanMap(Map<String, ContainerBeanDto> beanMap) {
this.beanMap = beanMap;
}
public Stream<ContainerBeanDto> getBeanStream() {
return beanStream;
}
public void setBeanStream(Stream<ContainerBeanDto> beanStream) {
this.beanStream = beanStream;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}

View File

@ -0,0 +1,22 @@
/*
* 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._3591;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import org.mapstruct.factory.Mappers;
@Mapper
public interface ContainerBeanMapper {
ContainerBeanMapper INSTANCE = Mappers.getMapper( ContainerBeanMapper.class );
@Mapping(source = "beanMap", target = "beanMap")
@Mapping(source = "beanStream", target = "beanStream")
ContainerBeanDto mapWithMapMapping(ContainerBean containerBean, @MappingTarget ContainerBeanDto containerBeanDto);
}

View File

@ -0,0 +1,79 @@
/*
* 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._3591;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.mapstruct.ap.testutil.IssueKey;
import org.mapstruct.ap.testutil.ProcessorTest;
import org.mapstruct.ap.testutil.WithClasses;
import org.mapstruct.ap.testutil.runner.GeneratedSource;
import static org.assertj.core.api.Assertions.assertThat;
@IssueKey("3591")
class Issue3591Test {
@RegisterExtension
GeneratedSource generatedSource = new GeneratedSource();
@ProcessorTest
@WithClasses({
BeanDto.class,
Bean.class,
BeanMapper.class
})
void mapNestedBeansWithMappingAnnotation() {
Bean bean = new Bean( "parent" );
Bean child = new Bean( "child" );
bean.setBeans( Collections.singletonList( child ) );
BeanDto beanDto = BeanMapper.INSTANCE.map( bean, new BeanDto() );
assertThat( beanDto ).isNotNull();
assertThat( beanDto.getValue() ).isEqualTo( "parent" );
assertThat( beanDto.getBeans() )
.extracting( BeanDto::getValue )
.containsExactly( "child" );
}
@ProcessorTest
@WithClasses({
ContainerBean.class,
ContainerBeanDto.class,
ContainerBeanMapper.class,
})
void shouldMapNestedMapAndStream() {
generatedSource.addComparisonToFixtureFor( ContainerBeanMapper.class );
ContainerBean containerBean = new ContainerBean( "parent" );
Map<String, ContainerBean> beanMap = new HashMap<>();
beanMap.put( "child", new ContainerBean( "mapChild" ) );
containerBean.setBeanMap( beanMap );
Stream<ContainerBean> streamChild = Stream.of( new ContainerBean( "streamChild" ) );
containerBean.setBeanStream( streamChild );
ContainerBeanDto dto = ContainerBeanMapper.INSTANCE.mapWithMapMapping( containerBean, new ContainerBeanDto() );
assertThat( dto ).isNotNull();
assertThat( dto.getBeanMap() )
.extractingByKey( "child" )
.extracting( ContainerBeanDto::getValue )
.isEqualTo( "mapChild" );
assertThat( dto.getBeanStream() )
.singleElement()
.extracting( ContainerBeanDto::getValue )
.isEqualTo( "streamChild" );
}
}

View File

@ -0,0 +1,85 @@
/*
* 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._3591;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Stream;
import javax.annotation.processing.Generated;
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2024-05-25T14:23:23+0200",
comments = "version: , compiler: javac, environment: Java 17.0.11 (N/A)"
)
public class ContainerBeanMapperImpl implements ContainerBeanMapper {
@Override
public ContainerBeanDto mapWithMapMapping(ContainerBean containerBean, ContainerBeanDto containerBeanDto) {
if ( containerBean == null ) {
return containerBeanDto;
}
if ( containerBeanDto.getBeanMap() != null ) {
Map<String, ContainerBeanDto> map = stringContainerBeanMapToStringContainerBeanDtoMap( containerBean.getBeanMap() );
if ( map != null ) {
containerBeanDto.getBeanMap().clear();
containerBeanDto.getBeanMap().putAll( map );
}
else {
containerBeanDto.setBeanMap( null );
}
}
else {
Map<String, ContainerBeanDto> map = stringContainerBeanMapToStringContainerBeanDtoMap( containerBean.getBeanMap() );
if ( map != null ) {
containerBeanDto.setBeanMap( map );
}
}
containerBeanDto.setBeanStream( containerBeanStreamToContainerBeanDtoStream( containerBean.getBeanStream() ) );
containerBeanDto.setValue( containerBean.getValue() );
return containerBeanDto;
}
protected Stream<ContainerBeanDto> containerBeanStreamToContainerBeanDtoStream(Stream<ContainerBean> stream) {
if ( stream == null ) {
return null;
}
return stream.map( containerBean -> containerBeanToContainerBeanDto( containerBean ) );
}
protected ContainerBeanDto containerBeanToContainerBeanDto(ContainerBean containerBean) {
if ( containerBean == null ) {
return null;
}
ContainerBeanDto containerBeanDto = new ContainerBeanDto();
containerBeanDto.setBeanMap( stringContainerBeanMapToStringContainerBeanDtoMap( containerBean.getBeanMap() ) );
containerBeanDto.setBeanStream( containerBeanStreamToContainerBeanDtoStream( containerBean.getBeanStream() ) );
containerBeanDto.setValue( containerBean.getValue() );
return containerBeanDto;
}
protected Map<String, ContainerBeanDto> stringContainerBeanMapToStringContainerBeanDtoMap(Map<String, ContainerBean> map) {
if ( map == null ) {
return null;
}
Map<String, ContainerBeanDto> map1 = new LinkedHashMap<String, ContainerBeanDto>( Math.max( (int) ( map.size() / .75f ) + 1, 16 ) );
for ( java.util.Map.Entry<String, ContainerBean> entry : map.entrySet() ) {
String key = entry.getKey();
ContainerBeanDto value = containerBeanToContainerBeanDto( entry.getValue() );
map1.put( key, value );
}
return map1;
}
}