MapStruct : Dealing with HashMap entries as source - java

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 );
}

Related

How to unit test if all mapstruct targets are really mapped (targeted)?

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.

Enum lookup by positioned values using MapStruct

I have implemented my basic requirements, which work well in one simple scenario as mentioned below code snippet. But for new requirements what is the best way out there I need help.
New requirement: Statuses in numeric format are used on other services but in request-response status representation are these user-friendly string ["Not Started", "In Progress", "Completed"]
#AllArgsConstructor
#Getter
public enum StatusEnum {
NOT_STARTED(1,"Not Started"),
IN_PROGRESS(2, "In Progress"),
COMPLETED(3, "Completed");
private final int key;
private final String value;
}
Below is my MapStruct logic to convert enum to string and visa-versa conversion logic. This works fine for basic requirements. But what is the logic of the new requirement?
ActionItem.java:
private Constants.StatusEnum status;
Basic Requirements works with below implementation:
#AllArgsConstructor
#Getter
public enum StatusEnum {
NOT_STARTED("Not Started"),
IN_PROGRESS("In Progress"),
COMPLETED("Completed");
private final String value;
}
#Mapper
public interface ActionItemMapper extents BaseMapper {
#Mapping(source = "status", target = "status", qualifiedByName = "statusEnumToString")
ActionItemResponse toActionItemResponse(ActionItem actionItem);
}
#Mapper
public interface BaseMapper {
#Named("statusEnumToString")
default String statusEnumToString(Constants.StatusEnum statusEnum) {
return statusEnum.getValue();
}
#Named("statusStringToEnum")
default Constants.StatusEnum statusStringToEnum(String status) {
return List.of(Constants.StatusEnum.values()).stream().filter(s -> s.getValue().equals(status)).findAny()
.orElse(null);
}
}
I got the solution.
#AllArgsConstructor
#Getter
public enum StatusEnum {
NOT_STARTED(1, "Not Started"),
IN_PROGRESS(2, "In Progress"),
COMPLETED(3, "Completed");
private final String key;
private final String value;
}
#Mapper
public interface ActionItemMapper extents BaseMapper {
#Mapping(source = "status", target = "status", qualifiedByName = "statusEnumToString")
ActionItemResponse toActionItemResponse(ActionItem actionItem);
}
#Mapper
public interface BaseMapper {
#Named("statusEnumKeyToValue")
default String statusEnumKeyToValue(Integer status) {
String value = null;
for (Constants.StatusEnum statusEnum: Constants.StatusEnum.values()) {
if (statusEnum.getKey().equals(status)) {
value = statusEnum.getValue();
break;
}
}
if (value == null) {
throw new IllegalArgumentException("No status value found for status key " +status);
}
return value;
}
#Named("statusEnumValueToKey")
default Integer statusEnumValueToKey(String status) {
return statusStringToEnum(status).getKey();
}
default Constants.StatusEnum statusStringToEnum(String status) {
return List.of(Constants.StatusEnum.values()).stream().filter(s -> s.getValue().equals(status)).findAny()
.orElseThrow()
}
}

How to fill in map inside target object by copying values from source object using -MapStruct?

