DAMapping framework 0.6.0

This page provides the documentation of the current version (0.6.0) of the DAMapping framework.

To get started using the framework, understand its purpose and usage, please look at the getting started with the DAMapping framework page first.

Dedicated class

A Class that holds a piece of object mapping code and is managed by the DAmapping framework is called a dedicated class.

how to write one

There is very little constraints on how to write a dedicated class:

  1. must be annotated with the @Mapper annotation provided by the framework
  2. must be public or package protected
  3. must define at least one public non-static method
    • it implies that it can not have any “getter” or “setter”, use the constructor to get dependencies
  4. must either define a single non-private constructor or no constructor at all

Content of the dedicated class is totally free, the DAMapping framework cares only of its “interface”.

dedicated enum

Using an enum as a dedicated class is supported by the DAMapping framework.

The only constraint is that it must define only a single value (which name does not matter).

The developer may find it relevant to use an enum in order to make the dedicated class a singleton.

@Mapper
public enum EnumBasedFooToBar {
  THE_ONE;

  @Override
  public Bar apply(@Nullable Foo foo) {
    // [...]
  }
}

mapper method

A “mapper method” is any non-static public method or function defined by the dedicated class.

Functions return void and must define at least two parameters. Methods can return any type but void and have any number of parameters (including none).

usage

The DAMapping framework assumes ownership of dedicated classes, therefor they are not really intended to be used directly except for unit tests.

This implies that the DAMapping framework will assume the responsibility of creating instances of the dedicated class.

naming

Types will be generated by the framework which will be named by appending Mapper or MapperImpl to the name of the dedicated class, it is best to name the dedicated class with that in mind.

E.g. a dedicated class implementing the mapping from an object of type Foo to an object of type Bar could be named FooToBar to get nicely named types FooToBarMapper and FooToBarMapperImpl.

Mapper interface

The DAMapping framework generates the interface that could have been implemented by the dedicated class to achieve loose coupling. This generated interface is known as the Mapper interface.

name and location

The name of the generated Mapper interface is created by appending Mapper to the name of the dedicated class: e.g. the Mapper interface for the dedicated class FooToBar will be named FooToBarMapper.

The Mapper interface is generated in the same package as the dedicated class.

annotations

The generated Mapper interface does not have any annotation except the @Generated annotation which indicates that it is a generated type.

The @Generated annotation is also used by DAMapping to resolve references to generated types in dedicated classes, see Using Mapper interfaces in dedicated class

interfaces: support for Guava’s Function

The generated Mapper interface does not extend any interface with a specific exception: if the dedicated class implements Guava’s Function interface, the Mapper interface will too.

Support for Guava’s Function is currently implemented in the core of the DAMapping framework but may latter be taken out into an extension.

method

If the dedicated class implements Guava’s Function interface, the Mapper interface will define no method.

Otherwise, it defines one method for each “mapper method” with the same signature (including annotations on return type and parameters) as the “mapper method”.

A compilation error will be raised if a dedicated class both implement Guava’s Function interface and define one or more “mapper method”.

example

FooToBarMapper generated from FooToBar exposes the same method as FooToBar, including annotations on the return type and each parameter.

@Mapper
public class FooToBar {
    @Nonnull
    public Bar map(@Nullable Foo foo) {
        // some code returning a Bar instance
        // initialized/populated from the specified Foo instance
    }

    public void update(@Nonnull Foo foo, @Nonnull Bar bar) {
      // some code updating the specified Bar instance from the
      // specified Foo instance
    }
}
@javax.annotation.Generated("fr.javatronic.damapping.processor.DAAnnotationProcessor")
public interface FooToBarMapper {
    @Nonnull
    Bar map(@Nullable Foo foo);

    void update(@Nonnull Foo foo, @Nonnull Bar bar);
}

MapperImpl class

The second type generated by the DAMapping framework is the class that implements the Mapper interface.

The dedicated class could be implementing this interface, but we do not want that. First, it is illegal to modify source code when doing annotation processing and second, we don’t want the framework to alter the developer’s code.

This class is the one supposed to be used in code. It delegates its implementation to a internal instance of the dedicated class (instanced in constructor).

name and location

The name of the MapperImpl class is created by appending MapperImpl to the name of the dedicated class, e.g. the name of the MapperImpl class for the dedicated class FooToBar is FooToBarMapperImpl.

The MapperImpl class is generated in the same package as the dedicated class.

annotations

The MapperImpl class is not generated with class annotation except for the @Generated annotation that indicates the type has been generated.

Also, when the dedicated class is annotated with @Injectable, any annotation annotated with @javax.inject.Scope on the dedicated class will be added to generated MapperImpl class.

Note that @Scope support is currently experimental since it can not be fully used until annotations with @javax.inject.Qualifier are also supported

constructor

If the dedicated class does not define any constructor or only a constructor without parameters, the MapperImpl class will have no explicit constructor.

