I currently have a Map<String, String> that contains values in the form key = value and I would like to "expand" those into a real object.
Is it possible to automate that with MapStruct and how would I do that?
To clarify: The code I would write by hand would look something like this:
public MyEntity mapToEntity(final Map<String, String> parameters) {
final MyEntity result = new MyEntity();
result.setNote(parameters.get("note"));
result.setDate(convertStringToDate(parameters.get("date")));
result.setCustomer(mapIdToCustomer(parameters.get("customerId")));
// ...
return result;
}
Method 1
The MapStruct repo gives us useful examples such as Mapping from map.
Mapping a bean from a java.util.Map would look something like :
#Mapper(uses = MappingUtil.class )
public interface SourceTargetMapper {
SourceTargetMapper MAPPER = Mappers.getMapper( SourceTargetMapper.class );
#Mappings({
#Mapping(source = "map", target = "ip", qualifiedBy = Ip.class),
#Mapping(source = "map", target = "server", qualifiedBy = Server.class),
})
Target toTarget(Source s);
}
Notice the use of the MappingUtil class to help MapStruct figuring out how to correctly extract values from the Map :
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");
}
}
Method 2
As per Raild comment on the issue related to this post, it is possible to use MapStruct expressions to achieve similar results in a shorter way :
#Mapping(expression = "java(parameters.get(\"name\"))", target = "name")
public MyEntity mapToEntity(final Map<String, String> parameters);
No note on performance though and type conversion may be trickier this way but for a simple string to string mapping, it does look cleaner.
Since version 1.5.0.Beta1 (Jul 2021) MapStruct supports mapping from Map to POJO.
Example:
#Mapper
public interface CustomerMapper {
#Mapping(target = "name", source = "customerName")
Customer toCustomer(Map<String, String> map);
}
Related
I just want to apply the generateToken method to the Token field, but mapstruct is forcing login too, how to solve this?
#Mapper(uses = MapperGenerateToken.class)
public interface TelemarketerAccountMapper {
TelemarketerAccountMapper INSTANCE = Mappers.getMapper( TelemarketerAccountMapper.class );
#Mapping(expression = "java(MapperGenerateToken.generateToken(assistant.getLogin()))", target = "token")
#Mapping(source = "assistant.login", target = "login")
AuthenticatedTelemarketerAccount map(TelemarketerAccount assistant);
Genetated code:
#Override
public AuthenticatedTelemarketerAccount map(TelemarketerAccount assistant) {
if ( assistant == null ) {
return null;
}
AuthenticatedTelemarketerAccount authenticatedTelemarketerAccount = new AuthenticatedTelemarketerAccount();
authenticatedTelemarketerAccount.setLogin( MapperGenerateToken.generateToken( assistant.getLogin() ) );
authenticatedTelemarketerAccount.setToken( MapperGenerateToken.generateToken(assistant.getLogin()) );
return authenticatedTelemarketerAccount;
}
MapStruct has the concept of reusing user defined mappings defined in other mappers defined via Mapper#uses.
There are 2 ways that you can solve this:
Use of qualifiers
In your MapperGenerateToken#generateToken method you can use the MapStruct #Named annotation and use that in the mapping.
e.g.
public class MapperGenerateToken {
#Named("generateToken")
public static String generateToken(String login) {
// your custom logic
}
}
#Mapper(uses = MapperGenerateToken.class)
public interface TelemarketerAccountMapper {
TelemarketerAccountMapper INSTANCE = Mappers.getMapper( TelemarketerAccountMapper.class );
#Mapping(target = "token", source = "login", qualifiedByName = "generateToken")
AuthenticatedTelemarketerAccount map(TelemarketerAccount assistant);
}
this is going to generate the code that you expect.
Note: The second #Mapping is not needed since MapStruct will detect the mapping for the login automatically.
Use of Mapper#imports
If you still want to use expression then you can use Mapper#imports to tell MapStruct to only import that class and not use it to look up for mapping methods.
e.g.
#Mapper(uses = MapperGenerateToken.class)
public interface TelemarketerAccountMapper {
TelemarketerAccountMapper INSTANCE = Mappers.getMapper( TelemarketerAccountMapper.class );
#Mapping(target = "token", expression = "java(MapperGenerateToken.generateToken(assistant.getLogin()))")
AuthenticatedTelemarketerAccount map(TelemarketerAccount assistant);
}
I would strongly suggest the use of the first approach using qualifiers.
I'm getting ambiguous mapping error by "mapping from map" using MapStruct 1.3.1.Final
#Mapper( uses = MappingUtil.class )
public interface SourceTargetMapper {
SourceTargetMapper MAPPER = Mappers.getMapper( SourceTargetMapper.class );
#Mapping(source = "map", target = "aProperty", qualifiedBy = A.class )
#Mapping(source = "map", target = "bProperty", qualifiedBy = B.class )
Target toTarget(Map<String,Map<String, Object>> map);
}
public class MappingUtil {
#Qualifier
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.CLASS)
public #interface A {
}
#Qualifier
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.CLASS)
public #interface B {
}
#A
public String abc(Map<String,Map<String, Object>> in) {
return (String) in.get("first_key").get("a_second_key");
}
#B
public String xyz(Map<String,Map<String, Object>> in) {
return (String) in.get("first_key").get("b_second_key");
}
}
When I change Either #A or #B to a different type, the error goes away
#B
public int xyz(Map<String,Map<String, Object>> in) {
return (int) in.get("first_key").get("b_second_key");
}
What am I missing? The docs example used two methods of the same type so it should not be an issue
EDIT: Same issue on Mapstruct 4.1.2.Final
Your implementation of qualifiers is not correct. Here is the link which shows how to implement custom mapping with qualifiers in case of ambiguous mappings.
Below is the short version of code of how you can create custom mappings based on qualifiers:
#Qualifier
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.CLASS)
public #interface Translator {
}
#Qualifier
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.CLASS)
public #interface A {
}
#Qualifier
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.CLASS)
public #interface B {
}
#Translator
public class MappingUtil {
#A
public String abc(Map<String,Map<String, Object>> in) {
return (String) in.get("first_key").get("a_second_key");
}
#B
public String xyz(Map<String,Map<String, Object>> in) {
return (String) in.get("first_key").get("b_second_key");
}
}
#Mapper( uses = MappingUtil.class )
public interface SourceTargetMapper {
SourceTargetMapper MAPPER = Mappers.getMapper( SourceTargetMapper.class );
#Mapping(source = "map", target = "aProperty", qualifiedBy = { Translator.class, A.class } )
#Mapping(source = "map", target = "bProperty", qualifiedBy = { Translator.class, B.class } )
Target toTarget(Map<String,Map<String, Object>> map);
}
Alternative:
You can use qualifiedByName if you do not want to create separate Utility class. Below is the example code for it:
#Mapper
public interface SourceTargetMapper {
SourceTargetMapper MAPPER = Mappers.getMapper( SourceTargetMapper.class );
#Mapping(source = "map", target = "aProperty", qualifiedByName= "abc" )
#Mapping(source = "map", target = "bProperty", qualifiedByName= "xyz" )
Target toTarget(Map<String,Map<String, Object>> map);
#Named("abc")
default String abc(Map<String,Map<String, Object>> in) {
// return whatever need to return
}
#Named("xyz")
default String xyz(Map<String,Map<String, Object>> in) {
// return whatever need to return
}
}
This seems to work correctly when using 1.4.2.Final. However, in 1.5.0.Beta1 this is no longer working due to the newly introduced Map to Bean mapping. We are investigating how we can fix this.
However, there is an alternative solution that would make this work properly:
#Mapper( uses = MappingUtil.class )
public interface SourceTargetMapper {
SourceTargetMapper MAPPER = Mappers.getMapper( SourceTargetMapper.class );
#Mapping(source = "first_property", target = "aProperty", qualifiedBy = A.class )
#Mapping(source = "first_property", target = "bProperty", qualifiedBy = B.class )
Target toTarget(Map<String,Map<String, Object>> map);
}
public class MappingUtil {
#Qualifier
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.CLASS)
public #interface A {
}
#Qualifier
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.CLASS)
public #interface B {
}
#A
public String abc(Map<String, Object> in) {
return (String) in.get("a_second_key");
}
#B
public String xyz(Map<String, Object> in) {
return (String) in.get("b_second_key");
}
}
Note the change of the signatures in the MappingUtil and the change in Mapping#source.
I have a Spring Boot project and I'm using mapstruct to map 2 objects.
And I have this king of structure, this is the first object:
ObjectA {
List<ObjectB> objectsB;
}
ObjectB {
String prId;
List<String> dtId;
}
---
The second object:
ObjectC {
List<ObjectD> objectsD;
}
ObjectD {
ObjectE objectE;
List<ObjectE> objectsE;
}
ObjectE {
String nmId;
}
And now using mapstruct I need to do this:
#Mapper(componentModel = "spring")
public interface AppMapper {
#Mappings({
#Mapping(target = "objectC.objectsD.objectE.nmId", source = "objectA.objectsB.prId"),
#Mapping(target = "objectC.objectsD.objectsE.nmId", source = "objectA.objectsB.dtId")
})
ObjectC objectAToObjectC(ObjectA objectA);
}
How can I do it? Any feedback will be apreciated! Thank you!
You have to add custom mapping method to map nested objectB to objectD and any string to ObjectE.
This should work for you given example:
#Mapper
public interface AppMapper
{
#Mapping(source = "objectsB", target = "objectsD")
ObjectC objectAToObjectC(ObjectA objectA);
#Mapping(source = "prId", target = "objectE.nmId")
#Mapping(source = "dtId", target = "objectsE")
ObjectD objectBtoObjectD(ObjectB objectB);
}
I have a UserDTO and User entity which I want to map. When creating a new User some fields (for example: password, modifiedBy) must be generated by some custom method (for example: password is randomly generated and encoded, but modifiedBy username is retrieved from security service). For this I autowire some services into the mapper. Many of them return String and MapStruct cannot understand which one to use in each case and just uses the first it found on everything that accepts String as input.
#Mapper(componentModel = "spring", uses = PasswordEncoder.class)
public interface UserMapper {
#Mapping(target = "password", qualifiedByName = "PASS")
User mapUser(UserDto dto);
#Named("PASS")
default String getPass(PasswordEncoder passwordEncoder){
return passwordEncoder.encode(some_random_generator);
}
}
This generates code that just uses method from PasswordEncoder in any setter that accepts String and getPass(...) method is not used at all.
However I need it to use my getPass(...) method on password field only.
Currently it is not possible to pass the used mapper or service to a default method. There is mapstruct/mapstruct#1637 open for that. Also you can't really do #Mapper( uses = PasswordEncoder.class ) as that would lead to all String to String to be mapped via the PasswordEncoder. What you can do though is to create your own custom PasswordEncoderMapper and use #Named on it, this way you would be in control.
This can look like this:
#Qualifier // org.mapstruct.Qualifier
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.CLASS)
public #interface EncodedMapping {
}
public class PasswordEncoderMapper {
protected final PasswordEncoder passwordEncoder;
public PasswordEncoderMapper(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}
#EncodedMapping
public String encode(String value) {
return passwordEncoder.encode(value);
}
}
#Mapper(componentModel = "spring", uses = PasswordEncoderMapper.class)
public interface UserMapper {
#Mapping(target = "password", qualifiedBy = EncodedMapping.class)
User mapUser(UserDto dto);
}
Regarding the modifiedBy property. You should do that as part of an #ObjectFactory or by using an expression.
With an expression this can look like:
#Mapper(componentModel = "spring", uses = PasswordEncoderMapper.class, imports = SecurityUtils.class)
public interface UserMapper {
#Mapping(target = "password", qualifiedBy = EncodedMapping.class)
#Mapping(target = "modifiedBy", expression = "java(SecurityUtils.getCurrentUserId())")
User mapUser(UserDto dto);
}
I have Story entity in my Spring Boot application. It has String field storyInfo which contains:
{"title":"random title", "description":"random description"}
For my Story entity I have StoryDTO with map field called storyInfo.
The question is: how can I convert String field from Strory into Map in StoryDTO using MapStruct?
Try following code, inspired from here
#Mapper(componentModel = "spring")
public interface StoryMapper {
#Mappings({
#Mapping(source = "storyInfo", target = "storyInfo", qualifiedByName = "fromJsonToMap")
})
StoryDTO toStoryDTO(Story story);
#Mappings({
#Mapping(source = "storyInfo", target = "storyInfo", qualifiedByName = "fromMapToJson")
})
Story toStory(StoryDTO storyDTO);
#Named("fromJsonToMap")
default Map<String, Object> fromJsonToMap(String storyInfo) throws IOException {
if (Objects.nonNull(storyInfo)) {
ObjectMapper objectMapper = new ObjectMapper();
Map<String, Double> result = objectMapper.readValue(storyInfo, new TypeReference<Map<String, Object>>() {});
return result;
}
return null;
}
#Named("fromMapToJson")
default String fromMapToJson(Map<String, Object> storyInfo) throws JsonProcessingException {
if (Objects.nonNull(storyInfo)) {
ObjectMapper objectMapper = new ObjectMapper();
String result = objectMapper.writeValueAsString(storyInfo);
return result;
}
return null;
}
}
Thank you guys for answers. Found the easiest solution for me by adding few manual mappers to MapStruct's StoryMapper interface.
// Manual convert to Map
default Map toMap(String text){
Map map = new HashMap();
try {
map = new ObjectMapper().readValue(text, new TypeReference<Map<String, String>>(){});
} catch (IOException e) {
e.printStackTrace();
}
return map;
}
// Manual convery from map
default String fromMap(Map map){
return new JSONObject(map).toString();
}
The already provided answer explains well how you can provide a Service to do the mapping with Jackson.
In order to make this work with MapStruct you can use qualifiers and annotate your service accordingly.
For example
#Qualifier // from the MapStruct package
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.CLASS)
public #interface FromJson {
}
public interface StringToMapConverter {
#FromJson
Map<String, String> convert(String string);
}
#Mapper(componentModel = "spring")
public interface MyMapper {
#Mapping(target = "storyInfo", qualifiedBy = FromJson.class)
StoryDTO convert(Story story);
}
The implementation of StringToMapConverter should be as in the already provided answer. You don't have to use a dedicated interface for the converter, you an also use an abstract mapper, inject the ObjectMapper and do the rest same.
MapStruct will then use it to convert the storyInfo String into the map.
Some other possible solution, outside of the scope of the question and if you use Hibernate. You can use Map<String, String> in your entity, but still map it to String in the DB. Have a look at hibernate-types by Vlad Mihalcea, it allows using extra types so you can persist objects as JSON in the database
You can create a generic tool class so other Mapper can also use.
p.s: JsonUtil just a util clss use to transform Object to Json.
And you can use jackson, fastjson, gson.
#Component
public class MapStructUtil {
#Qualifier
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.SOURCE)
public #interface JsonStrToMap {
}
#Qualifier
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.SOURCE)
public #interface MapToJsonStr {
}
#JsonStrToMap
public Map jsonStrToMap(String jsonStr) {
return JsonUtil.toMap(jsonStr);
}
#MapToJsonStr
public String mapToJsonStr(Map<String, String> map) {
return JsonUtil.toJsonString(map);
}
}
Then you can use it in your Mapper like this
p.s: Here use componentModel = "spring", so you need to add #Componnent annotation in MapStructUtil
#Mapper(componentModel = "spring", uses = {MapStructUtil.class})
public interface StoryMapper {
#Mapping(source = "storyInfo", target = "storyInfo", qualifiedBy = JsonStrToMap.class)
StoryDTO toStoryDTO(Story story);
#Mapping(source = "storyInfo", target = "storyInfo", qualifiedBy = MapToJsonStr.class)
Story toStory(StoryDTO storyDTO);
}
I am not familiar with MapStruct, but I might suggest an alternative since you are running your application in a Spring context.
Since your string is a JSON string, your best course of action would be to use a JSON library. Spring Boot comes with its own preconfigued instance of the Jackson ObjectMapper (which you may override to add/remove specific features by defining using a #Bean of type ObjectMapper in any #Configuration class).
You might inject an instance of this using:
#Autowired
ObjectMapper objectMapper;
After that, you are able to use the object mapper to transform the string into a HashMap<String, String> (or whichever types you need) as follows:
Map<String, String> result = objectMapper.readValue(storyInfo, new TypeReference<Map<String, String>>() {});
I will try to update this answer with a MapStruct approach, but perhaps this might be more practical for you at this time.