In some cases, you may wish to control whether nulls are mapped or ignored when they are encountered. By default, Orika will map null values when encountered. This behavior can be customized at different levels depending on how specific you’d like to be.
The global default behavior can be controlled using the mapNulls(false) method on DefaultMapperFactory.Builder; use this to ensure that a null value in a source object’s property will result in leaving the destination property unchanged, rather than causing it to be set to null (the default).
The global setting will take effect for in any case where a setting has not been applied at the ClassMap or FieldMap level to override.
Mapping of null values can be controlled on a ClassMapBuilder by using the mapNulls(true|false) or mapNullsInReverse(true|false) (for controlling mapping of nulls in the reverse direction). By setting this value on a ClassMapBuilder, all field mappings created on the same ClassMapBuilder (after the value is set) will take on that same value. For example:
mapperFactory.classMap(BasicPerson.class, BasicPersonDto.class) .mapNulls(true).mapNullsInReverse(true) .field("field1", "fieldOne") .mapNulls(false).mapNullsInReverse(false) .field("field2", "fieldTwo") .byDefault() .register();
Let’s break down this example; first, we set mapNulls(true) and mapNullsInReverse(true), which specifies that the field ‘fieldOne’ will be set to null if ‘field1’ is null (mapNulls(true)), and also that the field ‘field1’ will be set to null if ‘fieldOne’ is null (mapNullsInReverse(true)) — this is because this field mapping statement followed the original setting.
Next, we set mapNulls(false) and mapNullsInReverse(false), which means that all of the field mapping statements following it will not map null values.
Note that we can also set this value individually on specific field maps, as in the following example:
mapperFactory.classMap(BasicPerson.class, BasicPersonDto.class) .mapNulls(false).mapNullsInReverse(false) .fieldMap("field1", "fieldOne").mapNulls(true).mapNullsInReverse(true).add() .field("field2", "fieldTwo") .byDefault() .register();
In this example, we’ve set the default for this ClassMap to not map nulls (in either direction), but for the mapping of ‘field1’ to ‘fieldOne’ (in both directions), we specified that null values should be mapped.
For cases where an individual mapping requires a little extra customization, the customize() method can be used on a ClassMap to add a CustomMapper definition which will be called after the registered field mappings have been applied.
Let’s demonstrate with an example:
mapperFactory.classMap(Source.class, Destination.class) .byDefault() .customize( new CustomMapper<Source, Destination> { public void mapAtoB(A a, B b, MappingContext context) { // add your custom mapping code here } } .register();
In other cases, you may want to control the behavior of the ClassMapBuilder’s byDefault() method in aligning the unmatched properties. The out-of-the-box behavior is to only match properties whose names are an exact match (case-sensitive), but this behavior can be overridden by extending the basic ClassMapBuilder.
A (functional) sample customization of ClassMapBuilder has been provided which uses a scoring method to automagically ‘guess’ the proper alignment of fields, based on the edit distance of the property names (and some other things); this class is ma.glasnost.orika.metadata.ScoringClassMapBuilder, and it can be registered on the DefaultMapperFactory.Builder like so:
MapperFactory factory = new DefaultMapperFactory.Builder() .classMapBuilderFactory(new ScoringClassMapBuilder.Factory()) .build();
Note that we’ve registered the Factory for this class; review the source for this class to see how you could provide your own alternative
Most of the mapping details covered so far work fairly well to properties that conform to the JavaBeans standard; but what if your definition of “property” doesn’t follow those rules?
The built-in property resolver (which is used to parse the string values passed and line them up with the appropriate Property) has a special syntax available for defining properties on the fly, where you can override the getter, setter, name, and type to be used. As usual, the best way to explain is with an example…
Suppose we want to map from an object with a more generic structure, such as the Element class shown below, which has getters and setters more like Map keys.
public static class Element { Map<String,Object> attributes = new HashMap<String,Object>(); public Object getAttribute(String name) { return attributes.get(name); } public void setAttribute(String name, Object value) { attributes.put(name, value); } }
public static class Person { public String firstName; public String lastName; public String jobTitle; public String salary; }
We can define the properties of the Element type using in-line property definition, like so:
"employment:{getAttribute('employment')|setAttribute('employment', %s)|type=ma.glasnost.orika.test.property.PropertyResolverTestCase.Element}";
To break down the parts, we begin with the property name employment
, follwed by :{
to begin an in-line property definition.
We begin the in-line definition with the getter first getAttribute('employment')
, using the pipe |
to separate it from the setter which follows: setAttribute('employment', %s)
. Note the use of the %s
place-holder, which represents the value to be set.
We follow this with another pipe |
to separate the setter from the type definition). Note that the type definition begins with type=.
Finally, we end the in-line definition with a closing brace }
.
So, to bring it together, we can define properties for these elements with the code snippet below. Note that we’re defining some properties as nested (separated by the usual ‘.’ as separator ).
Also note that we can refer to an inline property already defined simply by using it’s name; no need to repeat the definition for each reference.
String employmentDef = "employment:{getAttribute('employment')|setAttribute('employment', %s)|type=ma.glasnost.orika.test.property.PropertyResolverTestCase.Element}"; String jobTitleDef = "jobTitle:{getAttribute(\"job's Title\")|setAttribute(\"job's Title\", %s)|type=List<String>}"; String salaryDef = "salary:{getAttribute(\"'salary'\")|setAttribute(\"'salary'\", %s)|type=java.lang.Long}"; String nameDef = "name:{getAttribute('name')|setAttribute('name',%s)|type=ma.glasnost.orika.test.property.PropertyResolverTestCase.Element}"; String firstNameDef = "first:{getAttribute('first')|setAttribute('first', %s)|type=java.lang.String}"; String lastNameDef = "last:{getAttribute('last')|setAttribute('last', %s)|type=java.lang.String}"; factory.classMap(Element.class, Person.class) .field(employmentDef + "." + jobTitleDef, "jobTitles") .field("employment." + salaryDef, "salary") // reuse the in-line declaration of 'employment' property .field(nameDef + "." + firstNameDef, "firstName") .field("name." + lastNameDef, "lastName") // reuses the in-line declaration of 'name' property .register();
Properties can also be defined in a more programmatic way, using the Property.Builder API, as seen in the following example:
Property.Builder.propertyFor(Element.class, "employment") .type(Element.class) .getter("getAttribute(\"employment\")") .setter("setAttribute(\"employment\", %s)");
In case you have many properties of the same type (say, our Element class), a custom PropertyResolverStrategy can be defined which automatically injects an inline definition matching our custom type in the case that the normal property lookup has failed. The definition is shown below:
public static class ElementPropertyResolver extends IntrospectorPropertyResolver { protected Property getProperty(java.lang.reflect.Type type, String expr, boolean isNestedLookup, Property owner) throws MappingException { Property property = null; try { property = super.getProperty(type, expr, isNestedLookup, null); } catch (MappingException e) { try { property = super.resolveInlineProperty(type, expr + ":{getAttribute(\""+ expr+"\")|setAttribute(\""+ expr+"\",%s)|type=" + (isNestedLookup ? Element.class.getName() : "Object") + "}"); } catch (MappingException e2) { throw e; // throw the original exception } } return property; } } /* * Register this on the DefaultMapperFactory.Builder */ MapperFactory factory = new DefaultMapperFactory.Builder() .propertyResolverStrategy(new ElementPropertyResolver()) .build();