Cannot make work mapping method selection based on qualifiers - java

I am using MapStruct to have a target bean mapped from attributes coming from 2 source beans. This is something very common that MapStruct easily does by Controlling nested mappings. My problem is that I need to "calculate/validate" a target attribute taking as input one of the attributes in a source bean.
I will explain with a few code snippets.
First, we have the beans, something like (simplified):
import lombok.Builder;
import lombok.Getter;
import java.util.List;
#Getter
#Builder
public class TargetBean {
private final List<String> targetListTransformed;
private final List<String> targetListDirectMapping;
private final List<String> targetListTransformedWithAnotherMethod;
private final String targetString;
private final Integer targetInteger;
}
One of the source beans:
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.List;
#Getter
#RequiredArgsConstructor
public class SourceBeanOne {
private final List<String> sourceListToTransform;
private final List<String> sourceListDirectMapping;
private final String sourceString;
}
and the other source bean:
import lombok.Getter;
import lombok.RequiredArgsConstructor;
#Getter
#RequiredArgsConstructor
public class SourceBeanTwo {
private final Integer sourceInteger;
}
Then the mapping attempt:
#Mapper(uses = SourceOneExtractor.class)
public abstract class TargetBeanMapper {
#Mapping(target = "targetListTransformed", source = "source1")
#Mapping(target = "targetListDirectMapping", source = "source1.sourceListDirectMapping")
#Mapping(target = "targetListTransformedWithAnotherMethod", qualifiedByName = {"sourceOneTransformer", "transformerTwo"})
#Mapping(target = "targetString", source = "source1.sourceString")
#Mapping(target = "targetInteger", source = "source2.sourceInteger")
abstract TargetBean map (SourceBeanOne source1, SourceBeanTwo source2);
List<String> mapTargetList(SourceBeanOne source1) {
final List<String> result = new ArrayList<>();
// simulate a transformation
result.add("one");
result.add("two");
return result;
}
}
As you can see, I have tried Invoking custom mapping method, where you can see see a dummy implementation what I need to do: use one, or more, attributes in SourceBeanOne to produce the List to be mapped into TargetBean.targetListTransformed.
That perfectly works. My problem came when I realized that I needed to produce another List for another Target attribute, doing different things with other attributes in SourceBeanOne. Another custom mapping method is not possible because MapStruct cannot disambiguate. That's how I ended trying 5.9. Mapping method selection based on qualifiers. And this is the resulting qualifier class based on #Named (to avoid generating a bunch of #Qualified annotations):
import org.mapstruct.Named;
import java.util.ArrayList;
import java.util.List;
#Named("sourceOneTransformer")
public class SourceOneExtractor {
#Named("transformerOne")
public List<String> getterMethodOne(SourceBeanOne s) {
final List<String> result = new ArrayList<>();
// simulate a transformation
result.add("one");
result.add("two");
return result;
}
#Named("transformerTwo")
public List<String> getterMethodTwo(SourceBeanOne s) {
final List<String> result = new ArrayList<>();
// simulate a transformation
result.add("three");
result.add("four");
return result;
}
}
Then I got this error:
No property named "targetListTransformedWithAnotherMethod" exists in source parameter(s). Please define the source explicitly.
At the beginning I had to deal with Lombok-Mapstruct issues reading some other posts in Stack Overflow and MapStruct documentation (Can I use MapStruct together with Project Lombok?), but once solved, everything is working and Lombok annotations are correctly processed before MapStruct's. But, just in case, I also tried with hand-made code for constructors, target builder and getters with the same result, so Lombok is not causing this. I also made sure I am using the right #Name annotation (Why does #Name not work?).
My real use case (code here is just a simplification for the sake of clarity) source does not have any attribute named "targetListTransformedWithAnotherMethod".

