Mapstruct - Ambiguous mapping methods found for mapping property - java

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

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

How convert string field to hashmap value with mapstruct java?

I want map accountNo to argument value with specific key(I mean key of hashmap), with mapstrcut. Any ideas?
Event is my target
private Map<String, Object> argument;
private LocalDateTime dateTime;
AccountRequest is my source
private String accountNo;
private LocalDateTime dateTime;
I have the mapper as below but I want the opposite one too.
#Mapping(target = "accountNo", expression = "java(event.getArgument().get(key).toString())")
AccountRequest eventToAccountRequest(Event event, String key);
Event accountRequestToEvent(AccountRequest accountRequest); // this is my question
Currently there is no out-of-the-box support for mapping from a Bean into a Map. There is however an open feature request for this functionality.
This means that for your example you'll need to do it in a custom mapping.
e.g.
#Mapping(target = "argument", source = "accountNo", qualifiedByName = "accountNoToArgument")
Event accountRequestToEvent(AccountRequest accountRequest);
#Named("accountNoToArgument")
default Map<String, Object> accountNotToArgument(String accountNo) {
return accountNo == null ? null : Collections.singletonMap("accountNo", accountNo);
}
This would make sure that the rest of the parameters are automatically mapped by MapStruct.
Side note about a better mapping for eventToAccountRequest. Instead of using an expression you can improve it with a custom mapping method and #Context. e.g.
#Mapping(target = "accountNo", source = "argument", qualifiedByName = "argumentToAccountNo")
AccountRequest eventToAccountRequest(Event event, #Context String key);
#Named("argumentToAccountNo")
default String argumentToAccountNo(Map<String, Object> argument, #Context key) {
return argument == null : null : argument.get(key).toString();
}
Using this you will avoid a potential NPE if event.getArgument() is null.
Why not using a default mapper. Something like:
default Event accountRequestToEvent(AccountRequest accountRequest) {
Event event = new Event();
event.setArguement(Collections.singletonMap(accountRequest.getAccountNo(), "value"));
return event;
}

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

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

Categories