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;
}
Related
#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);
}
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());
This question already has answers here:
How to distinguish between null and not provided values for partial updates in Spring Rest Controller
(8 answers)
Closed 2 years ago.
I have a PATCH REST endpoint, exposing a JSON interface, which can be used to partially update an entity, based on the attributes which are actually sent in the body.
Let's consider a sample class representing the entity:
class Account {
private UUID accountUuid;
private UUID ownerUuid;
private String number;
private String alias;
// constructor, getters and setters omitted
}
where accountUuid, ownerUuid and number are required properties, and alias is optional. Additionally I have a command class for updating said account:
class UpdateAccountCommand {
private UUID accountUuid;
private String number;
private String alias;
// constructor, getters and setters omitted
}
For my PATCH endpoint, e.g. PATCH /accounts/{account-uuid}, I'd like to implement a functionality, that only properties actually sent in the request are changed:
// this should only update the account number
{"number": "123456"}
// this should only update the alias, by setting it to null
{"alias": null}
// this shouldn't update anything
{}
For required properties, this is fairly easy to do. After deserialisation from the JSON string to UpdateAccountCommand instance using Jackson, I simply check if a required property is null, and when it's not null, I update the given property.
However the situation complicates with optional properties, since the alias property is null in both cases:
when the request body explicitly specifies the alias as null,
when the alias property is not specified in the request body at all.
How can I model these optional properties, so that I can indicate this removable mechanism?
A naive solution would be to introduce some sort of a wrapper, which would not only contain the raw value (e.g. for the alias: string property), but also a boolean, indicating whether a property was specified in the body or not. This would require you to write a custom deserialisation mechanism, which can be a tedious work.
Since the question is about Java 8, for Java 8 and newer, I recommend using a nullable Optional, which works pretty much out of the box with Jackson.
For optional (removable fields), you change the raw values by wrapping them in optional:
class UpdateAccountCommand {
private UUID accountUuid;
private String number;
private Optional<String> alias;
// constructor, getters and setters omitted
}
In order for Jackson to work with Optional<*> fields correctly, the Jdk8Module module has to be registered to the object mapper, e.g.:
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new Jdk8Module());
The following code:
public static void main(String[] args) throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new Jdk8Module());
String withNewAliasS = "{\"alias\":\"New Alias\"}";
String withNullAliasS = "{\"alias\":null}";
String withoutPropertyS = "{}";
UpdateAccountCommand withNewAlias = objectMapper.readValue(withNewAliasS, UpdateAccountCommand.class);
if (withNewAlias.getAlias() != null && withNewAlias.getAlias().isPresent()) {
System.out.println("withNewAlias specified new alias.");
}
UpdateAccountCommand withNullAlias = objectMapper.readValue(withNullAliasS, UpdateAccountCommand.class);
if (withNullAlias.getAlias() != null && !withNullAlias.getAlias().isPresent()) {
System.out.println("withNullAlias will remove an alias.");
}
UpdateAccountCommand withoutProperty = objectMapper.readValue(withoutPropertyS, UpdateAccountCommand.class);
if (withoutProperty.getAlias() == null) {
System.out.println("withoutProperty did not contain alias property on input at all.");
}
}
then prints out this to the console:
withNewAlias specified new alias.
withNullAlias will remove an alias.
withoutProperty did not contain alias property on input at all.
you can add an additional boolean property which says if the optional property was present in request
class UpdateAccountCommand {
//...
private String alias;
#JsonIgnore
private boolean isAliasSet;
#JsonProperty
public void setAlias(String value) {
this.alias = value;
this.isAliasSet = true;
}
}
the setter is called only when "alias" is present, be it null or with value
I am using below DTO class with respective annotations and are working fine also. But when I send a integer value for name/reqID(which is a String datatype) fields, still it is executing without any error/exception. How to avoid it or validate the datatype of incoming fields.
public class RequestDTO {
#NotEmpty(message = "Please provide reqID")
private String reqID;
#NotEmpty(message = "Please provide name")
private String name;
private Map <String, String> unknownProperties;
public AccountDTO(){
this.unknownProperties = new HashMap<String, String>();
}
public AccountDTO(String reqID, String name){
this.reqID= reqID;
this.name = name;
this.unknownProperties = new HashMap<String, String>();
}
#JsonAnySetter
public void add(String key, String value) {
this.unknownProperties.put(key, value);
}
#JsonAnyGetter
public Map <String, String> getUnknownProperties() {
return unknownProperties;
}
//getters and setters
}
working for { "reqID" : 56, "name" : 674 }. Have to check the datatype/reject the request. Any help would be appreciable.
If you're using Spring boot, by default it uses Jackson to parse JSON. There's no configuration option within Jackson to disable this feature
Here you will find interesting approaches to solving this problem:
Disable conversion of scalars to strings when deserializing with Jackson
You can disable MapperFeature ALLOW_COERCION_OF_SCALARS which is enabled by default.
Then conversions from JSON String are not allowed.
Doc Details here
public static final MapperFeature ALLOW_COERCION_OF_SCALARS
When feature is disabled, only strictly compatible input may be bound:
numbers for numbers, boolean values for booleans. When feature is
enabled, conversions from JSON String are allowed, as long as textual
value matches (for example, String "true" is allowed as equivalent of
JSON boolean token true; or String "1.0" for double).
Or create a custom json deserializer for string overriding default serializer JsonDeserializer<String>.
You could validate the input you are getting. But this is not specific to your DTO so if you have some sort of Utilities class with static methods (think about having one if you don't) it's better if you add it there and grab it for any DTO that might need this validation.
The validation method would look something like this:
public static boolean isNumber(String in) {
try{
Integer.parseInt(in);
// log something useful here
return true;
} catch(NumberFormatException e) {
return false;
}
}
You could then use this method throw your own exception. Then handle that the way you'd need:
if (Utilities.isNumber(reqID)){
throw new IllegalArgumentException("Meaningful Exception Message here");
}
I hope it helps! :)
Spring boot allows regular expression checking using #Patter annotation. So just add the following
#Pattern(regexp="[a-zA-Z]")
#NotEmpty(message = "Please provide name")
private String name;
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)
{
}