Enable run-time "filtering" based also on property name in @Conditional · mapstruct/mapstruct · Discussion #2831
Hi, all!
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 while reusing the mapping code.
I have implemented this feature with additional (@TargetPropertyName) annotated parameter in conditional / presence check methods.
I would like to make a pull request if this feature can be accepted.
For instance:
@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(); } }
Or even more complex example with passed @Context method parameter where run-time decisions based on target property name could be made:
@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 } } }
And more interesting / real-life example where glob like pattern filtering could be applied:
interface Entity {} interface Dto {} class Employee implements Entity { //default constructor, getters and setters ommited String name; Employee boss; Address address; List<Employee> subordinates; String title; } class Address implements Entity { //default constructor, getters and setters ommited String street; } class EmployeeDto implements Dto { //default constructor, getters and setters 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, getters and setters ommited String street; public AddressDto(String street) { this.street = street; } } class MapContext { Set<String> ignoreProps = new HashSet<>(); Deque<String> path = new LinkedList<>(); Deque<String> visited = new LinkedList<>(); String lastProp = null; @Condition public boolean collectAndFilter(@TargetPropertyName String propName) { if ( ignoreProps.contains( propName ) ) { return false; } this.lastProp = propName; this.path.offerLast( this.lastProp ); this.visited.offerLast( String.join( ".", this.path ) ); this.path.pollLast(); 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 (ctx.lastProp != null && dto != null) { ctx.path.offerLast( ctx.lastProp ); } } @AfterMapping default void after(Object dto, @Context MapContext ctx) { ctx.path.pollLast(); ctx.lastProp = ctx.path.peekLast(); } } @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 main(String args[]) { EmployeeMapper mapper = EmployeeMapper.INSTANCE; MapContext context = new MapContext(); context.ignoreProps.add( "boss" ); //could be extended to glob like features 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 ); System.out.printf( "Visited property name paths: \n%s\n", String.join( ", \n", context.visited ) ); //Visited property name paths: //name, //address, //address.street, //subordinates, //subordinates.subordinates.name, //subordinates.subordinates.address, //subordinates.subordinates.address.street, //subordinates.subordinates.subordinates, //subordinates.subordinates.title, //subordinates.subordinates.name, //subordinates.subordinates.address, //subordinates.subordinates.address.street, //subordinates.subordinates.subordinates, //subordinates.subordinates.title, //subordinates.subordinates.name, //subordinates.subordinates.address, //subordinates.subordinates.address.street, //subordinates.subordinates.subordinates, //subordinates.subordinates.title, //title }