-
Notifications
You must be signed in to change notification settings - Fork 0
Home
GMapper is a simple groovy library that can translate/convert POJOs to other POJOs using groovy closures for custom logic during the conversion. It depends on Groovy and Spring. GMapper achieves this by promoting DRY methodology and self documenting code. The resulting code is easy to maintain and promotes reuse.
Java programmers run into many cases that needs them to convert one POJO to another POJO. These cases include but are not limited to.
-
You need to represent database mapped bean to another bean for the sake of easier usability from frontend technologies such as JSP, JSF.
-
You are writing custom migration code that moves relational data to another representation such as nosql that requires changes in domain schemas.
-
You are using JAX-RS or other custom code to represent your bean to another representation that drives your service oriented architecture.
-
You are using polyglot persistence and you need to represent two beans from separate datasources into a single POJO using different conversion process
There are library such as Apache Commons BeanUtils that maybe used to copy properties from one bean to another bean, but it does not account for cases where you need to inject custom logic during copying.
What you tend to write are tons of lines of getter and setter with some custom logic in your conversion services that are hard to reuse and maintain.
Gmapper is comprised of couple of annotations and a tranformer.
Using the transformer is as simple as
OldObject oldObject
DomainTransformer dt = new DomainTrasformer();
NewObject newObject = dt.transform(NewObject.class, oldObject);
There are two ways of embedding the conversion logic. The logic of how to each field is mapped can be either
- embedded into the NewObject.class itself
- or it can be extracted out into another class.
I'll first go over how to set up the conversion logic within the NewObject class.
All you need is to set up a public static method that returns a map composed of fieldnames and closures. Let's begin with example.
public class OldObject {
Integer id;
String firstName;
String lastName;
String highSchoolName;
Double highSchoolGPA;
}
public class NewObject {
Integer id;
String firstName;
String lastName;
String fullName;
HighSchoolInfo highSchoolInfo;
}
public class HighSchoolInfo {
String name;
Double gpa;
}
Here note that high school information has been converted to a whole new object and we have additional field called 'fullName'. So let's come up with the map that converts the OldObject into NewObject.
public class NewObject {
Integer id;
String firstName;
String lastName;
String fullName;
HighSchoolInfo highSchoolInfo;
@Mapping(value = MappingType.FULL, originalClasses = [OldObject.class])
public static Map anyMethodName() {
[
id : {it.id}, //Or be explicit {OldObject oldObject -> oldObject.id}
firstName : {it.firstName},
lastName : {it.lastName},
fullName : {it.firstName + " " + it.lastName},
highSchoolInfo : { new HighSchoolInfo() },
"highSchoolInfo.name" : {it.highSchoolName},
"highSchoolInfo.gpa" : { Precision.round(it.highSchoolGPA,2)}
]
}
}
Couple things to note.
- Name of the method does not matter.
- To support key with a dot, key string was explicitly wrapped in double quotes.
- Note that originalClasses is an array of class.
MappingType is optional. Unless you need to, you will most likely only use MappingType.FULL which is the default. To learn more about MappingType, see this page
OriginalClasses is also optional unless you need to support multiple datasources, but is recommended to keep the code self documenting. To learn more about multiple data sources, see this page
If you need to merge more than one source objects to NewObject you would list all the classes in the originalClasses array. Calling the transformer using varargs.
dt.tranform(NewObject.class, firstOldObject, secondOldObject)
Then, the closures will have to account for multiple parameters {FirstOldObject f1, SecondOldObject f2 -> f1.id}
Now, what if you have complex object hierarchy you want the conversion to be independent of where it is used?
Here is an example
public class OldObject {
Integer id;
String firstName;
String lastName;
String highSchoolName;
Double highSchoolGPA;
OldAddress address
}
public class NewObject {
Integer id;
String firstName;
String lastName;
String fullName;
HighSchoolInfo highSchoolInfo;
NewAddress address;
}
public class OldAddress {
String address
String zipcode
}
public class NewAddress {
String line1
String line2
String zipcode
}
Let's say your NewAddress class is used in classes other than NewObject class. So you cannot have the mapping inside NewObject class. You need to have the mapping inside the NewAddress class.
@MappedClass
public class NewAddress {
String line1
String line2
String zipcode
@Mapping(MappingType.FULL, originalClasses=[OldAddress.class])
pubic static Map getMapping() {
[
line1 : {someStaticMethodParsingForFirstLine(it.address)},
line2 : {someStaticMethodParsingForSecondLine(it.address)}
]
}
}
public class NewObject {
Integer id;
String firstName;
String lastName;
String fullName;
HighSchoolInfo highSchoolInfo;
NewAddress newAddress;
@Mapping(value = MappingType.FULL, originalClasses = [OldObject.class])
public static Map anyMethodName() {
[
id : {it.id}, //Or be explicit {OldObject oldObject -> oldObject.id}
firstName : {it.firstName},
lastName : {it.lastName},
fullName : {it.firstName + " " + it.lastName},
highSchoolInfo : { new HighSchoolInfo() },
"highSchoolInfo.name" : {it.highSchoolName},
"highSchoolInfo.gpa" : { Precision.round(it.highSchoolGPA,2)},
newAddress : {it.address} //Simply passing OldAddress
]
}
}
What we did was annotate the NewAddress class with @MappedClass annotation. This will tell the transformer that whenever you see this class as a field, it needs to recursively perform the conversion. So we can simply pass the OldAddress in the closure.
In my experience, whenever your class has a mapping, I tend to always annotate the class with @MappedClass so I don't have to worry about it being used else where.
There are both pros and cons of embedding your new domain class with mapping method. You gain self documenting code by having all your conversion code at the same place as the domain class. But this approach also clutters your domain class with extra code that is only used during the conversion process. Another short fall of having the static method in the domain class is that you cannot use Spring managed bean(services) inside the closures.
To get around this problem, you can optionally extract out the mapping from your domain class using @MappingClass annotation
Here is our new example of NewAddress
@MappedClass
@MappingClass(NewAddressMapping.class)
public class NewAddress {
String line1
String line2
String zipcode
}
@Component
public class NewAddressMapping {
@Autowired
AddressParserService addressParserService;
@Mapping(MappingType.FULL, originalClasses=[OldAddress.class])
pubic Map getMapping() { //NOT STATIC
[
line1 : {addressParserService.parseForFirstLine(it.address)},
line2 : {addressParserService.parseForSecondLine(it.address)}
]
}
}
Here we've extracted our mapping from the domain class into a Spring managed bean. This means we are free to use any Spring manage services from the closures.
*Also note that the method that returns the map is no longer static.