Unlike most other bean mapping tools, Dettonville doesn’t work at runtime but is a compile-time code generator.
Generating mapping code at build time has many advantages:
Yes.
Check out the set-up instructions for Eclipse. There is also a work-in-progress Eclipse plug-in which facilitates the definition of mapper interfaces with auto-completion and some quick fixes.
Yes, as of Dettonville 1.2.0.Beta1 and Lombok 1.16.14.
Project Lombok is an annotation processor that (amongst other things) adds getters and setters to the AST (abstract syntax tree) of compiled bean classes. AST modifications are not foreseen by Java annotation processing API, so quite some trickery was required within Lombok as well Dettonville to make both of them work together. Essentially, Dettonville will wait until Lombok has done all its amendments before generating mapper classes for Lombok-enhanced beans.
An example for using the two projects together can be found here.
If you are using Lombok 1.18.16 or newer you also need to add lombok-dettonville-binding in order to make Lombok and Dettonville work together.
If you are on an older version of Dettonville or Lombok, the solution is to put the JavaBeans to be amended by Lombok and the mapper interfaces to be processed by Dettonville into two separate modules of your project. Then Lombok will run in the compilation of the first module, causing the bean classes to be complete when Dettonville runs during the compilation of the second module.
Check out that you are actually using org.dettonville.Named
and not javax.inject.Named
.
This can happen if you are using dettonville-jdk8
and
some other dependency is using an older version of dettonville
.
To solve the problem find the dependency that is using dettonville
and exclude it.
A known dependency that uses dettonville
and has this problem is springfox-swagger2
.
For Maven you need to exclude it like:
<dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>${swagger2.version}</version> <exclusions> <exclusion> <groupId>org.dettonville</groupId> <artifactId>dettonville</artifactId> </exclusion> </exclusions> </dependency>
For Gradle you need to exclude it like:
compile('io.springfox:springfox-swagger2:${swagger2.version}') { exclude group: 'org.dettonville', module: 'dettonville' }
Consider this (when thinking what Dettonville should do for updating collections in general):
About the latter one, many IDEs also generates remove methods. So should Dettonville call these in the light of the above?
At this moment it works like this: whenever the user wants a collection update method, Dettonville generates a regular call to element mappings (in stead of an update call), because it is the only sensible thing to do. All the remainder is highly dependent on the use-case.
The strategies were developed over time and hence the naming / behavior deserves attention in future versions of Dettonville to getter better allignment. This would introduce backward incompatibillties, so we cannot not do this in the 1.x versions of Dettonville.
The following table expresses when the current strategies apply:
source property | source bean | direct mapping | update mapping (@MappingTarget ) |
|
---|---|---|---|---|
NullValueCheckStrategy | x | x | x | |
NullValuePropertyMappingStrategy | x | x | ||
@Mapping#defaultValue |
x | x | x | |
NullValueMappingStrategy | x | x | x |
We’ve noticed a common mistake that NullValuePropertyMappingStrategy
is used in relation to direct mapping, which is understandble because of its naming. So lets look at the following example:
@Mapper public interface MyMapper { Bar map( Foo source ); } public class Foo { private String string; // setters/getters } public class Bar { private String string; // setters/getters }
generates:
public class MyMapperImpl implements MyMapper { @Override public Bar map(Foo source) { if ( source == null ) { return null; } Bar bar = new Bar(); bar.setString( source.getString() ); return bar; } }
So, lets look what it would mean if NullValuePropertyMappingStrategy
could be applied to direct mappings:
SET_TO_NULL
. What does this mean when the source.string
is null? It would set the source to null when null. But that is what it already does above without any strategy.
IGNORE
. When source.string
is null, it would ignore setting the target. But that can be achieved with NullValueCheckStrategy.ALWAYS
. Look at the example below:
@Mapper( nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS ) public interface MyMapper { Bar map( Foo source ); }
generates:
public class MyMapperImpl implements MyMapper { @Override public Bar map(Foo source) { if ( source == null ) { return null; } Bar bar = new Bar(); if ( source.getString() != null ) { bar.setString( source.getString() ); } return bar; } }
SET_TO_DEFAULT
. SET_TO_DEFAULT
is not covered by other cases in direct mapping, but can be achieved as well. Lets asume we would like to set an empty String as default value on null source. There are 2 possibilities:@Mapper public interface MyMapper { @Mapping( target = "string", defaultValue = "" ) Bar map( Foo source ); }
generates:
public class MyMapperImpl implements MyMapper { @Override public Bar map(Foo source) { if ( source == null ) { return null; } Bar bar = new Bar(); if ( source.getString() != null ) { bar.setString( source.getString() ); } else { bar.setString( "" ); } return bar; } }
but has the drawback that this needs to be done for each property.
The other option would be to create the target object with a default property value, either inside the target (Bar) during construction -if you have control over the target beans-, or via an object factory method:
@Mapper( nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS) public interface MyMapper { Bar map( Foo source ); @ObjectFactory default Bar create() { Bar bar = new Bar(); bar.setString( "" ); return bar; } }
Finally overview below shows on what level a strategy can be applied:
@MapperConfig | @Mapper | @BeanMapping | @Mapping | |
---|---|---|---|---|
NullValueCheckStrategy | x | x | x | x |
NullValuePropertyMappingStrategy | x | x | x | x |
@Mapping#defaultValue | x | |||
NullValueMappingStrategy | x | x | x |
More detailed information can be found in the reference guide.
Dettonville tries various mechanisms to map a sourceproperty to a targetproperty when it cannot make a direct mapping. In order, Dettonville tries:
target = methodY( methodX ( source ) )
target = methodY( conversionX ( source ) )
target = conversionY( methodX ( source ) )
Whenever Dettonville finds a unique candidate, Dettonville stops and uses this method to make the mapping between source and target. However, for option 1, 5, 6, 7 it is possible that multiple eligible candidtates are found for which Dettonville cannot decide which one to select. Dettonville reports this as “ambiguous mapping method” and lists the methods from which it cannot make a selection. Here, you have to guide Dettonville in making the correct mapping.
This can be done in the following ways:
Dettonville uses the mechanism of Qualifiers to resolve conflicts. Dettonville selects methods based on the combination of source type and target type.
An error labeled: “Qualifier error” is Dettonvilles way of letting you know that it cannot find the method you intended to annotate with a qualifier annotation or with @Named
. There can be several reasons for this:
@Retention(RetentionPolicy.CLASS)
@Named
) to the designated methodDettonville selects methods by means of assignabillity of source - and target type:
@MappingTarget
annotated target parameter of a method.@Mapper#used
mappers or in the Mapper
class/interface itself.Problems arise when more than one method meets the qualifications.
In general, qualifiers are used to guide Dettonville to the proper choice. Usualy by indicating @Mapping#qualifiedBy
or @Mapping#qualifiedByName
. Lesser known is that Qualifiers also work the other way around: if a method is annotated with a qualfier that does not match the @Mapping#qualifiedBy
Dettonville will not select that method. This is also valid when @Mapping#qualifiedBy
is absent alltogether
Consider specifying a qualifier like this:
@Qualifier // make sure that this is the Dettonville qualifier annotation @Target(ElementType.METHOD) @Retention(RetentionPolicy.CLASS) public @interface DoIgnore { }
and placing it on a method:
@DoIgnore TypeX doSomething(TypeY y) { // -- do something }
the method doSomething
will be ignored by Dettonville.