Mapping DTO using Mapstruct - java

I am new to Mapstruct and am having issues in a particular usecase
so, if my source attribute have hotmail.com my target attribute should receive "personal" and if my source has facebook.com my target should receive "corporate".
I was trying to use expression but couldn't get my way around it.
how do i do it?
#Mapping(source = "user.email", target = "emailType")
NewDTO myMapperMethod(MyRequest req);

You can use qualifiedByName and default interface method to qualify and define suitable mapping method for a given property:
#Mapper(componentModel = "spring")
public interface RequestMapper {
#Mapping(source = "user.email", target = "emailType", qualifiedByName = "EmailToType")
NewDTO myMapperMethod(MyRequest req);
#Named("EmailToType")
default String emailTypeResolver(String email) {
if ("hotmail.com".equals(email)) {
return "personal";
} else if ("facebook.com".equals(email)) {
return "corporate";
} else {
return "unknown";
}
}
}

It is not something MapStruct is supposed to solve for you. It is making a run-time decision based on content. Typically a business problem, not a mapping problem

Related

Add custom method in mapstruct

#Override
#Mappings(
{
#Mapping(target = "temperature", source = "pac.temperature"),
#Mapping(target = "containerId", ignore=true),
}
)
TargetABC toDto(Source source);
#AfterMapping
default void inRange(Source source, #MappingTarget TargerABC target) {
var temperature = source.getPac.getTemperature();
var range = source.getRange();
target.setContainerId(
range.calculate(temperature)
);
}
at the moment I have a solution using #AfterMapping, but I want to get rid of this approach in favor of qualifiedByName and do the mapping in the field itself by adding a method with the #Named annotation, is it possible that such a method will take two values? Maybe there is another better solution?
You can define custom method which will accept Sourceas parameter and will make required computations. Call this method from #Mapping using expression= "java(<method_name>)".
#Mapping(target = "containerId",expression = "java(calculateContainerId(source))")
TargetABC toDto(Source source);
default String calculateContainerId(Source source) {
var temperature = source.getPac.getTemperature();
var range = source.getRange();
return range.calculate(temperature);
}
The simplest solution is defining a default method for mapping source to containerId, which would MapStruct recognize and use if you define source as the source used for mapping, as shown below:
#Mapping(target = "temperature", source = "pac.temperature")
#Mapping(target = "containerId", source = "source")
TargerABC toDto(Source source);
default Integer calculateContainerIdFromSource(Source source) {
var temperature = source.getPac().getTemperature();
var range = source.getRange();
return range.calculate(temperature);
}

Map Enum field with MapStruct