The last part of the error finally gave me the hint: " Please define the source explicitly."
Looking at Mapstruct source I saw that it was trying to find an attribute in SourceBeanOne named "targetListTransformedWithAnotherMethod". As there was not one, mapping was not possible and it failed saying "[...] define the source explicitly".
Despite none of the examples in 5.9. Mapping method selection based on qualifiers indicated the source bean by means of the source parameter, I tried it and now it works.
I leave here the final code of the mapper and a test, for reference. The code of the qualifier is the same as in the question above.
The mapper:
#Mapper(uses = SourceOneExtractor.class)
public interface TargetBeanMapper {
#Mapping(target = "targetListTransformed", source= "source1", qualifiedByName = {"sourceOneTransformer", "transformerOne"})
#Mapping(target = "targetListDirectMapping", source = "source1.sourceListDirectMapping")
#Mapping(target = "targetListTransformedWithAnotherMethod", source= "source1", qualifiedByName = {"sourceOneTransformer", "transformerTwo"})
#Mapping(target = "targetString", source = "source1.sourceString")
#Mapping(target = "targetInteger", source = "source2.sourceInteger")
TargetBean map (SourceBeanOne source1, SourceBeanTwo source2);
}
A sample test:
class TargetBeanMapperTest {
private static TargetBeanMapper mapper;
#BeforeAll
static void instantiateMapper() {
// instantiate mapper under test
mapper = Mappers.getMapper(TargetBeanMapper.class);
}
#Test
void map() {
final SourceBeanOne s1 = new SourceBeanOne(List.of("this", "list", "will be transformed"),
List.of("this", "list", "will be directly mapped"),
"sourceOneString");
final SourceBeanTwo s2 = new SourceBeanTwo(66);
final TargetBean t = mapper.map(s1, s2);
assertNotEquals(s1.getSourceListToTransform(),
t.getTargetListTransformed()); // check that source1 list has been transformed
assertEquals(List.of("one", "two"), t.getTargetListTransformed());
assertEquals(List.of("three", "four"),
t.getTargetListTransformedWithAnotherMethod()); // check that source1 list has been transformed
assertEquals(s1.getSourceListDirectMapping(),
t.getTargetListDirectMapping());
assertEquals(s1.getSourceString(), t.getTargetString());
assertEquals(s2.getSourceInteger(), t.getTargetInteger());
}
}

Related

Mapstruct: How to merge two fields into one

Using the MapStruct framework, how do you map multiple fields into a single one (based on custom logic) while still mapping the other fields one to one?
Here is a simple example to illustrate what I mean:
public class Source {
private String firstname;
private String surname;
// other fields eg:
private String address;
private int age;
private int favoriteNumber;
}
public class Target {
private String fullname; // Sould be firstname + surname
// other fields eg:
private String address;
private int age;
private int favoriteNumber;
}
I know it's possible using expressions:
#Mapping(target = "fullname", expression = "java(el.getFirstname() + el.getSurname())")
But in my special use case, not depicted in this example, I need to use some external library for the merging/mapping of the two fields, which isn't feasible with expressions.
Is there a way to achieve merging two fields without expressions?
One approach would be to add a custom mapping method from the Source object to the merged value, and then declare the source for the merged field to be the whole source object:
interface CustomMappingMethodMapper {
#Mapping(target = "fullname", source = ".")
Target map(Source source);
default String getFullName(Source s) {
return s.getFirstname() + " " + s.getSurname();
}
}
You can use #AfterMapping annotation
https://mapstruct.org/documentation/stable/reference/html/#customizing-mappings-with-before-and-after
You would like to replace your interface with abstract class and then
#AfterMapping
void customMapping(#MappingTarget Target target, Source source) {
// any custom logic
}
You claim that calling an external library in an expression isn't feasible. This may not be true, depending on the nature of the library you're calling and the frameworks being used.
Static method
If the method being called is a static method on a class, it can be called directly within the expression annotation element of #Mapping. To avoid having to fully qualify the class being called, the imports element of #Mapper can be used.
#Mapper(imports = ExternalLibrary.class)
public interface SourceToTargetMapper {
#Mapping(target = "fullname",
expression = "java(ExternalLibrary.toFullName(s.getFirstname(), s.getSurname()))")
Target map(Source s);
}
Spring Framework bean
If the library method is a method on a Spring bean, the mapper can be made into a bean using the Spring component model, and the Spring bean containing the library method can be injected into the mapper:
#Mapper(componentModel = "spring")
public static abstract class SourceToTargetMapper {
#Autowired
ExternalLibrary externalLibrary;
#Mapping(target = "fullname",
expression = "java(externalLibrary.toFullName(s.getFirstname(), s.getSurname()))")
abstract Target map(Source s);
}
To use this mapper, inject it as a Spring bean, and call the mapping method on the bean:
#Component
public class Example {
#Autowired
private SourceToTargetMapper mapper;
public void demonstrate(Source s) {
System.out.println(mapper.map(s));
}
}
I haven't tested it myself, but I imagine this injection approach would work with the other component models supported by MapStruct (cdi & jsr330).
I needed to compose 2 basic fields into am Wrapped Object. Here's how I did id
#Mapping(source = "quantity", target = "energyQuantity.quantity")
#Mapping(source = "quantityUnit", target = "energyQuantity.energyQuantityUnit", qualifiedByName = "tradeSearchEnergyQuantityUnitToEnergyQuantityUnit")
Trade tradeSearchToDomainDTO(api.client.generated.model.Trade trade);
where energyQuantity is a Wrapper class for the 2 fields

