#2688: Access to the target property name by ivlcic · Pull Request #2834 · mapstruct/mapstruct
Implemented
Access to the target property name #2688
As agreed in discussion: #2831
Rationale
Currently there is no option to do run-time filtering based on target property name.
This comes very handy when mapping larger object graphs from ORM frameworks, to block
unnecessary loading from database and reuse mapping code.
With this feature you can for instance block further mapping in runtime based on some @Context variable state and reuse mapper code without having to foresee every possible property mapping combination.
Implementation
Implementation introduces support for new presence check method parameter annotated with @TargetPropertyName.
It is implemented with minimum impact to current code base, so it might be in architecture/elegance sense problematic.
It introduces no changes any current features; it's only an addition.
Unit tests are provided in org/mapstruct/ap/test/conditional.
Example 1
@Mapper public interface MyMapper { MyMapper INSTANCE = Mappers.getMapper( MyMapper.class ); Employee map(EmployeeDto employee); @Condition default <T> boolean isNotEmpty(Collection<T> collection, @TargetPropertyName String propName) { if ( "addresses".equalsIgnoreCase( propName ) ) { return false; } return collection != null && !collection.isEmpty(); } @Condition default boolean isNotBlank(String value, @TargetPropertyName String propName) { if ( propName.equalsIgnoreCase( "lastName" ) ) { return false; } return value != null && !value.trim().isEmpty(); } }
Example 2
@Mapper public interface MyMapper { MyMapper INSTANCE = Mappers.getMapper( MyMapper.class ); Employee map(EmployeeDto employee, @Context PresenceUtils utils); Address map(AddressDto addressDto, @Context PresenceUtils utils); class PresenceUtils { Set<String> visited = new LinkedHashSet<>(); @Condition public boolean decide(String value, @TargetPropertyName String propName) { return true; //decide if continue mapping } } }
Complex Example
interface Entity {} interface Dto {} class Employee implements Entity { // setters and getters ommited String name; Employee boss; Address address; List<Employee> subordinates; String title; } class Address implements Entity { // setters and getters ommited String street; } class EmployeeDto implements Dto { // default constructor, setters and getters ommited String name; EmployeeDto boss; AddressDto address; List<EmployeeDto> subordinates; String title; public EmployeeDto(String name, EmployeeDto boss, AddressDto address) { this.name = name; this.boss = boss; this.address = address; } public EmployeeDto(String name, AddressDto address, List<EmployeeDto> subordinates) { this.name = name; this.address = address; this.subordinates = subordinates; } } class AddressDto implements Dto { // default constructor, setters and getters ommited String street; public AddressDto(String street) { this.street = street; } } class PathSegment { String name; boolean cycleDetected; public PathSegment(String name, boolean cycleDetected) { this.name = name; this.cycleDetected = cycleDetected; } } class CollectionPathSegment extends PathSegment { int index; public CollectionPathSegment(String name, boolean cycleDetected) { super( name, cycleDetected ); } } class CollectionElementPathSegment extends PathSegment { int index; public CollectionElementPathSegment(String name, int index, boolean cycleDetected) { super( name, cycleDetected ); this.index = index; } } class MapContext { Set<String> ignoreProps = new HashSet<>(); Set<Integer> mapped = new HashSet<>(); Deque<PathSegment> path = new LinkedList<>(); Deque<String> visited = new LinkedList<>(); String lastProp = null; private String pathToString() { StringBuilder strPath = new StringBuilder(); for ( PathSegment segment : path ) { if ( segment instanceof CollectionElementPathSegment ) { strPath.append( segment.name ); } else { if ( strPath.length() > 0 ) { strPath.append( "." ); } strPath.append( segment.name ); } } if (path.isEmpty()) { return lastProp; } strPath.append( "." ); strPath.append( lastProp ); return strPath.toString(); } @Condition public boolean collect(@TargetPropertyName String propName) { PathSegment segment = path.peekLast(); if ( ignoreProps.contains( propName ) || (segment != null && segment.cycleDetected)) { return false; } this.lastProp = propName; this.visited.offerLast( pathToString() ); return true; } } interface FirmMapper<E extends Entity, D extends Dto> { E map(D dto, @Context MapContext ctx); @BeforeMapping default void before(Object dto, @Context MapContext ctx) { if ( dto == null ) { return; } boolean cycleDetected = !ctx.mapped.add( dto.hashCode() ); if ( ctx.lastProp == null ) { return; } if ( dto instanceof Collection ) { ctx.path.offerLast( new CollectionPathSegment( ctx.lastProp, cycleDetected ) ); } else { PathSegment segment = ctx.path.peekLast(); if ( segment instanceof CollectionPathSegment ) { ctx.path.offerLast( new CollectionElementPathSegment( "[" + ( (CollectionPathSegment) segment ).index + "]", ( (CollectionPathSegment) segment ).index, cycleDetected ) ); ( (CollectionPathSegment) segment ).index++; } else { ctx.path.offerLast( new PathSegment( ctx.lastProp, cycleDetected ) ); } } } @AfterMapping default void after(Object dto, @Context MapContext ctx) { ctx.path.pollLast(); PathSegment segment = ctx.path.peekLast(); if ( segment != null ) { ctx.lastProp = segment.name; } else { ctx.lastProp = null; } } } @Mapper interface EmployeeMapper extends FirmMapper<Employee, EmployeeDto> { EmployeeMapper INSTANCE = Mappers.getMapper( EmployeeMapper.class ); } @Mapper interface AddressMapper extends FirmMapper<Address, AddressDto> { AddressMapper INSTANCE = Mappers.getMapper( AddressMapper.class ); } // ... more mappers public void test) { EmployeeMapper mapper = EmployeeMapper.INSTANCE; MapContext context = new MapContext(); //context.ignoreProps.add( "boss" ); // glob - like exclude / include could be implemented EmployeeDto bossDto = new EmployeeDto( "Boss", new AddressDto("Testing st. 18"), new ArrayList<>() ); bossDto.subordinates.add( new EmployeeDto( "Employee1", bossDto, new AddressDto("Street 1") ) ); bossDto.subordinates.add( new EmployeeDto( "Employee2", bossDto, new AddressDto("Street 2") ) ); bossDto.subordinates.add( new EmployeeDto( "Employee3", bossDto, new AddressDto("Street 3") ) ); Employee boss = mapper.map( bossDto, context ); assertThat( List.of( "name", "boss", "address", "address.street", "subordinates", "subordinates[0].name", "subordinates[0].boss", "subordinates[0].address", "subordinates[0].address.street", "subordinates[0].subordinates", "subordinates[0].title", "subordinates[1].name", "subordinates[1].boss", "subordinates[1].address", "subordinates[1].address.street", "subordinates[1].subordinates", "subordinates[1].title", "subordinates[2].name", "subordinates[2].boss", "subordinates[2].address", "subordinates[2].address.street", "subordinates[2].subordinates", "subordinates[2].title", "title" ) ).isEqualTo( context.visited ); }