I want to map 2 models, where each of them has almost the same enums. Let me show:
The first model has enum:
public enum EventSource {
BETRADAR("SOURCE_BETRADAR"),
BETGENIUS("SOURCE_BETGENIUS"),
BETCONSTRUCT("SOURCE_BETCONSTRUCT"),
MODEL("SOURCE_MODEL");
Second model has enum:
public enum SportEventSource implements ProtocolMessageEnum {
SOURCE_UNKNOWN(0),
SOURCE_BETRADAR(1),
SOURCE_BETGENIUS(2),
SOURCE_BETCONSTRUCT(3),
UNRECOGNIZED(-1);
I have such custom mapping method:
#Named("eventSourceConverter")
default EventSource eventSourceConverter(SportEventSource source) {
switch (source) {
case SOURCE_MODEL:
return EventSource.MODEL;
case SOURCE_BETCONSTRUCT:
return EventSource.BETCONSTRUCT;
case SOURCE_BETGENIUS:
return EventSource.BETGENIUS;
case SOURCE_BETRADAR:
return EventSource.BETRADAR;
default:
return EventSource.MODEL;
}
}
And then I use:
#Mapping(target = "mainSource", source = "source", qualifiedByName = "eventSourceConverter")
AdapterCompetitor protoToModel(Competitor proto);
But get:
error: The following constants from the property "SportEventSource source" enum have no corresponding constant in the "*source*" enum and must be mapped via adding additional mappings: SOURCE_UNKNOWN, SOURCE_BETRADAR, SOURCE_BETGENIUS, SOURCE_BETCONSTRUCT, UNRECOGNIZED.
AdapterCompetitor protoToModel(Competitor proto);
I've also created the enum mapper like:
#ValueMappings({
#ValueMapping(source = "SOURCE_BETRADAR", target = "BETRADAR"),
#ValueMapping(source = "SOURCE_BETGENIUS", target = "BETGENIUS"),
#ValueMapping(source = "SOURCE_BETCONSTRUCT", target = "BETCONSTRUCT"),
#ValueMapping(source = "SOURCE_MODEL", target = "MODEL"),
#ValueMapping(source = "SOURCE_UNKNOWN", target = "MODEL"),
#ValueMapping(source = "UNRECOGNIZED", target = "MODEL")
})
EventSource eventSourceToSportEventSource(SportEventSource source);
But I don't need to have it separately, just want that enum field will be mapped within the internal mapping. Simply to say — when I do AdapterCompetitor protoToModel(Competitor proto) enum also should be mapped.
Thanks!
p.s. sorry for my eng, hope my questions make sense :)
This can be achieved using #ValueMapping
I think the most convenient way is just to make a default method in your mapper
default ProblemStatus problemStatusFromString(String status) {
return ProblemStatus.get(status);
}
default String problemStatusToString(ProblemStatus problemStatus) {
return problemStatus.getValue();
}
I used
#Mapper(componentModel = "spring")
public interface ConverterMapper {
#Named("UnitValueConverter")
default Long unitValueConverter(UInt64Value value) {
return value.getValue();
}
as an interface which contains all named mappers like above, and then I do
#Mapper(componentModel = "spring")
public interface EventMapper extends ConverterMapper
The issue was that I didn't add the #Mapper annotation to the ConverterMapper interface

Mapstruct - Ambiguous mapping methods found for mapping property

I am using mapstruct to map from one DTO to another. I have multiple default methods , but 2 of them with a return value of String and that uses the same class as the input parameter gives me "Ambiguous mapping methods using java Mapstruct" error. I am adding the relevant parts of the code here:
#Mappings({
#Mapping(source = "programInstance", target = "title", qualifiedByName = "title"),
#Mapping(source = "programInstance", target = "seriesName", qualifiedByName = "seriesName"),
#Mapping(source = "programInstance", target = "season", qualifiedByName = "season"),
#Mapping(source = "programInstance", target = "epNumber", qualifiedByName = "epNumber"),
})
DTO1 mapDTOs (DTO2 dto2);
#Named("title")
default String mapTitle(Program programInstance) {
Optional<String> title = Utils.getObject(() -> programInstance.getTitle().getDescriptions().get(0).getValue());
if (title.isPresent())
return title.get();
return null;
}
#Named("seriesName")
default String mapSeriesName(Program programInstance) {
Optional<String> seriesName = Utils.getObject(() -> programInstance.get(0).getProgram().getTitle().getDescriptions().get(0).getValue());
if (seriesName.isPresent())
return seriesName.get();
return null;
}
#Named("season")
default Integer mapSeasonNumber(Program programInstance) {
Optional<Integer> season = Utils.getObject(() -> programInstance.get(0).getSeasonOf().get(0).getOrderNo());
if (season.isPresent())
return season.get();
return null;
}
#Named("epNumber")
default Integer mapEpNumber(Program programInstance) {
Optional<Integer> epNumber = Utils.getObject(() -> programInstance.getEpOf().get(0).getOrderNo());
if (epNumber.isPresent())
return epNumber.get();
return null;
}
The error is
Ambiguous mapping methods found for mapping property "Program
programInstance" to java.lang.String: java.lang.String mapTitle(),
java.lang.String mapSeriesName().
I checked your example.. The problem is that the fields you try to target are of type String.
So:
public class IvpVodOfferStatusDTO {
private String seasonNumber;
private String episodeNumber;
}
MapStruct tries to match this with the signature you provide:
#Named("season")
default Integer mapSeasonNumber(Program programInstance) {
Optional<Integer> season = Utils.getObject(() -> programInstance.get(0).getSeasonOf().get(0).getOrderNo());
if (season.isPresent())
return season.get();
return null;
}
#Named("epNumber")
default Integer mapEpNumber(Program programInstance) {
Optional<Integer> epNumber = Utils.getObject(() -> programInstance.getEpOf().get(0).getOrderNo());
if (epNumber.isPresent())
return epNumber.get();
return null;
}
MapStruct has a predefined order of attempts:
User provided Mapping method
Direct (types source -target are the same)
Mapping method (built-in)
Type conversion
If this all fails MapStruct tries to do a number of 2 step approaches:
mapping method - mapping method
mapping method - type conversion
type conversion - mapping method
At 6. it finds 2 qualifying methods (Program to String). It's probably an error in MapStruct that it selects methods that do not qualify (need to check whether this is intentional) by the #Named. Otherwise, I'll write an issue.
The most easy solution is: adapt the target:
public class IvpVodOfferStatusDTO {
private Integer seasonNumber;
private Integer episodeNumber;
}
What is probably what you intend (I guess).. Otherwise you could change the signature not to return an Integer but a String
I was facing same issue and observed that, there was same method inherited by my mapper class using #Mapper(uses = {BaseMapper.class}) and using extends BaseMapper.
Removing extends solved the problem for me.
So, you can look for method received by custom mapper through multiple ways.
Even if the data types are matching, this could happen if the name given at qualifiedByName is not defined in as a bean instance
Because without a matching #Named qualifier, the injector would not know which bean to bind to which variable
#Mapping( source = "firstName", target = "passenger.firstName", qualifiedByName = "mapFirstName" )
public abstract Passenger mapPassenger( Traveller traveller );
#Named( "mapFirstName" )
String mapFirstName( String firstName)
{
}

