Force MapStruct to use custom method instead of dto value - java

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

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.

Java: How to add mapper for streamed items

I currently have the following controller method (it's a bit simplified to only show the relevant part):
#PostMapping(...)
...
public ResponseEntity<List<PresignedUrlsResponse>> getPresignedUrlBatch(#Valid #RequestBody PresignedUrlsRequest urlsRequest) {
List<PresignedUrlsResponse> presignedUrlResponses = urlsRequest.getRequests().stream().map(request -> {
// TODO: put this in it's own mapping
String url = this.mediaService.getPresignedUrl(request.getObjectId(), request.getBucket());
PresignedUrlsResponse response = new PresignedUrlsResponse();
response.setId(request.getId());
response.setUrl(url);
return response;
}).collect(Collectors.toList());
return ResponseEntity.ok().body(presignedUrlResponses);
}
As mentioned in the TODO, I want to simplify this controller method and add a mapper. I'm only used to mapping requests from a db call for example (in which I will get a List of entities) but not when the service method has to be called for a list of items.
Is there a best practice for this?
MapStruct supports mapping Stream to Collection and Stream to Stream.
However, in your use case you start with List and not with Stream.
You can move your entire logic in a mapper.
e.g.
#Mapper(componentModel = "spring", uses = {
PresignedUrlMappingService.class
})
public interface PresignedUrlsMapper {
List<PresignedUrlsResponse> map(List<PresignedUrlsRequest> requests);
#Mapping(target = "url", source = "request", qualifiedByName = "presignedUrl")
PresignedUrlsResponse map(PresignedUrlsRequest request);
}
Your PresignedUrlMappingService can look like:
#Service
public class PresignedUrlMappingService {
protected final MediaService mediaService;
public PresignedUrlMappingService(MediaService mediaService) {
this.mediaService = mediaService;
}
#Named("presignedUrl")
public String presignedUrl(PresignedUrlsRequest request) {
return this.mediaService.getPresignedUrl(request.getObjectId(), request.getBucket())
}
}
and finally your controller method will look like:
#PostMapping(...)
...
public ResponseEntity<List<PresignedUrlsResponse>> getPresignedUrlBatch(#Valid #RequestBody PresignedUrlsRequest urlsRequest) {
return ResponseEntity.ok().body(presignedUrlsMapper.map(urlsRequest.getRequests());
}

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

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

MapStruct QualifiedByName with multiple parameters

I have come across a situation where my mapping method has 3 parameters, and all the three are being used in deriving one of the properties of the target type.
I have created a default mapping method in the interface keeping the logic for deriving the property, now for calling this method, I could use an expression = "java( /*method call here*/ )" in the #Mapping annotation.
Is there any way to do this with any of the mapstruct annotation like #qualifiedByName, I tried commenting the annotation having expression property and used qualifiedByName, but it doesn't work :
#Mapper
public interface OneMapper {
#Mapping(target="id", source="one.id")
//#Mapping(target="qualified",expression = "java( checkQualified (one, projId, code) )")
#Mapping(target="qualified",qualifiedByName="checkQualifiedNamed")
OneDto createOne (One one, Integer projId, Integer val, String code);
#Named("checkQualifiedNamed")
default Boolean checkQualified (One one, Integer projId, Integer val, String code) {
if(one.getProjectId() == projId && one.getVal() == val && one.getCode().equalsIgnoreCase(code)) {
return Boolean.TRUE;
}
return Boolean.FALSE;
}
}
Currently MapStruct does not support mapping methods with multiple source properties.
However, in your case you can use the #Context from 1.2.0. From what I understand the projId and the code are there just as helper of the mapping, and they are not used to map target properties from.
So you can do something like (It should work in theory):
#Mapper
public interface OneMapper {
#Mapping(target="id", source="one.id")
#Mapping(target="qualified", qualifiedByName="checkQualifiedNamed")
OneDto createOne (One one, #Context Integer projId, #Context String code);
#Named("checkQualifiedNamed")
default Boolean checkQualified (One one, #Context Integer projId, #Context String code) {
if(one.getProjectId() == projId && one.getCode().equalsIgnoreCase(code)) {
return Boolean.TRUE;
}
return Boolean.FALSE;
}
}
Another alternative would be to extract all those properties into a separate class and pass that along (this would allow for multiple parameters of the same type).
The class would look like:
public class Filter {
private final Integer projId;
private final Integer val;
private final String code;
public Filter (Integer projId, Integer val, String code) {
this.projId = projId;
this.val = val;
this.code = code;
}
//getters
}
Your mapper will then look like:
#Mapper
public interface OneMapper {
#Mapping(target="id", source="one.id")
#Mapping(target="qualified", qualifiedByName="checkQualifiedNamed")
OneDto createOne (One one, #Context Filter filter);
#Named("checkQualifiedNamed")
default Boolean checkQualified (One one, #Context Filter filter) {
if(one.getProjectId() == filter.getProjId() && one.getVal() == filter.getVal() && one.getCode().equalsIgnoreCase(filter.getCode())) {
return Boolean.TRUE;
}
return Boolean.FALSE;
}
}
You can then call the mapper like: mapper.createOne(one, new Filter(projId, val, code));
Since version 1.2 it is supported:
http://mapstruct.org/documentation/stable/reference/html/#mappings-with-several-source-parameters
For example like this:
#Mapping(source = "person.description", target = "description")
#Mapping(source = "address.houseNo", target = "houseNumber")
DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Address address);
UPDATE
Since Mapstruct allows to map multiple source arguments into a single target, I would advise to extract the checkQualified method from the mapper and instead compute the outcome beforehand and invoke the mapper with the result of the checkQualified method. Mapstruct is a mapping library, and does not excel in performing arbitrary logic. It's not impossible, but personally, I don't see the value it adds in your particular case.
With the logic extracted your mapper could look like this:
#Mapper
public interface OneMapper {
OneDto toOneDto(One one, Boolean qualified);
}
The mapper can be used like this:
One one = new One(1, 10, 100, "one");
boolean qualified = checkQualified(one, 10, 100, "one");
boolean notQualified = checkQualified(one, 10, 100, "two");
OneDto oneDto = mapper.toOneDto(one, isQualified);
For a full example, see: https://github.com/phazebroek/so-mapstruct/blob/master/src/main/java/nl/phazebroek/so/MapStructDemo.java
If you need to calculate a single target field based on multiple source fields from the same source object, you can pass the full source object to the custom mapper function instead of individual fields:
Example Entity:
#Entity
#Data
public class User {
private String firstName;
private String lastName;
}
Example DTO:
public class UserDto {
private String fullName;
}
... and the mapper... Instead of passing a single source (firstName):
#Mapper
public abstract class UserMapper {
#Mapping(source = "firstName", target = "fullName", qualifiedByName = "nameTofullName")
public abstract UserDto userEntityToUserDto(UserEntity userEntity);
#Named("nameToFullName")
public String nameToFullName(String firstName) {
return String.format("%s HOW DO I GET THE LAST NAME HERE?", firstName);
}
... pass the full entity object (userEntity) as the source:
#Mapper
public abstract class UserMapper {
#Mapping(source = "userEntity", target = "fullName", qualifiedByName = "nameToFullName")
public abstract UserDto userEntityToUserDto(UserEntity userEntity);
#Named("nameToFullName")
public String nameToOwner(UserEntity userEntity) {
return String.format("%s %s", userEntity.getFirstName(), userEntity.getLastName());
}
You can create a default method which calls internally mapstruct method with additional context params.in this way, you can obtain all parameters in 'qualifiedByName' part
#Mapper
public interface OneMapper {
default OneDto createOne(One one, Integer projId, Integer val, String code) {
return createOneWithContext(one,porjId,val,code
one,porjId,val,code //as context params
);
}
#Mapping(target="id", source="one.id")
#Mapping(target="qualified",source="one",qualifiedByName="checkQualifiedNamed")
OneDto createOneWithContext (One one, Integer projId, Integer val, String code
#Context One oneAsContext,
#Context Integer projIdAsContext,
#Context Integer valAsContext,
#Context String codeAsContext
);
#Named("checkQualifiedNamed")
default Boolean checkQualified (One one, #Context Integer projId, #Context Integer val, #Context String code) {
if(one.getProjectId() == projId && one.getVal() == val && one.getCode().equalsIgnoreCase(code)) {
return Boolean.TRUE;
}
return Boolean.FALSE;
}
}
```

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