How to map 2 objects using MapStruct in a Spring Boot app? - java

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

Related

mapstruct duplicating code in another field

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.

Fix for ambiguous mapping methods found for mapping property error on #Qualifier with the same method type

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.

How to use a mapper in another mapper using Mapstruct and Kotlin?

I would like to map an entity to a DTO with a nested DTO using Mapstruct, in Kotlin.
I have a first DTO defined as follow:
data class FirstDto (
val something: String
)
This DTO is mapped in an entity and vice-versa using Mapstruct. Here is the Mapper:
#Mapper(componentModel = "spring")
interface FirstMapper {
fun entityToDto(entity: FirstEntity): FirstDto
fun dtoToEntity(dto: FirstDto): FirstEntity
}
And a second DTO nesting the first DTO:
data class SecondDto (
val somethingElse: String,
val firstDto: FirstDto
)
As for the first DTO, I define a Mapper using Mapstruct. But, I would like this mapper to use FirstMapper to map the nested DTO. So I should be using the uses property of the Mapper.
In Java, this would look like this: #Mapper(componentModel = "spring", uses = FirstMapper.class).
How should it be implemented using Kotlin?
It doesn't differ much. Purely syntax differences.
#Mapper(componentModel = "spring", uses = [FirstMapper::class])
interface SecondMapper {
#Mapping(source = "firstEntity", target = "firstDto")
fun entityToDto(entity: SecondEntity): SecondDto
#Mapping(source = "firstDto", target = "firstEntity")
fun dtoToEntity(dto: SecondDto): SecondEntity
}
Which generates
public class SecondMapperImpl implements SecondMapper {
private final FirstMapper firstMapper = Mappers.getMapper(FirstMapper.class);
#Override
public SecondDto entityToDto(SecondEntity entity) {
...
firstDto = firstMapper.entityToDto(entity.getFirstEntity());
somethingElse = entity.getSomethingElse();
SecondDto secondDto = new SecondDto(somethingElse, firstDto);
return secondDto;
}
#Override
public SecondEntity dtoToEntity(SecondDto dto) {
...
firstEntity = firstMapper.dtoToEntity(dto.getFirstDto());
somethingElse = dto.getSomethingElse();
SecondEntity secondEntity = new SecondEntity(somethingElse, firstEntity);
return secondEntity;
}
}

How can I convert String to Map using MapStruct?

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.

MapStruct: mapping from java.util.Map to Bean?

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

Categories

Resources