Fork me on GitHub

S3lm4 logo

Selma - Java bean mapping that compiles !

Selma stand for Stupid Simple Statically Linked Mapper, it generates static code to map properties from one bean to another. We Keep It Stupid Simple: convention over configuration, light footprint, easy ot use, easy to learn, are Selma's roots. On one side, Selma is an Annotation Processor that generate Java code to handle the mapping from field to field at compile time. On the over side, it is an API to instantiate and invoke the generated Mapper implementation.

Why another Java mapping framework ?

Existing frameworks massively use reflection API to build dynamically the maping code. Well let me see, it's fine ! In fact, it can be, but the better mapping framework I know is vanilla Java code with it's long list of if conditions. Because performance are far better, and I have total control on the mapping. More over, the static code is refactoring proof, and easy to debug. But that is painful :p. Writing all this code is always my preferred part, almost always since there is sometime more than 3 poor little beans. That's where Selma comes, the idea is simple get advantages of the manual mapping without the pain.

The 1 minute tutorial

First add Selma to your pom

<!-- scope provided because the processor is only needed for the compiler -->
    <dependency>
        <groupId>fr.xebia.extras</groupId>
        <artifactId>selma-processor</artifactId>
        <version>0.7</version>
        <scope>provided</scope>
    </dependency>

    <!-- This is the only real dependency you will have in youre binaries -->
    <dependency>
        <groupId>fr.xebia.extras</groupId>
        <artifactId>selma</artifactId>
        <version>0.7</version>
    </dependency>

There are 2 artifacts

Then declare your mapper

@Mapper
public interface SelmaMapper {
    OutBean asOutBean(InBean in);
}

Here we've defined SelmaMapper which maps InBean to an instance of OutBean.

Finally, use the mapper

// Get SelmaMapper
SelmaMapper mapper = Selma.getMapper(SelmaMapper.class);

// Map my InBean
OutBean res = mapper.asOutBean(in);

Build and enjoy!

Any issue will be reported in the compiler log, it will also give you fix tips.

Check our sample app project in github.

News

Brand new release 0.7
Few bugs were fixed for this release:

See release notes or Milestone.

Brand new release 0.6
Many performance improvements have been done, immutable class mapping does not generate null checks anymore.
We also added support for BigDecimal, and fixed the collections size constructor bug (generated code always passed a size parameter to the constructor even if not existing).
Final big feature is the new builder style API that allows to use multiple data sources and multiple custom mappers.
See release notes or Milestone.

Documentation

This is where you go when get stuck ! First keep in mind that Selma knows only one thing, generate implementation code for mapper interface.

Mapping

Selma match fields using setter and getter names. What does this mean ? Selma match can map only same field names. We only use setters and getters so don't worry about the inner field name.

By default, missing properties aka properties from in bean does not match any field in out bean and reverse, will break compilation.

Let see an example

First the model object a Person :

public class Person {

    private String firstName;
    private String lastName;
    private Date birthDay;
    private int age;
    private Long[] indices;
    private Collection tags;

    // + Getters and Setters
}

Then the DTO a PersonDto :

public class PersonDto {

    private String firstName;
    private String lastName;
    private Date birthDay;
    private int age;
    private Long[] indices;
    private List tags;

    // + Getters and Setters
}

Now we define the Mapper interface :

@Mapper
    public interface PersonMapper {

    PersonDto asPersonDto(Person in);
}

All properties are exactly matching so this will work.

Generated code is:

// GENERATED BY S3LM4
package fr.xebia.extras.selma.beans;


public final class PersonMapperSelmaGeneratedClass implements PersonMapper {


    /**
    * Single constructor
    */
    public PersonMapperSelmaGeneratedClass() {
    }

    /**
    * Mapping method overridden by Selma
    */
    @Override
    public final PersonDto asPersonDto(Person in) {
        fr.xebia.extras.selma.beans.PersonDto out = null;
        if (in != null) {
            out = new fr.xebia.extras.selma.beans.PersonDto();
            if (in.getTags() != null) {
                java.util.ArrayList atagsTmpCollection = new java.util.ArrayList(in.getTags().size());
                out.setTags(atagsTmpCollection);
                for (java.lang.String atagsItem : in.getTags()) {
                    atagsTmpCollection.add(atagsItem);
                }
            } else {
                out.setTags(null);
            }
            if (in.getBirthDay() != null) {
                out.setBirthDay(new java.util.Date(in.getBirthDay().getTime()));
            }else {
                out.setBirthDay(null);
            }
            out.setAge(in.getAge());
            if (in.getIndices() != null) {
                java.lang.Long[] aindicesTmpArray = new java.lang.Long[in.getIndices().length];
                int aindicesTotalCount = in.getIndices().length;
                out.setIndices(aindicesTmpArray);
                for (int aindicesIndex = 0 ; aindicesIndex < aindicesTotalCount; aindicesIndex++) {
                    aindicesTmpArray[aindicesIndex] = in.getIndices()[aindicesIndex];
                }
            }
            else {
                out.setIndices(null);
            }
            out.setLastName(in.getLastName());
            out.setFirstName(in.getFirstName());
        }
        return out;
    }
}