MapStruct Retrieving from arrayList

I'm trying to use MapStruct to map two objects. I have been searching for a while and I haven't been able to find anything, though I'm new to programming so I'm sure it's easier than I'm making it.
Here is some stripped back code (Note that the real code is more complex, with the child object from the arraylist not being the same type as the destination objects child variable as it is here):
SourceObject
public class SourceObject {
public ArrayList<ListObject> list = new ArrayList<ListObject>();
public SourceObject() {
list.add(new ListObject());
}
}
ListObject
public class ListObject {
public DetailsObject details = new DetailsObject();
public ListObject() {
details.forename="SourceForename";
details.surname="SourceSurname";
}
}
DestinationObject
public class DestinationObject {
public DetailsObject details = new DetailsObject();
public DestinationObject() {
details.forename="DestinationForename";
details.surname="DestinationSurname";
}
}
DetailsObject
public class DetailsObject {
public String forename;
public String surname;
}
Mapper
#Mappings({
#Mapping(target="details.forename", source="list.get(0).details.forename"),
#Mapping(target="details.surname", source="list.get(0).details.surname"),
})
DestinationObject toDestination(SourceObject source);
This will work fine if I put DetailsObject directly inside SourceObject, but becomes a problem when I try to take it from the list. The error I get is:
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.5.1:compile (default-compile) on project template: Compilation failure: Compilation failure:
[ERROR] .../src/main/java/Mapper/SourceToDestinationMap.java:[12,13] No property named "list.get(0).details.surname" exists in source parameter(s). Did you mean "list.empty"?
[ERROR] .../src/main/java/Mapper/SourceToDestinationMap.java:[11,9] No property named "list.get(0).details.forename" exists in source parameter(s). Did you mean "list.empty"?
EDIT: Current state of the mapper:
#Mapper
public interface SourceToDestinationMap {
#Mapping(target = "details", source = "list")
DestinationObject toDestination(SourceObject source);
default DetailsObject map(List<ListObject> source) {
return map(source.get(0));
}
DetailsObject map(ListObject source);
}
The mapping you are trying to do is not possible, since you are mixing bean properties and bean functions.
You can't use #Mapping(source = "list.get(0).XXX"). Using indexes to access elements of a list is not yet supported see #1321.
A way to solve your problem would be to use Qualifiers in the same way that they are used in the mapstruct-iterable-to-non-iterable example.
Or you can provide your own method that would perform the mapping.
#Mapper
public interface MyMapper {
#Mapping(target = "details", source = "list")
DestinationObject toDestination(SourceObject source);
default DetailsObject map(List<ListObject> source) {
return source != null && !source.isEmpty() ? map(source.get(0)) : null;
}
DetailsObject map(ListObject source);
}
MapStruct will then generate the correct code.
Another alternative would be to use #Mapping(expression="java()".
Your mapper will then look like:
#Mapper
public interface MyMapper {
#Mapping(target = "details.forename", expression = "java(source.list.get(0).details.forename)")
#Mapping(target = "details.surname", expression = "java(source.list.get(0).details.surname)")
DestinationObject toDestination(SourceObject source);
}
Note for expression. MapStruct will use the text from the expression as is, and won't perform any validation checks. Therefore it can be a bit brittle, as there won't be null and empty checks.