Jackson's #JsonPropertyOrder doesn't work with #JsonUnwrapped

I have an object that contains another object attribute like this:
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
#JsonPropertyOrder({"fA1","b","fA2"})
#Data
public class A {
private String fA1;
private String fA2;
#JsonUnwrapped
private B b = new B();
#Data
class B {
private String fB1;
private String fB2;
}
public static void main(String[] args) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
A a = new A ();
System.out.println(objectMapper.writeValueAsString(a));
}
}
what i want is generate json that respect this order :
{
"fA1":"",
"fB1":"",
"fA2":"",
"fB2":""
}
Is there any way to do this?
According to this issue in the jackson-databind repository on GitHub, the #JsonPropertyOrder annotation doesn't work with #JsonUnwrapped annotation. See the quote below:
True, unwrapped properties are not included in ordering, since they are not really known by enclosing serializer as regular properties. And specifically, as things are, will be output as a block of properties per contained, so even if known they can not be reordered separately.
Perhaps something could be done with Jackson 3.x once we get there.
But you may consider a workaround: as you seem to be using Lombok, you could annotate b with #Delegate and #JsonIgnore:
#Data
#JsonPropertyOrder({"fa1", "fb1", "fa2", "fb2"})
public class A {
private String fA1;
private String fA2;
#Delegate
#JsonIgnore
private B b = new B();
}
The #JsonIgnore annotation will ensure that the b property is not serialized by Jackson.
And the #Delegate annotation will tell Lombok to generate delegate methods that forward the call to the B methods. With that, the A class will have getters and setters that are delegated to the getters and setters of the fB1 and fB2 fields.

How to call MapStruct method from interface in java stream