Otherwise, the dedicated class must defined only one constructor, otherwise an exception will be raised.

The constructor of the MapperImpl class will have the same parameters, including annotations, as the dedicated class constructor.

constructor annotations

The constructor of the MapperImpl class will have no annotation except the @Inject annotation if the dedicated class is annotated with @Injectable.

DI frameworks support

The DAMapping framework provides some support to integrate generated classes and interfaces with a dependency injection framework.

This support is currently part of the core of the framework but it might get provided as an extension in the near future.

The general idea is that the developer has barely any extra thing to do to configure a dedicated class to generate a MapperImpl class to integrate with a DI framework that he or she would have to do to configure a class of its own:

  1. add the @Injectable annotation to the dedicated class, this annotation enables DI framework support (that’s the only extra step)
  2. add any DI related annotation to the dedicated class or the constructor, they will be included on the generated MapperImpl class and its constructor

JSR-330

The DAMapping framework offers a generic support for DI Framework through the JSR-330 annotations.

It is currently limited to generating class types that will declare having dependencies to be injected, exclusively using constructor injection for the moment.

These types can then be declared to the DI framework of the developer’s choice.

@Injectable

The @Injectable annotation provided by the framework can be added to a dedicated class to enable DI framework integration on it.

If the dedicated class declares a non-default constructor, the generated MapperImpl class will declare a constructor with the same parameters (as any Mapper class) but the constructor will additionally be annotated with @javax.inject.Inject.

Also, if the dedicated class has annotations annotated with @javax.inject.Scope, the generated MapperImpl class will also be annotated them.

constructor injection

Currently, the DAMapping framework supports only constructor injection.

Property or setter injection are not supported as it poses specific technical difficulties.

Qualifier annotations

Annotations annotated with @Qualifier on parameters of the constructor of the dedicated class will be added to the parameters of the constructor of the generated MapperImpl class. This enables the use of qualified dependencies in dedicated classes.

@Qualifier annotations can not yet be added to the MapperImpl class itself. This feature is planned for 0.6.0 and will enable features such as Spring’s package scan.

Scope annotations

Annotations annotated with @Scope on the dedicated class will be added to the generated MapperImpl class.

integration samples

You can check out the integration tests of the DAMapping framework to get examples of integration with:

Mapper factory

There might be times:

  • when object mapping code should be configured from a data which can not be injected, which is not available when instantiating the mapper
  • when the mapper should be stateful (hum… code smell?) and therefor a new instance used every times
  • when two or more mappers are only slight variations of the same one

In all theses cases (and maybe some others) you can use a factory which will return multiples implementations and/or instances of a Mapper interface instead of a single one.

@MapperFactory

A Mapper factory is defined as a dedicated class with at least one method annotated with @MapperFactory.

The @MapperFactory annotation can be added to:

  • constructors of the dedicated class
  • or static method(s) of the dedicated class that return the type of the dedicated class

Example of a MapperFactory using a runtime parameter as part of the mapping. Note that it uses a constructor mapper factory and that it implements Guava’s Function interface (the former is completely optional).

@Mapper
public class SaltedBigDecimalToString implements Function<BigDecimal, String> {
  private final String salt;

  @MapperFactory
  public SaltedBigDecimalToString(String salt) {
    this.salt = salt;
  }

  @Nullable
  @Override
  public String apply(@Nullable BigDecimal bigDecimal) {
    return bigDecimal + "-" + salt;
  }
}

specific type generation

Using a MapperFactory causes the DAMapping framework to generate three types instead of two for regular mappers:

  1. the Mapper interface: the same as the one generated from a regular dedicated class
  2. the MapperFactory interface: exposes methods created from the methods or constructors annotated with @MapperFactory
  3. the MapperFactoryImpl class: implements the MapperFactory interface and delegate the implementation of the MapperFactory interface method(s) to the dedicated class

MapperFactory interface

name and location

The name of the MapperFactory interface is created by appending MapperFactory to the name of the dedicated class, e.g. the name of the MapperFactory interface for the dedicated class FooToBar is FooToBarMapperFactory.

The MapperFactory interface is generated in the same package as the dedicated class.

annotations

The generated MapperFactory interface does not have any annotation except the @Generated annotation which indicates that it is a generated type.

interfaces

The generated MapperFactory interface does not extend any interface.

methods

The MapperFactory interface defines as many methods as there is constructors or static methods annotated with@MapperFactory in the dedicated class.

For each static method, a method exists in the MapperFactory interface with:

  • the same name
  • the same parameters (including annotations) as the static method
  • but the return type is the generated Mapper interface instead of the type of the dedicated class

For each constructor, a method exists in the MapperFactory interface with:

  • a constant name “get”
  • the same parameters (including annotations) as the constructor
  • but the return type is the generated Mapper interface

Advice: use a Static method instead of a constructor in order to get fine control over the name of the method exposed in the MapperFactory interface. This method implementation can always use a dedicated class constructor.