I am new to Mapstruct. I have a scenario where, in my target object I have a java map with key value pair<String,String> and I have to fill this map using source objects inner object properties/data member values.
My code is something like bellow(dummy code):
public class Student {
public String name;
public String rollNo;
public Map<String, String> marks;
}
public class ExamResult{
public String stud_name;
public String Stud_rollNo;
public Marks marks;
}
public class Marks{
public Integer English;
public Integer Maths;
public Integer Science;
}
How would I manually achieve the same thing like bellow:
Student target;
ExamResult source;
target.setName(source.stud_name);
target.setRollNo(source.Stud_RollNo);
target.marks.put("ENGLISH",source.marks.english_marks);
target.marks.put("MATHS",source.marks.math_marks);
target.marks.put("SCIENCE",source.marks.science_marks);
For direct property mapping I have found code, but not sure how I can map the values to be filled in the marks map.
I had given a thought to use java expression to fill in target map values, but didn't found any documentation or such example of expressions using for target object.
I was thinking to use like bellow but not sure it will work:
#Mapping(source = "stud_name", target = "name")
#Mapping(source = "Stud_RollNo", target = "rollNo")
#Mapping(source = "source.marks.english_marks",target = "java( marks.put(\"ENGLISH\",source.marks.english_marks )")
#Mapping(source = "source.marks.math_marks",target = "java( marks.put(\"MATHS\",source.marks.math_marks )")
#Mapping(source = "source.marks.science_marks",target = "java( marks.put(\"SCIENCE\",source.marks.science_marks )")
Student doConvert(ExamResult src)
Any help, any suggestion or any workaround is appreciated.
Thanks in Advance.
Using expressions in target is not allowed for MapStruct, that's why you couldn't make it work.
Mapping into maps from objects is also not supported yet, we have different issues requesting this feature.
What I would suggest is to use the automatic mapping as much as possible when you can and then resort to #AfterMapping when MapStruct can't do that. So in your case something like:
#Mapper
public interface StudentMapper {
#Mapping(source = "stud_name", target = "name")
#Mapping(source = "Stud_RollNo", target = "rollNo")
#Mapping(target = "marks", ignore = true) // mapped in #AfterMapping
Student doConvert(ExamResult src)
#AfterMapping
default void addStudentMarks(ExamResult result, #MappingTarget Student student) {
student.marks = new HashMap<>();
student.marks.put("ENGLISH", result.marks.ENGLISH);
student.marks.put("MATHS", result.marks.MATHS);
student.marks.put("SCIENCE", result.marks.SCIENCE);
}
}
I would write a Custom mapper method to convert the Marks into Map Object
#Mapping(source = "stud_name", target = "name")
#Mapping(source = "Stud_rollNo", target = "rollNo")
Student doConvert(ExamResult examResult);
static Map<String,String> mapMarks(Marks marks) {
Map<String,String> marksMap = new HashMap<>();
marksMap.put("ENGLISH", String.valueOf(marks.getEnglish()));
marksMap.put("MATHS", String.valueOf(marks.getMaths()));
return marksMap;
}
In case the map elements are too big to mention, Jackson Library could be used which can dynamically create the map with reference name as Key and Object value as value
#Mapping(source = "stud_name", target = "name")
#Mapping(source = "Stud_rollNo", target = "rollNo")
Student doConvert(ExamResult examResult);
ObjectMapper mapper = new ObjectMapper();
static Map<String,String> mapMarks(Marks marks) {
return mapper.convertValue(marks, Map.class);
}
what about this:
#Mapper
public interface MyMapper {
#Mapping( target = "name", source = "stud_name")
#Mapping( target = "rollNo", source = "stud_rollNo")
// marks will be mapped based on name equality
Student map( ExamResult result);
// mapstruct sees an unmapped property (although it only has a getter), marks
#Mapping( target = "marks", ignore = true )
MarksWrapper toWrapper(Marks marks );
// handwritten method, just to do the mapping
default Map<String, Integer> toMap( MarksWrapper wrapper) {
return wrapper.getMarks();
}
// this class does the conversion
class MarksWrapper {
private Map<String, Integer> marks = new HashMap<>( );
public void setEnglish( Integer mark) {
marks.put( "ENGLISH", mark );
}
public void setMaths( Integer mark ){
marks.put( "MATH", mark );
}
public void setScience( Integer mark ) {
marks.put( "SCIENCE", mark );
}
public Map<String, Integer> getMarks() {
return marks;
}
}
}
Note: I used a Map<String,Integer> Map<String,String> but the idea is the same..

#Mapping with custom source and target variable is not working as expected when target has variable with same name as source

I have below Source and Target classes, i am using lombok for generating getters and setters
public class Target {
private String name;
private String newName;
}
public class Source {
private String name;
}
and let say if I want to map Source.name to Target.newName
I am using below Mapper class with #Mapping to specify source and target variables.
but once i compile the code and check the generated ClassMapperImpl
it is maping Source.name to Target.name and not to Target.new Name
#Mapper
public interface ClassMapper {
#Mapping(source = "name", target = "newName")
Target sourceToTarget(Source s);
}
I think they are both mapped when I try:
public class ClassMapperImpl implements ClassMapper {
#Override
public Target sourceToTarget(Source s) {
if ( s == null ) {
return null;
}
Target target = new Target();
target.setNewName( s.getName() );
target.setName( s.getName() );
return target;
}
}
Please use ignore on the name property.
#Mapper
public interface ClassMapper {
#Mapping(source = "name", target = "newName")
#Mapping(ignore = true, target = "name")
Target sourceToTarget(Source s);
}

Mapstruct: #InheritInverseConfiguration --> There is no suitable result type constructor for reversing this method

I try to use mapstruct feature #InheritInverseConfiguration. But it gives me the following error: There is no suitable result type constructor for reversing this method.
But the generated Impl look pretty good.
#Mapper(uses = {
BOIdMapper.class,
DateMapper.class,
CarVOFactory.class,
})
public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper(CarMapper .class);
#Mappings({
#Mapping(source = "VOKey", target = "id"),
#Mapping(source = "gueltAb", target = "gueltigAb")
})
Car map(CarVO carVO);
#InheritInverseConfiguration
CarVO map(Car car);
public class CarVOFactory {
public CarVO createCarVO() {
return VOFactory.createVO(CarVO.class);
}
}
}
This is the POJO Car
public class Car {
private String id;
private LocalDate gueltigAb;
...
}
This is the other Car-Class:
public interface CarVO extends ... {
void setVOKey (VOKey<CAR> voKey);
}

Categories

Resources