Map Enum field with MapStruct - java

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

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

ModelMapper map value from Map

I am trying to get a grasp of ModelMapper for the following use case:
class A {
public String name;
public Map<String, ATranslation> translations;
}
class ATranslation {
public String desc;
public String content;
}
class DTO {
public String name;
public String desc;
public String content;
}
Assume Constructors, Getters and Setters.
public class App {
public static void main(String[] args) {
Map<String, ATranslation> translations = new HashMap<>();
translations.put("en", new ATranslation("en-desc","content1"));
translations.put("nl", new ATranslation("nl-desc","content2"));
A entity = new A("John Wick",translations);
System.out.println(App.toDto(entity,"en"));
System.out.println(App.toDto(entity,"nl"));
}
private static DTO toDto(A entity, String lang) {
ModelMapper modelMapper = new ModelMapper();
//how to set up ModelMapper?
return modelMapper.map(entity, DTO.class);
}
}
Without any setup the output is:
DTO(name=John Wick, desc=null, content=null)
DTO(name=John Wick, desc=null, content=null)
A converter does not work:
modelMapper
.createTypeMap(A.class, DTO.class)
.setConverter(new Converter<A, DTO>() {
public DTO convert(MappingContext<A, DTO> context) {
A s = context.getSource();
DTO d = context.getDestination();
d.setDesc(s.getTranslation().get(lang).getDesc());
d.setContent(s.getTranslation().get(lang).getContent());
return d;
}
});
A postConverter does work, but does not seem to be the most ModelMapper way...
modelMapper
.createTypeMap(A.class, DTO.class)
.setPostConverter(new Converter<A, DTO>() {
public DTO convert(MappingContext<A, DTO> context) {
A s = context.getSource();
DTO d = context.getDestination();
d.setDesc(s.getTranslation().get(lang).getDesc()); //tedious, if many fields...
d.setContent(s.getTranslation().get(lang).getContent()); //feels redundant already
return d;
}
});
DTO(name=John Wick, desc=en-desc, content=content1)
DTO(name=John Wick, desc=nl-desc, content=content2)
Is there a better way to use ModelMapper here?
Did not test it! This is just a theoretical answer composed from research.
Design considerations
Lead by your concerns about the tedious implementation of a Post-Converter the following solution is designed out of small components.
I tried to decompose your mapping use-case into smaller problems, and solve each using a component from the mapping framework.
Mechanics of a mapper
Looking at how a bean- or object- or model-mapper typically works, may shed some light on the issue at hand.
A mapper maps an object of type or source-class A to an new object of another type or target-class B.
ModelMapper - components to use
I tried to re-use examples from Baeldung's tutorial: Guide to Using ModelMapper. We need 3 steps:
(a) inferred property- or type-mapping for String name to equivalent target
(b) Expression Mapping: customized property-mapping for Map translation to String desc
(c) parameterized converter to translate and lookup by key String language and extract or convert the value ATranslation to String desc
The features we use are:
Property Mapping for (a) and (b)
Converters for (c)
1. property-mapping
In its simple form it infers the mapping of properties. It does so by mapping the properties of the source to the target. By default most mappers map the properties to ones equivalent by type or name:
field types and names perfectly match
In your case this worked for the property String name.
// GIVEN
Map<String, ATranslation> translations = Map.of(
"en", new ATranslation("en-desc"),
"nl", new ATranslation("nl-desc")
);
A entity = new A("John Wick", translations);
// SETUP (a) property-mapping by type
TypeMap<A, DTO> typeMap = modelMapper.createTypeMap(A.class, DTO.class);
// WHEN mapping the properties
DTO dto = modelMapper.map(entity, DTO.class);
// THEN desc expected to be null
assertNull(dto.getDesc());
assertEquals(entity.getName(), dto.getName());
2. conversion
In some use-cases you need some kind of conversion, when type or name of the properties to map can not be inferred by simple equivalence.
Define a translation factory-method to configure the converter. It creates a new converter or lets say "interpreter for the specified language" on demand.
// Converter: translates for specified language, means lookup in the map using a passed parameter
Converter<Map<String, ATranslation>, String> translateToLanguage(final String language) {
return c -> c.getOrDefault(language, new ATranslation("")).getDesc(); // language needs to be final inside a lambda
}
This method can be used to translate or convert
// SETUP (a) property-mapping as default type-map
TypeMap<A, DTO> typeMap = modelMapper.createTypeMap(A.class, DTO.class);
// (b) map the property translations (even so other type) to desc
typeMap.addMapping(Source::getTranslation, Destination::setDesc);
// (c) add the converter to the property-mapper
typeMap.addMappings(
mapper -> mapper.using(translateToLanguage("nl")).map(A::getTranslation, DTO::setDesc)
);
// WHEN mapping the properties
DTO dto = modelMapper.map(entity, DTO.class);
// THEN desc expected to be mapped to the specified language's translation
assertEquals(entity.getTranslation().get("nl").getDesc(), dto.getDesc());
assertEquals(entity.getName(), dto.getName());

Mapping DTO using Mapstruct

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

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.

Categories