Sunday, December 25, 2016

MapStruct: Transferring data from one bean to another

Converting data from one form to another is a highly utilized concept in IT industry. MapStruct allows annotation based bean conversion by generating mapper implementation at compile time. This makes sure there is no performance overhead at run-time.

 

What is MapStruct?

MapStruct is a code generator that greatly simplifies the implementation of mappings between Java bean types based on a convention over configuration approach.
The generated mapping code uses plain method invocations and thus is fast, type-safe and easy to understand.

Why MapStruct?

Multi-layered applications often require to map between different object models (e.g. entities and DTOs). Writing such mapping code is a tedious and error-prone task. MapStruct aims at simplifying this work by automating it as much as possible.
In contrast to other mapping frameworks MapStruct generates bean mappings at compile-time which ensures a high performance, allows for fast developer feedback and thorough error checking.

Implementation


pom.xml


In web.xml, add "maven-compiler-plugin", and with group id "org.apache.maven.plugins". You can add the specific jdk source/target version and get the latest version available from MapStruct website.

<plugin>
 <groupId>org.apache.maven.plugins</groupId>
 <artifactId>maven-compiler-plugin</artifactId>
 <version>3.5.1</version>
 <configuration>
  <source>1.6</source> <!-- or higher, depending on your project -->
  <target>1.6</target> <!-- or higher, depending on your project -->
  <annotationProcessorPaths>
   <path>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>1.1.0.Beta1</version>
   </path>
  </annotationProcessorPaths>
 </configuration>
</plugin>

Now add the mapstruct jar as a dependency.

<dependency>
 <groupId>org.mapstruct</groupId>
 <artifactId>mapstruct</artifactId>
 <version>1.1.0.Beta1</version>
</dependency>

Problem Statement and Solution

Suppose we have two pojos representing personal and business contacts as mentioned below and we are using both at specific jsps. Now for a functionality where both the contacts are same we need to transfer data from one pojo to another.

PrimaryContact.java

public class PrimaryContact {

 private String name;
 private String phone;
 private String email;

 public PrimaryContact() {
  super();
 }

 public PrimaryContact(String name, String phone, String email) {
  super();
  this.name = name;
  this.phone = phone;
  this.email = email;
 }

 public String getName() {
  return name;
 }

 public void setName(String name) {
  this.name = name;
 }

 public String getPhone() {
  return phone;
 }

 public void setPhone(String phone) {
  this.phone = phone;
 }

 public String getEmail() {
  return email;
 }

 public void setEmail(String email) {
  this.email = email;
 }

 @Override
 public String toString() {
  return "PrimaryContact [name=" + name + ", phone=" + phone + ", email=" + email + "]";
 }

}

BusinessContact.java

public class BusinessContact {

 private String firstName;
 private String lastName;
 private String businessPhone;
 private String businessEmail;
 private String businessCountry;

 public BusinessContact() {
  super();
 }

 public BusinessContact(String firstName, String lastName, String businessPhone, String businessEmail,
   String businessCountry) {
  super();
  this.firstName = firstName;
  this.lastName = lastName;
  this.businessPhone = businessPhone;
  this.businessEmail = businessEmail;
  this.businessCountry = businessCountry;
 }

 public String getFirstName() {
  return firstName;
 }

 public void setFirstName(String firstName) {
  this.firstName = firstName;
 }

 public String getLastName() {
  return lastName;
 }

 public void setLastName(String lastName) {
  this.lastName = lastName;
 }

 public String getBusinessPhone() {
  return businessPhone;
 }

 public void setBusinessPhone(String businessPhone) {
  this.businessPhone = businessPhone;
 }

 public String getBusinessEmail() {
  return businessEmail;
 }

 public void setBusinessEmail(String businessEmail) {
  this.businessEmail = businessEmail;
 }

 public String getBusinessCountry() {
  return businessCountry;
 }

 public void setBusinessCountry(String businessCountry) {
  this.businessCountry = businessCountry;
 }

 @Override
 public String toString() {
  return "BusinessContact [firstName=" + firstName + ", lastName=" + lastName + ", businessPhone=" + businessPhone
    + ", businessEmail=" + businessEmail + ", businessCountry=" + businessCountry + "]";
 }

}

We write a Mapper to transfer the data as below. The annotation @Mappings define which attributes from source pojo will be transferred to specific attribute in target pojo. The annotation define that @InheritInverseConfiguration inverse mapping to be done.

ContactMapper.java

/**
 * @author javareferencegv
 */
@Mapper
@DecoratedWith(ContactMapperDecorator.class)
public interface ContactMapper {
 ContactMapper INSTANCE = Mappers.getMapper(ContactMapper.class);
    
 /**
  * We define only those mappings which doesn't have same signature in source and target
  */   
    @Mappings({ 
     @Mapping(source = "phone", target = "businessPhone"),
     @Mapping(source = "email", target = "businessEmail"),
     @Mapping(target = "businessCountry", constant="USA")
    })
    BusinessContact primaryToBusinessContact(PrimaryContact primary);
    @InheritInverseConfiguration
    PrimaryContact businessToPrimaryContact(BusinessContact business);
   
}

There will be scenarios where mapping in not straight forward and we need custom logic before mapping one attribute to another. One such instance here is that primary contact has full name while business contact has first and last names. In such scenario we use a Decorator to add custom implementation. This is defined annotation @DecoratedWith adding in the mapper. The implementation for decorator is as below :

ContactMapperDecorator.java

public abstract class ContactMapperDecorator implements ContactMapper{
 
 private final ContactMapper delegate;
  
    public ContactMapperDecorator(ContactMapper delegate) {
        this.delegate = delegate;
    }
    
    @Override
    public BusinessContact primaryToBusinessContact(PrimaryContact primary){
     BusinessContact business = delegate.primaryToBusinessContact(primary); //Executes the mapper
     String[] names = primary.getName().split(" ");
     business.setFirstName(names[0]);
     business.setLastName(names[1]);
     return business;
    }
    
    @Override
    public PrimaryContact businessToPrimaryContact(BusinessContact business){
     PrimaryContact primary = delegate.businessToPrimaryContact(business); //Executes the mapper
     primary.setName(business.getFirstName() + " " + business.getLastName());
     return primary;
    }
    
}

Execution :

Once we build an implementation class file will be generated by mapstruct. We are all set to run the mapper.:
public class ContactConvertor {

 public static void main(String[] args) {
  PrimaryContact primary = new PrimaryContact("Jack Sparrow","9999999999","test@javareferencegv.com");
  BusinessContact business = ContactMapper.INSTANCE.primaryToBusinessContact(primary);
  System.out.println(business);
  PrimaryContact primaryConverted = ContactMapper.INSTANCE.businessToPrimaryContact(business);
  System.out.println(primaryConverted);
 }

}

Output :
BusinessContact [firstName=Jack, lastName=Sparrow, businessPhone=9999999999, businessEmail=test@javareferencegv.com, businessCountry=USA]
PrimaryContact [name=Jack Sparrow, phone=9999999999, email=test@javareferencegv.com]

No comments:

Post a Comment