Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NEXT_RELEASE_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### Bugs

* Fix `@InheritInverseConfiguration` ignoring `ignore = true` for nested properties when the forward mapping uses a source path that includes the source parameter name (e.g. `source.client.id`) (#3997)

### Documentation

### Build
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -880,6 +880,37 @@ public ReadAccessor getReadAccessor(String propertyName, boolean allowedMapToBea
return readAccessors.get( propertyName );
}

/**
* Whether each segment of {@code propertyNames} resolves as a nested read-accessor path on this type.
* Aligns with {@code SourceReference} matching of a full property path.
*/
public boolean hasReadPropertyPath(String[] propertyNames) {
if ( propertyNames == null || propertyNames.length == 0 ) {
return false;
}
Type current = this;
for ( int i = 0; i < propertyNames.length; i++ ) {
Type noBoundsType = current.withoutBounds();
if ( noBoundsType.isOptionalType() ) {
noBoundsType = noBoundsType.getOptionalBaseType();
}
if ( !( noBoundsType.getTypeMirror() instanceof DeclaredType ) ) {
return false;
}
ReadAccessor readAccessor = noBoundsType.getReadAccessor( propertyNames[i], true );
if ( readAccessor == null ) {
return false;
}
if ( i < propertyNames.length - 1 ) {
current = typeFactory.getReturnType( (DeclaredType) noBoundsType.getTypeMirror(), readAccessor );
if ( current == null ) {
return false;
}
}
}
return true;
}

public PresenceCheckAccessor getPresenceChecker(String propertyName) {
if ( hasStringMapSignature() ) {
return PresenceCheckAccessor.mapContainsKey( propertyName );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@
import org.mapstruct.ap.internal.gem.NullValueCheckStrategyGem;
import org.mapstruct.ap.internal.gem.NullValuePropertyMappingStrategyGem;
import org.mapstruct.ap.internal.model.common.FormattingParameters;
import org.mapstruct.ap.internal.model.common.Parameter;
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;

import static org.mapstruct.ap.internal.util.Collections.first;

/**
* Represents a property mapping as configured via {@code @Mapping} (no intermediate state).
*
Expand Down Expand Up @@ -493,8 +496,15 @@ public boolean canInverse() {
public MappingOptions copyForInverseInheritance(SourceMethod templateMethod,
BeanMappingOptions beanMappingOptions ) {

// When the original source path includes the source parameter name (e.g. "source.client.id"),
// strip that prefix so the inverse target is the property path ("client.id"). Otherwise
// ignore/redefinition matching fails on the raw target name (#3997).
String inverseTargetName = sourceName != null
? stripSourceParameterPrefix( sourceName, templateMethod )
: targetName;

MappingOptions mappingOptions = new MappingOptions(
sourceName != null ? sourceName : targetName,
inverseTargetName,
templateMethod.getExecutable(),
targetAnnotationValue,
sourceName != null ? targetName : null,
Expand All @@ -516,6 +526,39 @@ public MappingOptions copyForInverseInheritance(SourceMethod templateMethod,

}

/**
* Strips a source-parameter-name prefix from a source path when forming the inverse target.
* <p>
* With a single source parameter, {@code source = "param.prop.nested"} may use {@code param}
* only as the parameter name. On inverse that must become target {@code "prop.nested"}.
* </p>
* <p>
* Matches {@code SourceReference.BuilderFromMapping#buildFromSingleSourceParameters}: try the
* full path as property accessors first; strip the parameter name only when that full path
* does not resolve. A first segment that merely happens to be a property name is not enough
* to keep the path (e.g. parameter {@code client} and path {@code client.id} when {@code Client}
* has no {@code id}).
* </p>
*/
private static String stripSourceParameterPrefix(String originalSourceName, SourceMethod templateMethod) {
Parameter sourceParameter = first( templateMethod.getSourceParameters() );
String parameterName = sourceParameter.getName();
String prefix = parameterName + ".";

if ( !originalSourceName.startsWith( prefix ) ) {
return originalSourceName;
}

String[] segments = originalSourceName.split( "\\." );
// Keep the path only when the entire path resolves as properties on the source type
// (same question SourceReference asks before stripping the parameter name).
if ( sourceParameter.getType().hasReadPropertyPath( segments ) ) {
return originalSourceName;
}

return originalSourceName.substring( prefix.length() );
}

/**
* Creates a copy of this mapping
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* 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._3997;

import org.mapstruct.InheritInverseConfiguration;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;

/**
* @author Kamil Krzywanski
*/
@Mapper
public interface Issue3997Mapper {

Issue3997Mapper INSTANCE = Mappers.getMapper( Issue3997Mapper.class );

@Mapping(target = "clientId", source = "source.client.id")
@Mapping(target = "name", source = "source.name")
@Mapping(target = "city", source = "source.address.city")
OrderDto toDto(Order source);

@InheritInverseConfiguration
@Mapping(target = "client", ignore = true)
@Mapping(target = "name", ignore = true)
@Mapping(target = "address", ignore = true)
Order toEntity(OrderDto dto);

class Order {
private Long id;
private Client client;
private String name;
private Address address;

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public Client getClient() {
return client;
}

public void setClient(Client client) {
this.client = client;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Address getAddress() {
return address;
}

public void setAddress(Address address) {
this.address = address;
}
}

class OrderDto {
private Long id;
private Long clientId;
private String name;
private String city;

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public Long getClientId() {
return clientId;
}

public void setClientId(Long clientId) {
this.clientId = clientId;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getCity() {
return city;
}

public void setCity(String city) {
this.city = city;
}
}

class Client {
private Long id;

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}
}

class Address {
private String city;

public String getCity() {
return city;
}

public void setCity(String city) {
this.city = city;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* 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._3997;

import org.mapstruct.InheritInverseConfiguration;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.ap.test.bugs._3997.Issue3997Mapper.Order;
import org.mapstruct.ap.test.bugs._3997.Issue3997Mapper.OrderDto;
import org.mapstruct.factory.Mappers;

/**
* Same nested source paths as {@link Issue3997Mapper}, but without inverse ignores —
* ensures parameter-prefix stripping does not break nested reverse mapping.
*
* @author Kamil Krzywanski
*/
@Mapper
public interface Issue3997MapperWithoutIgnores {

Issue3997MapperWithoutIgnores INSTANCE = Mappers.getMapper( Issue3997MapperWithoutIgnores.class );

@Mapping(target = "clientId", source = "source.client.id")
@Mapping(target = "name", source = "source.name")
@Mapping(target = "city", source = "source.address.city")
OrderDto toDto(Order source);

@InheritInverseConfiguration
Order toEntity(OrderDto dto);
}
Original file line number Diff line number Diff line change
@@ -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._3997;

import org.mapstruct.InheritInverseConfiguration;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;

/**
* Parameter name collides with a source property, but the rest of the path is not on that property.
* Forward mapping strips the parameter name; inverse must strip too so ignore on {@code client} works
* for other nested targets, and {@code id} is still mapped.
*
* @author Kamil Krzywanski
*/
@Mapper
public interface Issue3997ParameterNameCollidesWithPropertyMapper {

Issue3997ParameterNameCollidesWithPropertyMapper INSTANCE =
Mappers.getMapper( Issue3997ParameterNameCollidesWithPropertyMapper.class );

// parameter name "client" also exists as Order.getClient(), but Client has no id —
// SourceReference strips and maps Order.getId() → dtoId
@Mapping(target = "dtoId", source = "client.id")
OrderDto toDto(Order client);

@InheritInverseConfiguration
@Mapping(target = "client", ignore = true)
Order toEntity(OrderDto dto);

class Order {
private Long id;
private Client client;

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public Client getClient() {
return client;
}

public void setClient(Client client) {
this.client = client;
}
}

class OrderDto {
private Long dtoId;

public Long getDtoId() {
return dtoId;
}

public void setDtoId(Long dtoId) {
this.dtoId = dtoId;
}
}

class Client {
private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}
}
Loading