Add the generated source to youre IDE
Using a maven build, you will find the generated sources under target/generated-sources/annotations.

Mapper class

All mappers are generated using the name of the interface with a suffix SelmaGeneratedClass. The class are generated in the same package as the interface and declared final.

Null values

Selma generated code always protect against NPE. The mapper will process mapping if value is not null else the mapper will explicitly set null to the destination bean.

Strict copy

Selma warrants immutability of the in bean, all values setted in the destination bean are strict copy of original values except for already immutable classes like String, BigDecimal, Enum, ...

Ignore fields

Selma makes strict copy, so it will try to match each single property. When matching properties are not found in source or destination bean, the compiler will fail reporting missing properties.

At this point, you have 2 solutions:

Scope the @IgnoreFields

You can define this annotation either at the class level of the Mapper interface, or for specific mapping methods.


@Mapper
@IgnoreFields({"name", "size"}) // Ignore "name" and "size" properties for all beans
public interface IgnoreFieldMapper {

    PersonOut asPersonOut(PersonIn in);

    @IgnoreFields({"id"})   // Ignore "id" only for the CityIn/Out beans
    CityOut asCityOut(CityIn in);

}

Qualify @IgnoreFields

The @IgnoredField support 3 notations to qualify the property to ignore:

  1. Simple property name
  2. Simple class name with propertie (MyClass.name)
  3. Fully qualified class name with propertie (org.package.MyClass.name)

        @IgnoreFields("name")  // Will ignore all fields named name

        @IgnoreFields("Library.name") // Will ignore name field if found in any Library class

        @IgnoreFields("fr.xebia.extras.selma.Library.name") // Will Ignore name field for FQCN Library class.

Enumerations

Default behavior, is to consider that all values in source enum exists in destination enum. We take great care to build a separate method that will be used every time the same enum to enum mapping is needed. This method maps using a Switch block without default.

In some situation, you'll need more than the default behavior. The EnumMapper annotation is there for you, with it, Selma will use a slightly different strategy. The processor will still build a separate method but this time it map only identical values from both @hwithwithEnums and use the given default value. This annotation can be used at interface level inside the Mapper annotation.

@Mapper( withwithwithEnums = @EnumMapper(from=EnumA.class, to=EnumB.class, defaultValue="UNKNOWN"))
public interface PersonMapper {

    PersonDto asPersonDto(Person in);

}
         

But you can also use it at method level.

@Mapper
public interface PersonMapper {

    @EnumMapper(defaultValue="UNKNOWN")
    EnumB asEnumB(EnumA in);

}
         

All parameters are optional but ...
At class level, you need to specify from and to, if you don't specify defaultValue it will defaults to null in generated code. At method level, if in and out types are withEnums, you won't need to specify from and to.

Field name mapping

By default the generated mapper, will map fields with same name. But you can also specify a custom name to name mapping using the @Field annotation.

@Field are defined in the @Fields annotation, it contains the list of custom field name mapping. See below, we map the field "nom" to "lastname" and the field "prenom" to "firstname".

@Mapper(ignoreMissingProperties = true)
@Fields({
        @Field({"nom", "lastname"}), @Field({"prenom", "firstname"})
})
public interface CustomFieldMapper {

    SimplePersonDto convertFrom(SimplePerson in);

}
         

@Fields as a scope
In the exemple above, we define two @Field available for the whole mapper implmentation class, but you can also define fields name mapping on a per-method basis. @Fields is your friend for this purpose.

Nested bean

Mapper code will declare a single separated method for each nested bean in the graph.

Declare nested bean methods
Selma will override the methods defined in the mapper interface and use it everywhere it is needed. If they are declared, you will be able to use them from the interface.

@Mapper
public interface UseDeclaredMethodMapper {

    /**
     * Handles mapping of PersonIn to PersonOut
     */
    PersonOut asPersonOut(PersonIn in);

    /**
     * Handles mapping of AddressIn to AddressOut.
     * will be called by asPersonOut() method to map persons addresses
     */
    AddressOut asAddressOut(AddressIn in);

     /*
        This way you can also customize your mappings at method level
      */
}
 

Collections and Maps

Collections

Selma detects class implementing java.util.Collection and either use a default implementation, or the given one. This means that Selma can convert any Collection to another Collection class. Let say you have a LinkedList in the inBean and an ArrayList in the out bean or you want to convert from Collection type to a Set.

Maps

