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:

  1. 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.
  2. 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:

  1. 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 and OrderOverviewDto 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 call addMap() 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 using bind() (line 7, 8 and 9).
  2. Build mapper from configuration. Call buildMapper() method to get Mapper instance (line 10).
  3. Preform mappings using Mapper instance. Let see how to perform mapping using Mapper. 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 time
  • beforeMap zero or many times
  • useConvention zero or one time (conventions are discussed later)
  • bind and bindConstant and mapInner methods zero or many times in any order
  • afterMap 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 by excludeDestinationMembers 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 by includeDestinationMembers 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:

  1. Use conventions only at prototype stage and then replace them with regular declarative maps.
  2. 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.
  3. Check if all properties are mapped using failIfNotAllDestinationMembersMapped or failIfNotAllSourceMembersMapped depending on situation. If some properties are mapped by bind, bindConstant or mapInner 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