I recently started using the MapStruct mapping tool in a project. In the past, for mapping DTO -> Entity and vice versa I used custom mapper like:
public static CustomerDto toDto(Customer customer) {
return isNull(customer)
? null
: CustomerDto.builder()
.id(customer.getId())
.name(customer.getName())
.surname(customer.getSurname())
.phoneNumber(customer.getPhoneNumber())
.email(customer.getEmail())
.customerStatus(customer.getCustomerStatus())
.username(customer.getUsername())
.NIP(customer.getNIP())
.build();
}
In case when I was trying to get one single Optional object after all I was able to map my entity to dto in the following way:
public Optional<CustomerDto> findOneById(final long id) {
return customerRepository.findById(id).map(CustomerMapper::toDto);
}
Currently, as I mentioned before I am using mapStruct and the problem is that my mapper it's, not class, it's the interface like:
#Mapper
public interface CommentMapper {
#Mappings({
#Mapping(target = "content", source = "entity.content"),
#Mapping(target = "user", source = "entity.user")
})
CommentDto commentToCommentDto(Comment entity);
#Mappings({
#Mapping(target = "content", source = "dto.content"),
#Mapping(target = "user", source = "dto.user")
})
Comment commentDtoToComment(CommentDto dto);
}
I want to know if it possible to use somehow this interface method in stream gentle to map my value without wrapping values like:
public Optional<CommentDto> findCommentById(final long id) {
Optional<Comment> commentById = commentRepository.findById(id);
return Optional.ofNullable(commentMapper.commentToCommentDto(commentById.get()));
}
Thanks for any help.
Access the mapper like:
private static final YourMapper MAPPER = Mappers.getMapper(YourMapper.class);
final Optional<YourEntity> optEntity = entityRepo.findById(id);
return optEntity.map(MAPPER::toDto).orElse(null);
Basically we do a similar thing with enumerations
#Mapping(target = "type", expression = "java(EnumerationType.valueOf(entity.getType()))")
you can define java expressions in your #Mapping annotation
#Mapping(target = "comment", expression = "java(commentMapper.commentToCommentDto(commentRepository.findById(entity.commentId).orElse(null)))"
Otherwise you should be able to make use of a
class CommentMapper { ... }
which you automatically can refer with
#Mapper(uses = {CommentMapper.class})
your implementation will detect the commentEntity and Dto and will automatically use the CommentMapper.
A MapStruct mapper is workling like: Shit in Shit out, so remember your entity needs the commentEntity so the dto can has the commentDto.
EDIT
2nd solution could be using:
#BeforeMapping
default void beforeMappingToDTO(Entity source, #MappingTarget Dto target) {
target.setComment(commentMapper.commentToCommentDto(commentRepository.findById(entity.commentId).orElse(null)));
}
#Spektakulatius answer solved a problem.
To reach a goal I made a few steps:
First of all, I created an object of my mapper to use it in a java stream:
private CommentMapper commentMapper = Mappers.getMapper(CommentMapper.class);
In the last step I used my object commentMapper like:
public Optional<CommentDto> findCommentById(final long id) {
return commentRepository.findById(id).map(CommentMapper.MAPPER::commentToCommentDto);
}
After all that completely gave me a possibility to use my custom MapStruct mapper in stream.

MapStruct Map Object to List

I am trying to use Mapstruct to map source object to a target list. What should be a clean mapstruct way of doing this?
Below are my DTO's.
Source DTO
#Data
class Source
{
String a;
String b;
String C;
}
Target DTO
#Data
class Target
{
String name;
List<Child> customList;
}
#Data
class Child
{
String attr1;
boolean attr2;
}
I am facing issues with Mapper Class. Trying to achieve something like below.
public interface CustomMapper
{
#Mapper(target="customList" expression="java(new Child(a,false))"
#Mapper(target="customList" expression="java(new Child(b,true))"
#Mapper(target="customList" expression="java(new Child(c,false))"
Target sourceToTarget(Source source);
}
I don't want to use qualifiedBy function like below to achieve this, as all conversion needs to be coded for each element.
List<Child> toList(Source source)
{
List<Child> customList = new ArrayList<Child>();
customList.add(new Child(source.getA(),false));
customList.add(new Child(source.getB(),true));
customList.add(new Child(source.getC(),false));
return customList;
}
I have used an expression to solve this problem. The expression is to perform the mapping (for objects, straightforward for Strings), and then convert it to a list.
#Mapping(target = "names", expression = "java(Arrays.asList(map(source.getName())))")
TargetObject map(SourceObject source);
TargetName map(SourceName source)
You need to import "Arrays" class in the #Mapper definition as below.
#Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE, componentModel = "spring", imports = {Arrays.class})
I had similar use case where i want to convert and Single Object to List of objects.
As these are very custom requirements, will be hard for mapstruct to provide some API for use cases like this.
I ended up implementing using default method like this
#Mapper(componentModel = "spring")
interface MyMapper {
default List<SomeObject> from(SourceObject sourceObject) {
//Add Mappig logic here
//return the list
}
}
//If you want to use some mapper for mapping
#Mapper(componentModel = "spring")
public abstract class SomeArrayMapper {
#Autowired
SomeMapper mapper;
public SomeUser[] from(SingleObject singleObject) {
SomeUsers[] Users = new SomeUser[1];
Users[0] = mapper.toUser(singleObject);;
return Users ;
}
}
In some cases Decorators can also be useful take a look at here
There is no clean way of doing this at the moment in MapStruct. MapStruct is considerring bean tot map mapping. See here: https://github.com/mapstruct/mapstruct/pull/1744 which might come in helpful once implemented.
However, if you really have a lot of properties and this is a recurring problem, and you dislike reflection - like I do - you might want to give code generation an attempt and DIY. I posted an example some while ago for generating a mapper repository here: https://github.com/mapstruct/mapstruct-examples/tree/master/mapstruct-mapper-repo but, it's a bit of an steep learning curve.. sorry

