User manual
This user manual describes how to set up and use bean-cp library. It covers all available features. Before you start you have to have good knowledge of Java programming language version 8 including lambda expressions. If you get any trouble please checkout support page.
Source code of examples from this tutorial is available on GitHub (for browse and for download).
Setting up environment
Firstly, bean-cp requires Java version 8 or later. If you do not have it please download it from Oracle website, install and then continue with this user manual.
You have generally two options to get a copy of bean-cp:
- Download it from this website. Note that bean-cp has two dependencies: Apache Commons Lang and Javassist. File beancp-1.0.1-all.zip contains library and all dependencies. Otherwise you can download from this site only beancp-1.0.1.jar and get dependencies from another source.
- Use build system like Maven. The artifacts can be found in Maven's central repository.
Dependency declaration:
<dependency> <groupId>com.github.erchu</groupId> <artifactId>beancp</artifactId> <version>1.0.1</version> </dependency>
If you want to use different version of Apache Commons Lang or Javassist the recommended way is to download bean-cp source code and execute all bean-cp unit tests against versions you would like to use to prove compatibility.
First mapping
There are few mapping scenarios supported. We will start with declarative map used to map from one JavaBean to another JavaBean.
Let's start with simple example. Suppose we have following two classes in our domain model:
public class Customer { private long id; private String fullName; public long getId() { return id; } public void setId(long id) { this.id = id; } public String getFullName() { return fullName; } public void setFullName(String fullName) { this.fullName = fullName; } } public class Order { private long id; private Customer customer; private BigDecimal totalAmount; public long getId() { return id; } public void setId(long id) { this.id = id; } public Customer getCustomer() { return customer; } public void setCustomer(Customer customer) { this.customer = customer; } public BigDecimal getTotalAmount() { return totalAmount; } public void setTotalAmount(BigDecimal totalAmount) { this.totalAmount = totalAmount; } }
Our task is to map Order
to OrderDto
:
public static class OrderOverviewDto { private long id; private String customerFullName; private BigDecimal totalAmount; public long getId() { return id; } public void setId(long id) { this.id = id; } public String getCustomerFullName() { return customerFullName; } public void setCustomerFullName(String customerFullName) { this.customerFullName = customerFullName; } public BigDecimal getTotalAmount() { return totalAmount; } public void setTotalAmount(BigDecimal totalAmount) { this.totalAmount = totalAmount; } }
Hare is how to do that in bean-cp way:
Mapper mapper = new MapperBuilder() .addMap( Order.class, OrderOverviewDto.class, (conf, source, destination) -> conf .bind(source::getId, destination::setId) .bind(source.getCustomer()::getFullName, destination::setCustomerFullName) .bind(source::getTotalAmount, destination::setTotalAmount)) .buildMapper();
There are three steps:
- Configure mapper using MapperBuilder.
To configure mapper you need to create instance of MapperBuilder
(line 2) and add one or mapping configurations (line 3 - 9). Each mapping configuration declares source and destination class (
Order
andOrderOverviewDto
in our example). Source and destination classes must have default (with no arguments) public or protected constructor, must have not been final and cannot be inner non-static classes. There are three options to add mapping configuration: declarative map, converter and convention. In this section we will focus on declarative maps. They are used to map from one JavaBean to another. To add declarative map calladdMap()
method (line 3). This method takes three parameters: source class (in our example:Order
), destination class (in our example:OrderOverviewDto
) and map declaration. Map declaration is lambda expression taking three parameters: configuration reference, source object reference and destination object reference (line 6). Finally we define map using series of operations like binding from source member to destination member usingbind()
(line 7, 8 and 9). - Build mapper from configuration. Call buildMapper() method to get Mapper instance (line 10).
- Preform mappings using
Mapper
instance. Let see how to perform mapping usingMapper
. There are two options here. If you already have destination object instance and only want to fill it with data from source object then you write code like this:Order order = new Order(); OrderOverviewDto destination = new OrderOverviewDto(); // Some other stuff here mapper.map(order, destination);
Otherwise bean-cp can create destination object instance for you:Order order = new Order(); // Some other stuff here OrderOverviewDto destination = mapper.map(order, OrderOverviewDto.class);
That's it! First mapping is ready.
Declarative maps in deep
Bind constant
Destination member can be bind to constant using bindConstant
. For example
bindConstant("", destination::setCustomerFullName)
will bind empty string to customerFullName
property.
Calculated members
Method bind
supports also calculated members. For example:
bind( () -> source.getTotalAmount().setScale(2, RoundingMode.CEILING), destination::setTotalAmount)
calculates rounded value of totalAmount
value. Note that name of setScale
method of BigDecimal
class can be misleading, because it do not modifies current instance but returns new one (see
Java Platform API specification).
Term "calculated member" is not limited to mathematical calculations – any expression which returns value of proper type can be used. Those
calculation should have no side effects, especially should not modify source object.
Conditional mapping
Moreover bind
and bindConstant
methods support BindingOptions
. This allows to perform conditional
mappings and null substitution. Mapping condition defines when to execute mapping. For example following binding will map only positive
values of totalAmount
property:
bind( source::getTotalAmount, destination::setTotalAmount, BindingOption.mapWhen(() -> source.getTotalAmount().compareTo(BigDecimal.ZERO) > 0)
Null substitution
Binding options can be also used to substitute null
value with specified value. For example following binding will substitute
null
value with "unknown"
string.
bind( source.getCustomer()::getFullName, destination::setCustomerFullName, BindingOption.withNullSubstitution("unknown"))
Map inner objects
Suppose we have in our application classes presented on the below diagram:
Now suppose you want to map Line
to this model:
What is new about this situation (comparing to previous examples) is need for inner object mapping (object graph in other words).
LineDto
do not references Point
, but references PointDto
. This means that we need to map
Point
to PointDto
in first step and then map Line
to LineDto
. Our map configuration
should look like this:
Mapper mapper = new MapperBuilder() .addMap(Point.class, PointDto.class, (conf, source, destination) -> conf .bind(source::getX, destination::setX) .bind(source::getY, destination::setY)) .addMap(Line.class, LineDto.class, (conf, source, destination) -> conf .mapInner(source::getStart, destination::setStart, PointDto.class) .mapInner(source::getEnd, destination::setEnd, PointDto.class)) .buildMapper();
In line 6 and 7 mapInner
operation is used to map Point
to PointDto
and then assign it to proper
LineDto
's property. If you do not want to always create new PointDto
but map to existing one you will need to
additionally provide destination's member getter:
.mapInner(source::getStart, destination::setStart, destination::getStart, PointDto.class) .mapInner(source::getEnd, destination::setEnd, destination::getEnd, PointDto.class))
Destination object construction
As was already mentioned bean-cp can create destination object during mapping:
OrderOverviewDto destination = mapper.map(order, OrderOverviewDto.class);
By default objects are constructed using no-argument constructor. This could be changed using constructDestinationObjectUsing
declaration. For example suppose that exists OrderOverviewDtoFactory
which should be used to construct OrderOverviewDto
objects. Here is how to use constructDestinationObjectUsing
in that situation (line 5 - 9):
Mapper mapper = new MapperBuilder().addMap( Order.class, OrderOverviewDto.class, (conf, source, destination) -> conf .constructDestinationObjectUsing(() -> { OrderOverviewDto result = OrderOverviewDtoFactory.getOrderOverviewDto(); return result; }) // and so on...
Be aware that constructDestinationObjectUsing
must be first statement before bind
and bindConstant
statements.
Before and after map actions
There are situations when you need to perform an operation before or after mapping. Here is proper code snipped to do that:
Mapper mapper = new MapperBuilder().addMap( Order.class, OrderOverviewDto.class, (conf, source, destination) -> conf .beforeMap(() -> Logger.debug( "Starting mapping of Order (id: " + source.getId() + ")")) // Some other stuff here... .afterMap(() -> Logger.debug( "Finished mapping of Order (id: " + source.getId() + ")")) // and so on...
You can also use mapper reference in "before" and "after" actions:
Mapper mapper = new MapperBuilder().addMap( Order.class, OrderOverviewDto.class, (conf, source, destination) -> conf .beforeMap(mapperRef -> Logger.debug( "Starting mapping of Order (id: " + source.getId() + ") by mapper " + mapperRef)) // Some other stuff here... .afterMap(mapperRef -> Logger.debug( "Finished mapping of Order (id: " + source.getId() + ") by mapper " + mapperRef)) // and so on...
Field binding
In case you need to bind some fields (in example: sourceMember
field to destinationMember
field):
.bind(() -> { return source.sourceMember; }, v -> { destination.destinationMember = v; })
Statement order
Finally, please note that methods must be executed in the following order:
constructDestinationObjectUsing
zero or one timebeforeMap
zero or many timesuseConvention
zero or one time (conventions are discussed later)bind
andbindConstant
andmapInner
methods zero or many times in any orderafterMap
zero or many times
Converters
When and how to create converter?
Time to time you will need to map from or to object which is not JavaBean. There are also situations when object do not have default public or protected constructor. For those kind of objects you will need to write converter. Then converters can be used to map inner objects just like we did in "Map inner objects" section. Good example of valid converter usage is conversion to LocalDate. LocalDate is value-based class, so it is immutable, do not have accessible constructors, and instances are instead instantiated through factory methods. It is not a JavaBean.
When you use converter you generally need to write conversion code by your self. For example this code converts Date to LocalDate.
Date input = // ... Instant instant = source.toInstant(); ZonedDateTime zonedDateTime = instant.atZone(ZoneId.systemDefault()); LocalDate result = zonedDateTime.toLocalDate();
Here is example how to make and register converter:
Mapper mapper = new MapperBuilder() .addConverter(Date.class, LocalDate.class, source -> { Instant instant = source.toInstant(); ZonedDateTime zonedDateTime = instant.atZone(ZoneId.systemDefault()); LocalDate result = zonedDateTime.toLocalDate(); return result; }).buildMapper();
There are few other addConverter
methods, but idea is generally similar. For details please have a look at
MapperBuilder API specification.
Common converters
There are two sets of converters provided with bean-cp: CollectionConverters
and NumberConverters
.
Conversion from array to collection:
Mapper mapper = new MapperBuilder() .addConverter(CollectionConverters.getArrayToCollection(long.class)) .buildMapper(); Collection<?> result = mapper.map(new long[] { 1, 2, 3 }, Collection.class);
Conversion from collection to array:
Mapper mapper = new MapperBuilder() .addConverter(CollectionConverters.getCollectionToArray(String.class)) .buildMapper(); Collection<String> collectionInstance = // ... String[] result = mapper.map(collectionInstance, String[].class);
Number conversion:
Mapper mapper = new MapperBuilder() .addConverter(NumberConverters.get()) .buildMapper(); Double result = mapper.map(1l, Double.class);
Conventions
If you plan to use bean-cp then your application probably contain more than one object model of the same real-life objects. If those models are similar this could result in sequence of code like this:
Mapper mapper = new MapperBuilder() .addMap( UserDto.class, User.class, (conf, source, destination) -> conf .bind(source::getId, destination::setId) .bind(source::getFirstName, destination::setFirstName) .bind(source::getLastName, destination::setLastName) .bind(source::getPassword, destination::setPassword) .bind(source::getPhoneNumber, destination::setPhoneNumber) .bind(source::getEmailAddress, destination::setEmailAddress)) .buildMapper();
In this case it is easy to find out that there is general pattern for matching source and destination properties: bind properties of the same name. In bean-cp component generating bindings according to some patterns is named convention. In other words convention analyse source and destination class structure and generates list of bindings.
Name-based convention
Currently bean-cp provides one name-based convention which matches properties and fields by name (you could also write your own convention – see "Write your own convention").
OK... so how to replace series of bind
operations from last code listing with convention? Use
useConvention(NameBasedMapConvention.get())
method. Full example:
Mapper mapper = new MapperBuilder() .addMap(User.class, UserDto.class, (conf, source, destination) -> conf .useConvention(NameBasedMapConvention.get())) .buildMapper();
If some property is of different type at source than at destination then convention will try to map this inner type in first step. For
example if source::getEmailAddress
returns EmailAddress
class instance and you registered converter from
EmailAddress
to String
and destination::setEmailAddress
accepts String
then
convention will convert instance of EmailAddress
to String
in first step and then assign result of
this conversion to emailAddress
property at destination.
Flattening feature. Name-based convention has few options. One of them is flattening feature. Let's consider
following bind
instruction.
.bind(source.getPrimaryCustomer()::getName, destination::setPrimaryCustomerName)
Above we bind name
property to primaryCustomerName
. Properties' names are different, but if we take into
consideration full "path" then we notice that "primaryCustomer" + "name" = "primaryCustomerName"
(ignoring case). To recognize
such patterns we need to enable flattening feature:
.useConvention(NameBasedMapConvention.get().enableFlattening())
Mix convention with declarative map. Conventions can be mixed with regular bind
, bindConstant
and mapInner
instructions. For example:
Mapper mapper = new MapperBuilder() .addMap(User.class, UserDto.class, (conf, source, destination) -> conf .useConvention(NameBasedMapConvention.get()) .bind( () -> source.getFirstName() + ' ' + source.getLastName(), destination::setFullName)) .buildMapper();
Controlling field/properties mapped by convention. There is option is used to control which fields and properties should be mapped by convention:
includeDestinationMembers
– sets list of destination members which will be included by convention. Each entry must be regular expression matching field name or bean property name (according to beans specification). If not specified (empty array) all members are subject to map by convention. If specified (not empty array) only selected members could be mapped by convention. This list has lower priority that exclude list specified byexcludeDestinationMembers
method. Note that when you put some member on list then it is not guaranteed that it will be mapped — it still have to have matching source's member according to convention configuration.excludeDestinationMembers
– sets list of destination members which will be excluded (ignored) by convention. Each entry must be regular expression matching field name or bean property name (according to beans specification). This list has higher priority that include list specified byincludeDestinationMembers
method.
How to make conventions ready for refactoring?
Conventions are probably the most important feature of bean-cp because they lets you speed up your development.
However there is drawback of this approach: your refactoring tools are not aware of convention. In above example if you change
setEmailAddress
to setEmail
and then e-mail address become unmapped. For large applications this
could be a problem. Here are few tips how to deal with this problem:
- Use conventions only at prototype stage and then replace them with regular declarative maps.
- Write unit tests for your mappings. This is somehow controversial because you speed up development of mapping code, but you spend time at writing low priority unit tests. If the only reason to write such tests is use of convention you probably should not use convention at all.
-
Check if all properties are mapped using
failIfNotAllDestinationMembersMapped
orfailIfNotAllSourceMembersMapped
depending on situation. If some properties are mapped bybind
,bindConstant
ormapInner
instructions you need to exclude it explicitly by convention. Then write simple unit test checking if mapping configuration is valid.public class MapperProvider { @SuppressWarnings("unchecked") public Mapper getMapper() { return new MapperBuilder() .addMap(User.class, UserDto.class, (conf, source, destination) -> conf .useConvention(NameBasedMapConvention.get() .excludeDestinationMembers("FullName") .failIfNotAllDestinationMembersMapped()) .bind( () -> source.getFirstName() + ' ' + source.getLastName(), destination::setFullName)) .buildMapper(); } } public class MapperProviderTest { @Test public void mapper_configuration_is_valid() { // This test only check if getMapper() will not throw any exception new MapperProvider().getMapper(); } }
Map any convention
What is more you can define (one or more) conventions used only if no specific mapping from type any A to B is defined:
Mapper mapper = new MapperBuilder() .addMapAnyByConvention(NameBasedMapConvention.get()) .buildMapper();
Write your own convention
It is really easy. You need to only implement MapConvention
interface:
public interface MapConvention { /** * Returns list of bindings for specified source and destination classes. Must * be thread-safe. * * @param mappingsInfo current mapping information. * @param sourceClass source class. * @param destinationClass destination class. * @return found bindings. */ List<Binding> getBindings( final MappingInfo mappingsInfo, final Class sourceClass, final Class destinationClass); }
Implementation must be thread-safe. Be aware that Binding
class has two subclasses available
BindingWithValueConversion
, BindingWithValueMap
and you can write your own if you need to.
Put it all together
Where are almost done. What I like to show you to sum up is bigger example combining features already described in this user manual. If you are not interested in such example please skip this section, but do not forget to read summary section.
package com.github.erchu.beancp.tutorial; import java.util.Collection; import java.util.Date; import java.util.LinkedList; import java.util.Random; import java.util.concurrent.ExecutionException; import org.junit.Test; import com.github.erchu.beancp.Mapper; import com.github.erchu.beancp.MapperBuilder; import com.github.erchu.beancp.commons.CollectionConverters; import com.github.erchu.beancp.commons.NameBasedMapConvention; import com.github.erchu.beancp.commons.NumberConverters; public class _21_Conventions_Put_it_all_together { public static class AuditLog { // Test addMapAnyByConvention private Date createdOn; private Date updatedOn; public Date getCreatedOn() { return createdOn; } public void setCreatedOn(Date createdOn) { this.createdOn = createdOn; } public Date getUpdatedOn() { return updatedOn; } public void setUpdatedOn(Date updatedOn) { this.updatedOn = updatedOn; } } public static class AuthorInfo { // Test converter (to String) private String name; public static AuthorInfo getFromName(final String name) { AuthorInfo result = new AuthorInfo(); result.name = name; return result; } public String getName() { return name; } } public static class PointExtension { // Test flattening private Long z; // Test NumberConverter private final Collection<Integer> otherDimensions; // Test CollectionConverters public PointExtension() { this.otherDimensions = new LinkedList<>(); } public Collection<Integer> getOtherDimensions() { return otherDimensions; } public Long getZ() { return z; } public void setZ(Long z) { this.z = z; } } public static class Point { // Test NameBasedConvention, including // failIfNotAllDestinationMembersMapped option private int x; // Test DeclarativeMap.bind() public int y; // Test DeclarativeMap.bind() private AuthorInfo author; private AuditLog audit; private PointExtension extension; public AuditLog getAudit() { return audit; } public void setAudit(AuditLog audit) { this.audit = audit; } public int getX() { return x; } public void setX(int x) { this.x = x; } public PointExtension getExtension() { return extension; } public void setExtension(PointExtension extension) { this.extension = extension; } public AuthorInfo getAuthor() { return author; } public void setAuthor(AuthorInfo author) { this.author = author; } } public static class PointInfo { private int metric; private int extensionZ; private int[] extensionOtherDimensions; private String author; private AuditLogInfo audit; public AuditLogInfo getAudit() { return audit; } public void setAudit(AuditLogInfo audit) { this.audit = audit; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = author; } public int getMetric() { return metric; } public void setMetric(int metric) { this.metric = metric; } public int getExtensionZ() { return extensionZ; } public void setExtensionZ(int extensionZ) { this.extensionZ = extensionZ; } public int[] getExtensionOtherDimensions() { return extensionOtherDimensions; } public void setExtensionOtherDimensions(int[] extensionOtherDimensions) { this.extensionOtherDimensions = extensionOtherDimensions; } } public static class AuditLogInfo { private Date createdOn; private Date updatedOn; public Date getCreatedOn() { return createdOn; } public void setCreatedOn(Date createdOn) { this.createdOn = createdOn; } public Date getUpdatedOn() { return updatedOn; } public void setUpdatedOn(Date updatedOn) { this.updatedOn = updatedOn; } } private final Random random = new Random(); @Test @SuppressWarnings("unchecked") public void mapper_should_map_objects_in_parallel_threads() throws InterruptedException, ExecutionException { // Configure mapper Mapper mapper = new MapperBuilder() .addMapAnyByConvention(NameBasedMapConvention.get()) .addConverter(AuthorInfo.class, String.class, source -> { return source.getName(); }) .addConverter(CollectionConverters.getCollectionToArray(int.class)) .addConverter(NumberConverters.get()) .addMap( Point.class, PointInfo.class, (config, source, destination) -> config .useConvention(NameBasedMapConvention.get() .enableFlattening() .excludeDestinationMembers("metric") .failIfNotAllDestinationMembersMapped()) .bind(() -> source.getX() + source.y, destination::setMetric)) .buildMapper(); // Sample data PointExtension pointExtension = new PointExtension(); pointExtension.setZ((long) random.nextInt()); int otherDimensionNumber = random.nextInt(10); for (int i = 0 ; i < otherDimensionNumber ; i++) { pointExtension.getOtherDimensions().add(i); } Point source = new Point(); source.setX(random.nextInt()); source.y = random.nextInt(); source.setExtension(pointExtension); source.setAuthor(AuthorInfo.getFromName("U" + random.nextInt())); AuditLog auditLog = new AuditLog(); auditLog.setCreatedOn(new Date()); auditLog.setUpdatedOn(new Date()); source.setAudit(auditLog); // Map @SuppressWarnings("unused") PointInfo result = mapper.map(source, PointInfo.class); } }
Summary
bean-cp is relatively new library, so your freedback and any other contribution is very, very important. If you have any thoughts about bean-cp please share them with us on bean-cp project forum on "What do you think about bean-cp?" thread.
One more thing. Apart of user manual you may also find helpful to read unit tests source code included in beancp-1.0.1-sources.jar