Dealing with changed ENUM definitions - database

Introduction
The lead architect went and changed the ENUM definition in a spring boot project.
From:
public enum ProcessState{
C("COMPLETE"), P("PARTIAL");
}
To:
public enum ProcessState{
COMPLETE("COMPLETE"), PARTIAL("PARTIAL");
}
What is the proper way to deal with this? Some other Java Spring Boot applications are now breaking. Would there be a way to tell the jackson deserializer to perform some kind of conversion in these situations?
My Current Work-Around
What I did was to run two update statements on the oracle database:
UPDATE store set PAYLOAD = REPLACE(PAYLOAD, '"processState":"P"','"processState":"PARTIAL"') where PAYLOAD like '%"processState":"P"%';
UPDATE store set PAYLOAD = REPLACE(PAYLOAD, '"processState":"C"','"processState":"COMPLETE"') where PAYLOAD like '%"processState":"C"%';
Question
So are there other ways? Could I do it by adding some deserialization/conversion code somewhere for these specific cases? Is there a more elegant way than running a replace SQL statement?
Could I do some kind of hack on a specific java sub-package, and say "use this enum instead of that enum..." or use one of the two? But without affecting the rest of the code?
The error:
java.lang.IllegalArgumentException: No enum constant
Ideally we store value of emum rather than Enum.
So, you should save ENUM values like COMPLETE,PARTIAL
For JSON serialization and de-serialization, use #JsonValue
#JsonValue
public String toValue() {
return value;
}
One additional solution to the others posted:
#JsonCreator
public static ProcessState factory(String inputValue) {
if(inputValue.length() == 1){
for(ProcessState type : ProcessState.values()){
if(inputValue.equals(type.getValue().substring(0,inputValue.length()))){
return type;
}
}
}
return ProcessState .valueOf(inputValue);
}
Implement a JPA converter like this:
#Converter(autoApply = true)
public class ProcessStateConverter
implements AttributeConverter<ProcessState, String> {
private ImmutableBiMap<ProcessState, String> map = ImmutableBiMap.<ProcessState, String>builder()
.put(COMPLETE, "C")
.put(COMPRESSING, "P")
.build();
#Override
public String convertToDatabaseColumn(ProcessState attribute) {
return Optional.ofNullable(map.get(attribute))
.orElseThrow(() -> new RuntimeException("Unknown ProcessState: " + attribute));
}
#Override
public ProcessState convertToEntityAttribute(String dbData) {
return Optional.ofNullable(map.inverse().get(dbData))
.orElseThrow(() -> new RuntimeException("Unknown String: " + dbData));
}
}
Remember to treat your Enum like a simple column and not #Enumerated i.e.
#Entity
public class MyEntity {
#Column //no #Enumerated
private ProcessState processState;
//...
}
The drawback is that you need to maintain the converter each time something changes. So better create a unit test to check if everything is correctly mapped.

Categories