Java Reflection: Invoking Setter and Getter method for collection type Object

I have two different packages of User define Objects.....
1) ws.lender.dto (all Objects exists in this package are source side).
2) copl.com.dto (all Objects exists in this package are destination side).
Objects hierarchy and Objects name different in both side. I wan to
copy source side object to destination side object field by field or
via getter and setter using Reflection.
For Example
Source side Objects
package ws.lender.dto;
#XmlAccessorType(XmlAccessType.FIELD)
#XmlType(name = "CustomerAddresses", propOrder = {
"previousAddresses"
})
public class CustomerAddresses {
protected PreviousAddresses previousAddresses;
public PreviousAddresses getPreviousAddresses() {
return previousAddresses;
}
public void setPreviousAddresses(PreviousAddresses value) {
this.previousAddresses = value;
}
}
#XmlAccessorType(XmlAccessType.FIELD)
#XmlType(name = "PreviousAddresses", propOrder = {
"previousAddress"
})
public class PreviousAddresses {
#XmlElement(name = "PreviousAddress", required = true)
protected List<PreviousAddress> previousAddress;
public List<PreviousAddress> getPreviousAddress() {
if (previousAddress == null) {
previousAddress = new ArrayList<PreviousAddress>();
}
return this.previousAddress;
}
}
#XmlAccessorType(XmlAccessType.FIELD)
#XmlType(name = "PreviousAddress", propOrder = {
"streetNo",
"streetName"
})
public class PreviousAddress {
#XmlElement(name = "StreetNo", required = true)
protected String streetNo;
#XmlElement(name = "StreetName", required = true)
protected String streetName;
public String getStreetNo() {
return streetNo;
}
public void setStreetNo(String value) {
this.streetNo = value;
}
public String getStreetName() {
return streetName;
}
public void setStreetName(String value) {
this.streetName = value;
}
}
Destination side Objects
package copl.com.dto;
#javax.persistence.Entity
public class Customer implements java.io.Serializable
{
private Set<CustomerAddress> customerAddresses;
public Set<CustomerAddress> getCustomerAddresses()
{
return customerAddresses;
}
public void setCustomerAddresses(Set<CustomerAddress> customerAddresses)
{
this.customerAddresses = customerAddresses;
}
}
#javax.persistence.Entity
public class CustomerAddress implements java.io.Serializable
{
private String unitNumber;
private String streetName;
private String streetNumber;
public String getUnitNumber()
{
return unitNumber;
}
public void setUnitNumber(String unitNumber)
{
this.unitNumber = unitNumber;
}
public String getStreetName()
{
return streetName;
}
public String getStreetNumber()
{
return streetNumber;
}
public void setStreetName(String streetName)
{
this.streetName = streetName;
}
public void setStreetNumber(String streetNumber)
{
this.streetNumber = streetNumber;
}
}
I think you could use MapStruct to mapping between POJO's that has different attribute names.
But your scenario is complex, because you want to convert ws.lender.dto.CustomerAddresses to copl.com.dto.Customer, and this implies to convert a List<ws.lender.dto.PreviousAddress> contained into a ws.lender.dto.PreviousAddresses object, to a Set<copl.com.dto.CustomerAddress> contained into a copl.com.dto.Customer object.
So, I will explain step by step.
1. Convert from ws.lender.dto.PreviousAddress to copl.com.dto.CustomerAddress
To do this conversion you need an interface (MapStruct will create an instance for this) responsible for mapping from source object to destination object:
import ws.lender.dto.PreviousAddress;
import copl.com.dto.CustomerAddress;
#Mapper
public interface CustomerAddressesMapper{
CustomerAddressesMapper INSTANCE = Mappers.getMapper( CustomerAddressesMapper.class );
#Mappings(#Mapping(source = "streetNo", target = "streetNumber")
CustomerAddress previousToCustomerObject(PreviousAddress address);
}
This interface will map a PreviousAddress object to a CustomerAddress considering that streetNo attribute has to be mapped to streetNumber. There is no mapping for unitNumber attribute cause there is no source for it.
2. Convert a List<ws.lender.dto.PreviousAddress> to Set<copl.com.dto.CustomerAddress>
Now you have to add another mapping method to existing CustomerAddressesMapper interface:
Set<CustomerAddress> previousToCustomerSet(List<PreviousAddress> addresses);
This method will use the former previousToCustomerObject to convert every element of source list to destination set.
3. Convert from ws.lender.dto.CustomerAddresses to copl.com.dto.Customer
Finally, you need to add the last mapping method to CustomerAddressesMapper interface:
#Mappings(#Mapping(source = "previousAddresses.previousAddress", target = "customerAddresses")
Customer customerAddrsToCustomerObject(CustomerAddresses addresses);
This is the where you map an origin object, converting previousAddresses.previousAddress attribute to customerAddresses attribute, using former methods.
4. Using the mapper
To use the mapper you have to write some code as following:
CustomerAddressesMapper mapper = CustomerAddressesMapper.INSTANCE;
CustomerAddresses origin = //Retrieve it from anywhere
Customer dest = mapper.customerAddrsToCustomerObject(origin);
5. Setup
MapStruct is a source code generator, so you need to properly configure your pom.xml to include MapStruct dependency and to invoke this code generation. You can see how to do this here
Well I don't build and run this code, but this is the way to do it.
Hope it helps!
I have studied many objects mapping frameworks for this assignments like
Orika
MapStruct
ModelMapper
Dozer
Commons-BeanUtils
Finally I choose the Orika framework to complete that above Objects to Objects mapping. We can do the that mapping via other mapper framework, but I was like Orika framework because this framework very easy to use for mapping Objects to Objects.
I will explain step by step.
1. Create the Objects of Source side Object and Destination side Object.
like this..
Customer destination = new Customer();
CustomerAddresses source = new CustomerAddresses();
source = filledCustomerAddressesObject();
2. Constructing the DefaultMapperFactory
MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
3. Mapping fields
ClassMapBuilder<CustomerAddresses, Customer> builder;
builder= mapperFactory.classMap(CustomerAddresses.class, Customer.class).constructorA();
builder.field("previousAddresses.previousAddress{streetNo}","customerAddresses{streetNumber}");
builder.field("previousAddresses.previousAddress{streetName}","customerAddresses{streetName}");
builder.register();
BoundMapperFacade<CustomerAddresses, Customer> boundMapper;
boundMapper = mapperFactory.getMapperFacade(CustomerAddresses.class, Customer.class);
destination = boundMapper.map(source, destination);
Its work fine Cheers
You can try Object Mapper better for casting or copying. The object to other classes in other packages you can add some property value like
senderClass and rvcClass
Later you can read those properties and proceed for converting the class. Probably you already have mapping ready for sender class against receiver class.
If I understand you correctly, you need a way to copy all like-named properties from one object to another. Like-named properties would be cases in which the source object has a method called something like getPropertyName() and the destination object has one called setPropertyName().
If this is right, then you want to use the copyProperties method of the BeanUtils class from the Apache Commons library. Documentation is here.
Now, in your example, you have some corresponding properties that are not like-named, such as StreetNumber and StreetNo. I'm afraid there's no easy way to handle that sort of thing automatically through reflection; you would need to define the mappings between source and target properties yourself, perhaps by defining a helper class to do the copying.

Categories

Resources