I have a question about Lombok & MapStruct combination.
First, the class is as follows.
Person.java
#Data
#Builder
#NoArgsConstructor
#AllArgsConstructor
public class Person {
private String name;
#JsonProperty("name")
public void setCustomName(String name) {
this.name = "custom = " + name;
}
#JsonProperty("name")
public String getCustomName() {
return this.name;
}
public static class PersonBuilder {
public PersonBuilder customName(String name) {
return this.customName(name);
}
}
}
PersonMapper.java
#Mapper
public interface PersonMapper {
#Mapping(target = "customName", source = "personName")
#Mapping(target = "name", ignore = true)
Person toEntity(PersonRequest req);
}
I'm find that #Mapper generating code depends on dependencies order (Lombok, MapStruct)
If i use Lombok first and MapStruct second, 'XXMapperImpl.java' use setter method.
but use in reversed order, 'XXMapperImpl.java' use builder method.
// dependencies order: Lombok's next MapStruct
#Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2021-01-25T18:59:21+0900",
comments = "version: 1.3.1.Final, compiler: javac, environment: Java 14.0.2 (AdoptOpenJDK)"
)
public class PersonMapperImpl implements PersonMapper {
#Override
public Person toEntity(PersonRequest req) {
if ( req == null ) {
return null;
}
Person person = new Person();
person.setCustomName( req.getPersonName() );
return person;
}
}
but reversed order
// dependencies order: Lombok's next MapStruct
#Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2021-01-25T18:59:21+0900",
comments = "version: 1.3.1.Final, compiler: javac, environment: Java 14.0.2 (AdoptOpenJDK)"
)
public class PersonMapperImpl implements PersonMapper {
#Override
public Person toEntity(PersonRequest req) {
if ( req == null ) {
return null;
}
PersonBuilder person = Person.builder();
person.customName( req.getPersonName() );
return person.build();
}
}
So, I decided to guess like this about Annotation Processor(AP)'s process.
Reference link
this case is first Lombok, second MapStruct.
Lombok make getter/setter method with #Data
MapStruct can use access method. So, make mapper code by 'setter'
---------- Next round in AP---------
Lombok make builder method with #Builder
MapStruct don't be changed. Because, code already generated.
-> Finally, 'XXMapperImpl.java' use 'setter' method.
Next case is first MapStruct, second Lombok.
MapStruct try to generate, but there is nothing access method in Object class.
Lombok make getter/setter method with #Data.
---------- Next round in AP---------
Lombok make builder method with #Builder.
MapStruct pick up changes created by Lombok. So, MapStruct generate code by using builder.
-> Finally, 'XXMapperImpl.java' use 'builder' method.
Question)
Did I understand correctly?
Is Lombok getter/setter and builder creation divided into two rounds? First is getter/setter and next builder?
Related
I am using MapStruct to convert a database entity to Immutable model object. So Immutable object doesn't have setters but Mapstruct requires setters when mapping objects. So I created an explicit builder using Immutable object builder to provides to Mapstruct. Below are the snippets from code:
#Value.Immutable
#Value.Style(overshadowImplementation = true)
public interface CarModel {
#Nullable String getCarId();
}
#Mapper(uses = ImmutablesBuilderFactory.class)
public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
#Mapping(source = "id", target = "carId")
ImmutableCarModel.Builder toModel(CarEntity carEntity);
}
public class ImmutablesBuilderFactory {
public ImmutableCarModel.Builder createCarModelBuilder() {
return ImmutableCarModel.builder();
}
}
Below code was generated by Mapstruct:
public class CarMapperImpl implements CarMapper {
#Autowired
private final ImmutablesBuilderFactory immutablesBuilderFactory
#Override
public Builder toModel(CarEntity carEntity) {
if ( carEntity == null ) {
return null;
}
Builder builder = immutablesBuilderFactory.createCarModelBuilder();
if ( carEntity.getId() != null ) {
builder.carId( carEntity.getId() );
}
return builder;
}
}
I was able to convert an entity to Immutable model object but unit test is failing for this. It is throwing NPE at below line of code in CarMapperImpl class while calling CarMapper.INSTANCE.toModel(carEntity).build(); in unit test
Builder builder = immutablesBuilderFactory.createCarModelBuilder();
Does anyone have any idea what's going wrong here?
The reason for the NPE is because you are mixing the usage of the default and spring component model.
The Mappers#getMapper is only meant to be used with the default component model. When using a dependency injection framework you need to use the framework to get access to the mapper.
This was due to below property in MapStruct configuration
-Amapstruct.defaultComponentModel=spring
After removing this, Mapstruct was not autowiring and was able to create an instance of ImmutablesBuilderFactory.
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.
Given the following classes and a mapper that takes mulitple source arguments
(I use lombok to keep source as short as possible.)
#Getter
#Setter
public class MySourceOne {
private String src1;
}
#Getter
#Setter
public class MySourceTwo {
private String src2;
}
#Getter
#Setter
public class MyTargetObject {
private String prop1;
private String prop2;
}
#Mapper
public interface MyTargetObjectMapper {
#Mapping(target="prop1", source="a")
#Mapping(target="prop2", source="b")
public MyTargetObject mapMyObject(String a, String b);
}
#Getter
#Setter
public class MyComplexTargetObject {
private MyTargetObject myTargetObject;
}
I am trying to create a mapper for MyComplexTargetObject that will invoke implicitly the MyTargetObjectMapper .
But the "source" won't allow to map multiple parameter like this
#Mapper(uses= {MyTargetObjectMapper.class})
public interface MyComplexTargetObjectMapper {
#Mapping(target="myTargetObject", source="one.src1, two.src2")
public MyComplexTargetObject convert(MySourceOne one, MySourceTwo two);
}
So I am trying to use an expression="..." instead of source, but nothing works so far.
Any thoughts a clean way to do this without calling the MyTargetObjectMapper in a concrete method?
MapStruct does not support selection of methods with multiple sources.
However: you can do target nesting to do this.
#Mapper
public interface MyComplexTargetObjectMapper {
#Mapping(target="myTargetObject.prop1", source="one.src1" )
#Mapping(target="myTargetObject.prop2", source="two.src2")
public MyComplexTargetObject convert(MySourceOne one, MySourceTwo two);
}
And let MapStruct take care of generating the mapper. Note: you can still use a MyComplexTargetObjectMapper to do single source to target to achieve this.
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.
When I want to deserialize an Entity with a polymorph member, Jackson throws a com.fasterxml.jackson.databind.JsonMappingException, complaining about a missing type info (...which is actually present in the JSON -> see example).
Unexpected token (END_OBJECT), expected FIELD_NAME: missing property '#class' that is to contain type id (for class demo.animal.Animal)\n at [Source: N/A; line: -1, column: -1] (through reference chain: demo.home.Home[\"pet\"])"
All actual work is done by a PagingAndSortingRepository from Spring HATEOAS.
I use spring-boot V 1.2.4.RELEASE, which means jackson is V 2.4.6 and Spring HATEOAS is V 0.16.0.RELEASE.
Example:
I have a pet at home:
#Entity
public class Home {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int id;
#OneToOne(cascade = {CascadeType.ALL})
private Animal pet;
public Animal getPet() {
return pet;
}
public void setPet(Animal pet) {
this.pet = pet;
}
}
That Pet is some Animal - in this case either a Cat or a Dog. It's type is identified by the #class property...
#Entity
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "#class")
public abstract class Animal {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int id;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
#Entity
public class Cat extends Animal {
}
#Entity
public class Dog extends Animal {
}
Then there is this handy PagingAndSortingRepository, which allows me to access my home via REST/HATEOAS...
#RepositoryRestResource(collectionResourceRel = "home", path = "home")
public interface HomeRepository extends PagingAndSortingRepository<Home, Integer> {
}
To confirm all that stuff is working, I have a test in place...
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = DemoApplication.class)
#WebAppConfiguration
public class HomeIntegrationTest {
#Autowired
private WebApplicationContext ctx;
private MockMvc mockMvc;
#Before
public void setUp() {
this.mockMvc = webAppContextSetup(ctx).build();
}
#Test
public void testRename() throws Exception {
// I create my home with some cat...
// http://de.wikipedia.org/wiki/Schweizerdeutsch#Wortschatz -> Büsi
MockHttpServletRequestBuilder post = post("/home/")
.content("{\"pet\": {\"#class\": \"demo.animal.Cat\", \"name\": \"Büsi\"}}");
mockMvc.perform(post).andDo(print()).andExpect(status().isCreated());
// Confirm that the POST request works nicely, so the JSON thingy is correct...
MockHttpServletRequestBuilder get1 = get("/home/").accept(MediaType.APPLICATION_JSON);
mockMvc.perform(get1).andDo(print()).andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$._embedded.home", hasSize(1)))
.andExpect(jsonPath("$._embedded.home[0].pet.name", is("Büsi")));
// Now the interesting part: let's give that poor kitty a proper name...
MockHttpServletRequestBuilder put = put("/home/1")
.content("{\"pet\": {\"#class\": \"demo.animal.Cat\", \"name\": \"Beauford\"}}");
mockMvc.perform(put).andDo(print()).andExpect(status().isNoContent());
// PUT will thow JsonMappingException exception about an missing "#class"...
MockHttpServletRequestBuilder get2 = get("/home/").accept(MediaType.APPLICATION_JSON);
mockMvc.perform(get2).andDo(print()).andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$._embedded.home", hasSize(1)))
.andExpect(jsonPath("$._embedded.home[0].pet.name", is("Beaufort")));
}
}
Interestingly I can create my home with the cat as a pet, but when I want to update the name of the cat it cannot deserialize the JSON anymore...
Any suggestions?
I'm going to attempt a half-answer.
When processing a PUT (probably PATCH as well), spring-data-rest-webmvc merges the given JSON data into the existing entity. While doing so, it strips all properties that don't exist in the entity from the JSON tree before passing it to the Jackson ObjectMapper. In other words, your #class property is gone by the time Jackson gets to deserialize your object.
You can work around this (for testing/demonstration purposes) by adding your #class property as an actual property to your entity (you have to rename it of course, say classname). Now everything will work fine, however your entity now has an otherwise useless classname property, which is probably not what you want.
Using the #JsonTypeInfo(use=JsonTypeInfo.Id.NAME, include=As.WRAPPER_OBJECT) approach also won't work, for a similar reason (except this time the entire wrapper object is removed). Also as with the original approach, GET and POST will work fine.
The whole thing looks like a bug or #JsonTypeInfo not supported in spring-data-rest-webmvc situation.
Maybe somebody else can shed some more light on this.