There is no restriction on the number of parameters of methods or constructors annotated with @MapperFactory.

Note 1: using both static methods and constructors annotated with @MapperFactory will raise a compilation error
Note 2: methods and constructors annotated with @MapperFactory must be public
Note 3: static methods annotated with @MapperFactory must return the type of the dedicated class

Mapper interface

This Mapper interface is exactly the same has the one generated from a dedicated class without any method or constructor annotated with @MapperFactory.

Below the Mapper interface for the dedicated class is the previous example:

@javax.annotation.Generated("fr.javatronic.damapping.processor.DAAnnotationProcessor")
public interface SaltedBigDecimalToStringMapper extends Function<BigDecimal, String> {
}

MapperFactoryImpl class

name and location

The name of the MapperFactoryImpl class is created by appending MapperFactoryImpl to the name of the dedicated class, e.g. the name of the MapperFactoryImpl class for the dedicated class FooToBar is FooToBarMapperFactoryImpl.

The MapperFactoryImpl class is generated in the same package as the dedicated class.

annotations

The MapperFactoryImpl class is not generated with class annotation except for the @Generated annotation that indicates the type has been generated.

constructor

The MapperFactoryImpl class does not define any constructor.

methods

The MapperFactoryImpl class implements each method defined in the MapperFactory interface by returning an private implementation of the Mapper interface.

The private implementation is a wrapper around the instance of the dedicated class returned by the dedicated class constructor or static method. As the MapperImpl class, this object delegates the implementation of the method from the Mapper interface to its inner dedicated class instance.

Example for the dedicated class from the previous example:

@javax.annotation.Generated("fr.javatronic.damapping.processor.DAAnnotationProcessor")
public class SaltedBigDecimalToStringMapperFactoryImpl implements SaltedBigDecimalToStringMapperFactory {

    @Override
    public SaltedBigDecimalToStringMapper instanceByConstructor(String salt) {
        return new SaltedBigDecimalToStringMapperImpl(new SaltedBigDecimalToString(salt));
    }

    private static class SaltedBigDecimalToStringMapperImpl implements SaltedBigDecimalToStringMapper {
        private final SaltedBigDecimalToString instance;

        public SaltedBigDecimalToStringMapperImpl(SaltedBigDecimalToString instance) {
            this.instance = instance;
        }

        @Override
        @Nullable
        public String apply(@Nullable BigDecimal bigDecimal) {
            return instance.apply(bigDecimal);
        }
    }
}

Patterns

object-tree mapping

Using a generated Mapper interface in dedicated class is supported and actually highly encouraged as it is the basic pattern to implement mapping of trees of objects.

To use a Mapper interface in the object mapping code of a dedicated class, simply declare a (private and final) property and have it initialized in the constructor of the dedicated class.

@Mapper
public class AcmeToViten {
    private final FooToBarMapper fooToBarMapper;

    public AcmeToViten(FooToBarMapper fooToBarMapper) {
        this.fooToBarMapper = fooToBarMapper;
    }

    public Viten map(Acme acme) {
        // [...]
        vilen.setBar(fooToBarMapper.map(acme.getFoo()));
        // [...]
        return vilen;
    }
}

factory of singletons

Using an enum as a dedicated class and static method(s) annotated with @MapperFactory, one can implement a factory that will return multiple singleton implementations of the same Mapper.

@Mapper
public enum MultipleImplementationAsEnum {
  BIG_DECIMAL(true),
  INTEGER(false);

  /* parameter-less factory method returning the BIG_DECIMAL instance */
  @MapperFactory
  public static MultipleImplementationAsEnum bigDecimal() {
    return BIG_DECIMAL;
  }

  /* parameter-less factory method returning the INTEGER instance */
  @MapperFactory
  public static MultipleImplementationAsEnum integer() {
    return INTEGER;
  }

  /** factory method taking a flag parameter returning either the BIG_DECIMAL instance or the INTEGER instance */
  @MapperFactory
  public static MultipleImplementationAsEnum instance(boolean bigDecimal) {
    if (bigDecimal) {
      return BIG_DECIMAL;
    }
    return INTEGER;
  }

  private final boolean bigDecimal;

  @Nonnull
  public Integer apply(@Nullable String input) {
    if (bigDecimal) {
      return new BigDecimal(input).intValue();
    }
    return Integer.parseInt(input);
  }
}

IDE integration

IntelliJ IDEA

Since IntelliJ IDEA does not compile source files, it is not aware of types generated by the annotation processing integrated into the compiling process.

Therefor, unless the project is built and the generated source directory is added to the classpath, references to generated Mapper interfaces or generated MapperImpl classes will be displayed has errors.

To avoid building the project all the time and make the coding experience with the DAMapping framework more fluent, the developer should install the DAMapping plugin for IntelliJ (work in progress).

Eclipse

Since the DAMapping framework does not yet support the Eclipse compiler, build in Eclipse will fail.

Support for the Eclipse compiler is planned for version 0.7.0.