Sample Objects
public class Source {
String fromPersonFirstName;
String fromPersonLastName;
String toPersonFirstName;
String toPersonLastName;
String message;
}
public class Target {
Person from;
Person to;
String message;
}
public class Person {
String firstName;
String lastName;
}
#Mapper(componentModel = "spring")
public interface PersonMapper {
Person toPerson(String firstName, String lastName);
}
The Question
Now, what would be a clean way of employing as much as mapstruct as possible? Without using expressions?
Ideally I'd tell mapstruct to do the following
#Mapper(componentModel = "spring", uses = PersonMapper.class)
public interface ExampleMapper {
#Mapping(target = "from", source = "fromPersonFirstName, fromPersonLastName")
#Mapping(target = "to", source = "toPersonFirstName, toPersonLastName")
Target toTarget(Source s);
}
Alternatively I could annotate PersonMapper and toPerson, then for the toTarget method qualifiedByName = "TheClassNameValue, TheMethodNameValue". But I still don't know how to tell the method which source fields to use.
What would be another option? I could use a local method toPerson, qualifiedByName. But then I'd have to import the other mapper in some way. Maybe change the ExampleMapper to abstract and autowire the other mapper. But I still can't specify multiple fields.
I know that expression is probably meant to fill in this gap. But ... I would rather never use it if possible. Unless the alternative is considerably more complicated.
Solution 1, Mapping method selection based on qualifiers
#Mapper
public interface ExampleMapper {
#Mapping(target = "from", source = "source", qualifiedByName = "fromPersonMapping")
#Mapping(target = "to", source = "source", qualifiedByName = "toPersonMapping")
Target toTarget(Source source);
#Named("fromPersonMapping")
#Mapping(target = "firstName", source = "fromPersonFirstName")
#Mapping(target = "lastName", source = "fromPersonLastName")
Person fromPersonMapping(Source source);
#Named("toPersonMapping")
#Mapping(target = "firstName", source = "toPersonFirstName")
#Mapping(target = "lastName", source = "toPersonLastName")
Person toPersonMapping(Source source);
}
Generated code:
public class ExampleMapperImpl implements ExampleMapper {
#Override
public Target toTarget(Source source) {
if ( source == null ) {
return null;
}
Target target = new Target();
target.from = fromPersonMapping( source );
target.to = toPersonMapping( source );
target.message = source.message;
return target;
}
#Override
public Person fromPersonMapping(Source source) {
if ( source == null ) {
return null;
}
Person person = new Person();
person.firstName = source.fromPersonFirstName;
person.lastName = source.fromPersonLastName;
return person;
}
#Override
public Person toPersonMapping(Source source) {
if ( source == null ) {
return null;
}
Person person = new Person();
person.firstName = source.toPersonFirstName;
person.lastName = source.toPersonLastName;
return person;
}
}
Alternative with custom methods:
#Mapper
public interface ExampleMapper {
#Mapping(target = "from", source = "source", qualifiedByName = "fromFieldsMapping")
#Mapping(target = "to", source = "source", qualifiedByName = "toFieldsMapping")
Target toTarget(Source source);
Person toPerson(String firstName, String lastName);
#Named("fromFieldsMapping")
default Person fromFieldsMapping(Source source) {
return toPerson(source.fromPersonFirstName, source.fromPersonLastName);
}
#Named("toFieldsMapping")
default Person toFieldsMapping(Source source) {
return toPerson(source.toPersonFirstName, source.toPersonLastName);
}
}
Generated code:
public class ExampleMapperImpl implements ExampleMapper {
#Override
public Target toTarget(Source source) {
if ( source == null ) {
return null;
}
Target target = new Target();
target.from = fromFieldsMapping( source );
target.to = toFieldsMapping( source );
target.message = source.message;
return target;
}
#Override
public Person toPerson(String firstName, String lastName) {
if ( firstName == null && lastName == null ) {
return null;
}
Person person = new Person();
if ( firstName != null ) {
person.firstName = firstName;
}
if ( lastName != null ) {
person.lastName = lastName;
}
return person;
}
}
With separate mapper:
#Mapper(uses = PersonMapper.class)
public interface ExampleMapper1 {
#Mapping(target = "from", source = "source", qualifiedBy = FromAnnotation.class)
#Mapping(target = "to", source = "source", qualifiedBy = ToAnnotation.class)
Target toTarget(Source source);
}
#Mapper
public interface PersonMapper {
Person toPerson(String firstName, String lastName);
#FromAnnotation
default Person fromFieldsMapping(Source source) {
return toPerson(source.fromPersonFirstName, source.fromPersonLastName);
}
#ToAnnotation
default Person toFieldsMapping(Source source) {
return toPerson(source.toPersonFirstName, source.toPersonLastName);
}
}
#Qualifier
#java.lang.annotation.Target(ElementType.METHOD)
#Retention(RetentionPolicy.CLASS)
public #interface ToAnnotation {
}
#Qualifier
#java.lang.annotation.Target(ElementType.METHOD)
#Retention(RetentionPolicy.CLASS)
public #interface FromAnnotation {
}
Solution 2, Expression
#Mapper
public interface ExampleMapper {
#Mapping(target = "from", expression = "java(toPerson(source.fromPersonFirstName, source.fromPersonLastName))")
#Mapping(target = "to", expression = "java(toPerson(source.toPersonFirstName, source.toPersonLastName))")
Target toTarget(Source source);
Person toPerson(String firstName, String lastName);
}
Generated code:
#Override
public Target toTarget(Source source) {
if ( source == null ) {
return null;
}
Target target = new Target();
target.message = source.message;
target.from = toPerson(source.fromPersonFirstName, source.fromPersonLastName);
target.to = toPerson(source.toPersonFirstName, source.toPersonLastName);
return target;
}
#Override
public Person toPerson(String firstName, String lastName) {
if ( firstName == null && lastName == null ) {
return null;
}
Person person = new Person();
if ( firstName != null ) {
person.firstName = firstName;
}
if ( lastName != null ) {
person.lastName = lastName;
}
return person;
}
Solution 3, regular mapping
#Mapper
public interface ExampleMapper {
#Mapping(target = "from.firstName", source = "source.fromPersonFirstName")
#Mapping(target = "from.lastName", source = "source.fromPersonLastName")
#Mapping(target = "to.firstName", source = "source.toPersonFirstName")
#Mapping(target = "to.lastName", source = "source.toPersonLastName")
Target toTarget(Source source);
}
Generated code:
public class ExampleMapperImpl implements ExampleMapper {
#Override
public Target toTarget(Source source) {
if ( source == null ) {
return null;
}
Target target = new Target();
target.from = sourceToPerson( source );
target.to = sourceToPerson1( source );
target.message = source.message;
return target;
}
protected Person sourceToPerson(Source source) {
if ( source == null ) {
return null;
}
Person person = new Person();
person.firstName = source.fromPersonFirstName;
person.lastName = source.fromPersonLastName;
return person;
}
protected Person sourceToPerson1(Source source) {
if ( source == null ) {
return null;
}
Person person = new Person();
person.firstName = source.toPersonFirstName;
person.lastName = source.toPersonLastName;
return person;
}
}
Related
I am thinking about how to verify by a unit test to find out if all targets of the mapstruct mappings are active / in use...
I have the following construct:
#Mapper(uses = {DateMapper.class, StateMapper.class})
public abstract class Blah
implements IDtoContextMapper<SourceEntity, StuffDto> {
#Mapping(source = "id", target = "id")
#Mapping(source = "someNumber", target = "someStuffNumber")
#Mapping(target = "status", qualifiedByName = "mapStatus")
public abstract Blah toDto(
SourceEntity entity, #Context MappingContext mappingContext);
#Override
public Class<StuffDto> getClassOfDto() {
return StuffDto.class;
}
#Override
public Class<SourceEntity> getClassOfEntity() {
return SourceEntity.class;
}
}
And the generated map struct impl looks like:
#Override
public StuffDto toDto(SourceEntity entity, MappingContext mappingContext) {
StuffDto target = mappingContext.getMappedInstance( entity, StuffDto.class );
if ( target != null ) {
return target;
}
if ( entity == null ) {
return null;
}
StuffDto stuffDto = new StuffDto();
mappingContext.incrementDepth( entity, StuffDto );
stuffDto.setId( entity.getId() );
stuffDto.setStatus( stateMapper.mapStatus( entity.getStatus() ) );
stuffDto.setSomeStuffNumber( entity.getSomeNumber() );
mappingContext.updateOccurences( entity, stuffDto );
return stuffDto;
}
What I am looking for is a simple junit test that verfies that all targets (=all members) of the StuffDto are mapped somehow (by source->target mapping or by qualifiedByName).
Which approach could I begin to try?
Any ideas welcome, I just started to play around :-)
What MapStruct offers is to have compile errors if some target properties is not mapped.
This can be configured through the ReportingPolicy on the Mapper#unmappedTargetPolicy for the target properties or Mapper#unmappedSourcePolicy.
So in your case to make sure that all target properties are mapped you can use
Mapper(uses = {DateMapper.class, StateMapper.class}, unmappedTargetPolicy = ReportingPolicy.ERROR)
public abstract class Blah
implements IDtoContextMapper<SourceEntity, StuffDto> {
#Mapping(source = "someNumber", target = "someStuffNumber")
#Mapping(target = "status", qualifiedByName = "mapStatus")
public abstract Blah toDto(
SourceEntity entity, #Context MappingContext mappingContext);
#Override
public Class<StuffDto> getClassOfDto() {
return StuffDto.class;
}
#Override
public Class<SourceEntity> getClassOfEntity() {
return SourceEntity.class;
}
}
Note: I have removed #Mapping(source = "id", target = "id") since it is obsolete. MapStruct will implicitly map this property.
i have an issue with my mapping definition. I hope you can give me a hint how to fix it.
My initial situation is, that I have classes with nested classes. Most of the attributes of the Input-nested class could be directly mapped to the target-nested class. But one attribute of the input-nested class must be mapped to the taget class directly.
This is my code:
public class Input {
public String aa;
public NestedClass nestedMember;
public static class NestedClass {
public String ab;
public String ac;
public String ad;
public String ae;
}
}
public class Target {
public NestedClass nestedMember;
public static class NestedClass {
public String ba;
public String bb;
public String bc;
public String bd;
}
}
#Mapping(target = "nestedMember.ba", source = "aa")
#Mapping(target = "nestedMember", source = "nestedMember")
Target map(Input input);
#Mapping(target = "bb", source = "ab")
#Mapping(target = "bc", source = "ac")
#Mapping(target = "bd", source = "ad")
#Mapping(target = "ba", ignore = true)
NestedClass mappingNested(test.Input.NestedClass nestedClass);
My assumption was, that mapstruct will use the mappingNested method for mapping the nested class and then a direct attribute mapping for aa to nestedMember.ba.
But the generated class looks like this:
public class MyMapperImpl implements MyMapper {
#Override
public Target map(Input input) {
if ( input == null ) {
return null;
}
Target target = new Target();
if ( input.nestedMember != null ) {
if ( target.nestedMember == null ) {
target.nestedMember = new NestedClass();
}
nestedClassToNestedClass( input.nestedMember, target.nestedMember );
}
if ( target.nestedMember == null ) {
target.nestedMember = new NestedClass();
}
inputToNestedClass( input, target.nestedMember );
return target;
}
#Override
public NestedClass mappingNested(test.Input.NestedClass nestedClass) {
if ( nestedClass == null ) {
return null;
}
NestedClass nestedClass1 = new NestedClass();
nestedClass1.bb = nestedClass.ab;
nestedClass1.bc = nestedClass.ac;
nestedClass1.bd = nestedClass.ad;
return nestedClass1;
}
protected void nestedClassToNestedClass(test.Input.NestedClass nestedClass, NestedClass mappingTarget) {
if ( nestedClass == null ) {
return;
}
}
protected void inputToNestedClass(Input input, NestedClass mappingTarget) {
if ( input == null ) {
return;
}
mappingTarget.ba = input.aa;
}
}
So mapstruct gernerates the mappingNested method correctly, but doesn't use it to map the nestedClasses. Mapstuct generates a method (nestedClassToNestedClass) without any mappings. If I remove the explicit aa mapping, everithing works fine. But then I have the problem how I should mapp the aa mapping? I don't what to map every attribute of the nestedClass in the map-method.
Does anybody has an Idea?
Best regards.
I have found a solution:
#Mapping(target = "nestedMember", source = ".")
Target map(Input input);
#Mapping(target = "bb", source = "nestedClass.ab")
#Mapping(target = "bc", source = "nestedClass.ac")
#Mapping(target = "bd", source = "nestedClass.ad")
#Mapping(target = "ba", ignore = true)
NestedClass mappingNested(test.Input.NestedClass nestedClass);
default NestedClass mappingNested(Input input) {
NestedClass nestedClass = mappingNested(input.nestedMember);
nestedClass.ba = input.aa;
return nestedClass;
}
We have a POJO class with Optional getter fields. The mapstruct 1.3.0.Final generating wrong code for the Optional collection field.
We have ProgramAggregate POJO which contains Collection of Program (Collection) and it is an Optional getter type.
When we run with mapstruct 1.2.0.Final, we are seeing proper code generation.
But the same code with 1.3.0.Final generating wrong code. It is not generating the Collection mapping method for Optional Collection getter methods.
Generated code image
#Data
#NoArgsConstructor
#ToString(callSuper = true)
#FieldDefaults(makeFinal = false, level = AccessLevel.PRIVATE)
public class ProgramAggregate {
public static final long serialVersionUID = 1L;
Collection<Program> programs;
public Optional<Collection<Program>> getPrograms() {
return Optional.ofNullable(programs);
}
}
#Data
#NoArgsConstructor
#FieldDefaults(makeFinal = false, level = AccessLevel.PRIVATE)
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "_type")
public class Program {
String name;
String number;
public Optional<String> getName() {
return Optional.ofNullable(name);
}
public Optional<String> getNumber() {
return Optional.ofNullable(number);
}
}
#Data
#NoArgsConstructor
#FieldDefaults(makeFinal = false, level = AccessLevel.PRIVATE)
public class ProgramResponseDto {
Collection<ProgramDto> programs;
public Optional<Collection<ProgramDto>> getPrograms() {
return Optional.ofNullable(programs);
}
}
#Data
#NoArgsConstructor
#FieldDefaults(makeFinal = false, level = AccessLevel.PRIVATE)
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "_type")
public class ProgramDto {
String name;
String number;
String oldNumber;
public Optional<String> getName() {
return Optional.ofNullable(name);
}
public Optional<String> getNumber() {
return Optional.ofNullable(number);
}
public Optional<String> getOldNumber() {
return Optional.ofNullable(oldNumber);
}
}
#Mapper(nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS, unmappedTargetPolicy = ReportingPolicy.WARN,
collectionMappingStrategy = CollectionMappingStrategy.TARGET_IMMUTABLE)
public interface IProgramMapper extends IOptionalMapper, IDefaultMapper {
ProgramResponseDto map(ProgramAggregate programAggregate);
ProgramDto map(Program sourceProgramDto);
Collection<ProgramDto> mapPrograms(Collection<Program> sourcePrograms);
}
Result:
#Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2020-10-12T13:10:32+0530",
comments = "version: 1.3.0.Final, compiler: javac, environment: Java 1.8.0_231 (Oracle Corporation)"
)
public class IProgramMapperImpl implements IProgramMapper {
#Override
public ProgramResponseDto map(ProgramAggregate programAggregate) {
if ( programAggregate == null ) {
return null;
}
ProgramResponseDto programResponseDto = new ProgramResponseDto();
**Collection<ProgramDto> collection = fromOptional( programAggregate.getPrograms() );**
if ( collection != null ) {
programResponseDto.setPrograms( collection );
}
return programResponseDto;
}
#Override
public ProgramDto map(Program sourceProgramDto) {
if ( sourceProgramDto == null ) {
return null;
}
ProgramDto programDto = new ProgramDto();
if ( sourceProgramDto.getName() != null ) {
programDto.setName( fromOptional( sourceProgramDto.getName() ) );
}
if ( sourceProgramDto.getNumber() != null ) {
programDto.setNumber( fromOptional( sourceProgramDto.getNumber() ) );
}
return programDto;
}
#Override
public Collection<ProgramDto> mapPrograms(Collection<Program> sourcePrograms) {
if ( sourcePrograms == null ) {
return null;
}
Collection<ProgramDto> collection = new ArrayList<ProgramDto>( sourcePrograms.size() );
for ( Program program : sourcePrograms ) {
collection.add( map( program ) );
}
return collection;
}
}
Below is the error after maven build.
Error:
[ERROR] //mapstruct_latest_example/target/generated-sources/annotations/IProgramMapperImpl.java:[21,57] incompatible types: inference variable T has incompatible bounds
[ERROR] equality constraints: java.util.Collection
[ERROR] upper bounds: java.util.Collection,java.lang.Object
Issue:
Allow generic of generics in types when matching
e.g. method signature such as
<T> T fromOptional(Optional<T> optional)
when T resolves to another generic class such as Collection
This might be resolved in mapstruct 1.4.2. version.
I have the below structure and i want to map this using mapstruct.
class DTO
{
private Integer id;
String comment;
//getters & setters
}
class ParentEntity
{
private Integer id;
CommentEntity comment;
//getters & setters
}
class CommentEntity
{
private Integer id;
private String text;
//getters & setters
}
#Mapper(componentModel = "spring")
public interface SampleMapper
{
#Mapping(source = "entity.comment.text", target = "comment")
public DTO toDTO(final ParentEntity entity);
#Mapping(source = "dto.comment", target = "comment.text")
public ParentEntity toEntity(final DTO dto);
}
The below is the implementation generated by mapstruct for toDTO method
#Override
public DTO toDTO(ParentEntity entity) {
if ( entity == null ) {
return null;
}
DTO dto = new DTO();
dto.setComment( entityCommentText( entity ) );
....................
}
private String entityCommentText(ParentEntity entity) {
if ( entity == null ) {
return null;
}
Comment comment = entity.getComment();
if ( comment == null ) {
return null;
}
String text = comment.getText();
if ( text == null ) {
return null;
}
return text;
}
The below is the implementation generated by mapstruct for toEntity method
#Override
public ParentEntity toEntity(DTO dto) {
if ( dto == null ) {
return null;
}
ParentEntity entity = new ParentEntity();
entity.setComment( dtoToCommentEntity( dto ) );
.............
}
protected CommentEntity dtoToCommentEntity(DTO dto) {
if ( dto == null ) {
return null;
}
CommentEntity commentEntity = new CommentEntity();
commentEntity.setText( dto.getComment() );
return commentEntity;
}
My question is the toDTO() method is setting the comment only if the text is not null. But the toEntity() method is not checking for the null or empty text.
So if i get "comment":null in my DTO, it is creating a new comment object and setting text as null.
How to avoid this?
Can someone explain the behavior and suggest me the proper way to do it?
Thanks!
Like this:
#Mapper( componentModel = "spring" )
public interface MyMapper {
#Mapping(source = "entity.comment.text", target = "comment")
DTO toDTO(final ParentEntity entity);
// make sure the entire parameter dto is mapped to comment
#Mapping(source = "dto", target = "comment")
ParentEntity toEntity(final DTO dto);
// and MapStruct will select your own implementation
default CommentEntity dTOToCommentEntity(DTO dTO) {
if ( dTO == null ) {
return null;
}
CommentEntity commentEntity = null;
if ( dTO.getComment()!= null ) {
commentEntity = new CommentEntity();
commentEntity.setText( dTO.getComment() );
}
return commentEntity;
}
}
Here is my source object:
public class Record {
public final long captureTime;
public final String environnement;
public final String bundle;
public final String type;
public final String id;
public final Map<String,Object> meta;
}
Here is my destination object:
public class MappedRecord {
public final long captureTime;
public final String environnement;
public final String bundle;
public final String type;
public final String id;
public final String ip;
public final String server;
}
And my mapper looks like the following:
public interface RecordMapper {
RecordMapper MAPPER = Mappers.getMapper( RecordMapper.class );
#Mappings({
#Mapping(source = "captureTime", target = "captureTime"),
#Mapping(source = "environnement", target = "environnement"),
#Mapping(source = "bundle", target = "bundle"),
#Mapping(source = "type", target = "type"),
#Mapping(source = "id", target = "id"),
#Mapping(expression = "java((String) r.meta.get(\"ip\"))", target = "ip"),
#Mapping(expression = "java((String) r.meta.get(\"server\"))", target = "server"),
})
MappedRecord toMappedRecord(Record r);
}
For now it works well but I would like to know if there is a more "elegant" way to set Map entries as source. Because with this I did not manage to add transformation functions using the "qualifiedByName" property, it looks like it can only work when a "source" is specified. Did I misunderstand something ?
I tried the following approaches without satisfying results :
Overwrite getter for specific fields in my Record class
Add a transformation function with the "qualifiedByName" property. Something like:
#Named("metaGetter")
default String dataGetter (String property) {
return (String) r.meta.get(property);
}
But obviously this does not compile as the property name is not valid as a proper source.
Thanks for your time.
Write your own qualifier:
public class MappingUtil {
#Qualifier
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.SOURCE)
public #interface Ip {
}
#Qualifier
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.SOURCE)
public static #interface Server {
}
#Ip
public String ip(Map<String, Object> in) {
return (String)in.get("ip");
}
#Server
public String server(Map<String, Object> in) {
return (String)in.get("server");
}
}
and then just add to the mapping:
#Mapper( uses = MappingUtil.class )
public interface SourceTargetMapper {
SourceTargetMapper MAPPER = Mappers.getMapper( SourceTargetMapper.class );
#Mappings( {
#Mapping(source = "map", target = "ip", qualifiedBy = MappingUtil.Ip.class ),
#Mapping(source = "map", target = "server", qualifiedBy = MappingUtil.Server.class ),
} )
Target toTarget( Source s );
}