Selma will also process the mapping of java.util.Map ensuring that both the Key and the values will be converted as any type. Both key and value are subject to mapping, so Pojo will be mapped in both case.

Defaults

Selma uses default collection implementation, when mapping requires to return an interface. See the table below to find corresponding implementation.

Interface Implementation
Set HashSet
SortedSet TreeSet
BlockingDeque LinkedBlockingDeque
BlockingQueue LinkedBlockingQueue
Queue PriorityQueue
Deque ArrayDeque
List ArrayList
NavigableSet ConcurrentSkipListSet
Map HashMap
ConcurrentMap ConcurrentHashMap
ConcurrentNavigableMap ConcurrentSkipListMap
NavigableMap TreeMap
SortedMap TreeMap
Collection ArrayList

Custom Mapper

Sometimes, Selma will not do what you need. Let say, you want to convert from String to date or from Integer to Float. In Selma, we do not want magic conversion, so converting from boxed Integer to native int and reverse is supported but, we do not support auto-magically convert from int to float because there can be data loss. So you can define youre own custom mapper.

A custom mapper is a class that contains methods one or more methods taking an input parameter (the source bean / value) and return the destination value after hand coded mapping. You only need to add the class to the withCustom attribute of the @Mapper annotation.

Let see it

First, we add an address field to Person and PersonDto classes.

public class Person {

    private String firstName;
    // ...
    private Address address;

               // + Getters and Setters
}

Here address is stored in a nested bean, but in PersonDto:

public class PersonDto {

    private String firstName;
    // ...
    private String address;
    // + Getters and Setters
}

We use a simple String to store the address so on mapping we want to serialize the Address to String in PersonDTO. Now lets define the CustomMapper:

public class AddressMapper {

    public String addressToString (Address in) {
        return in.toString();
    }
}

For simplicity, we use the toString() method to demonstrate the feature. The method name is not relevant since Selma only match methods based on the modifier, return type and single parameter type. Compiler will report Custom Mapper miss use.

Finally, we should define the Mapper interface:

@Mapper( withCustom = AddressMapper.class )
public interface PersonMapper {

    PersonDto asPersonDto(Person in);

}

Here we are, the custom mapper is now called from Selma generated base code to map our address field from Address class to String.

Custom Mapper Tips
You can define multiple mapping methods in a CustomMapper class. But you can also, define multiple CustomMapper in the @Mapper definition.All custom mapper class should define a public default constructor so the generated class can instantiate it.

Just remember, for selma method name is nothing, just ensure to define the good In/Out type pair.

Injecting your custom mapper instance

Sometimes the default mapping depends on services, as an exemple you need an encryption service to the password from clear string to a ciphered one. Selma allows you to give your custom mapper instance when you get the generated Mapper from Selma.

            // Here we inject the AddressMapper instance to the Selma Mapper
Selma.getMapperWithCustom(PersonMapper.class, new AddressMapper());

Mapper interceptor

Not convince by custom mapper, you need to do special things on the source or target bean after the mapping occurs. Selma, allows you to define a hook in the custom mapper which will be executed after the mapping itself.

You just need to define a method returning void and taking two parameters the in bean and the out bean. See the example below:

public class AddressMapper {

    public void addressToString (Address in, AddressDto out) {
        // Do some processing here the out is already filled in using in.
    }
}

Sourced beans

Sourced beans ? What do you mean ?
In some case, the beans needs to be bound to DataSource. With most libraries you'll need to provide a Factory that will be called to get an instance of the targeted bean. But this kind of behaviour drives the developer to use Reflection during the mapping. STOP, this breaks our philosophy because we want static compile code.

So how does Selma do ? Well, what we support is passing one or more object to the bean constructor. Doing this we are able to pass a DataSource object to the bean. The only, constraints are to pass the source to Selma when retrieving the generated Mapper and to declare sources type in the mapper interface.

@Mapper(withSources = DataSource.class)
public interface SourcedBeanMapper {

    AddressOutWithDataSource asAddressOut(AddressIn in);
}

Below is the out class:

public class AddressOutWithDataSource {

    // ...
    public final DataSource dataSource;

    public AddressOutWithDataSource(DataSource dataSource) {
       this.dataSource = dataSource;
    }

    // ...
}

Now you can retrieve, the mapper by passing the dataSource:

       DataSource dataSource = new DataSource();
       SourcedBeanMapper mapper = Selma.getMapper(SourcedBeanMapper.class, dataSource);

Never forget constructors
For sourced beans, Selma will call a constructor passing the source in the same exact order as defined in @Mapper. So ensure, that you have defined the constructors, and don't forget custom mappers !

Builder style API

Selma offers a builder API to configure and build the mapper instance. This is the only way to use multiple data sources and / or multiple custom mapper.

       MyMapper mapper = Selma.builder(MyMapper.class).
                                withCustom(customMappers).
                                withSource(dataSources).build();