Mapper - 3.1
One of the core features of Slice is the Mapper. It's an object-resource mapper which can be perceived as JCR equivalent of object-relational mapping (ORM). Basically, it maps a JCR content to Java objects, so that you have access to a repository in your Java objects.
How to map a resource to an object?
If you want a resource to be mapped to an Java object, you have to meet three requirements:
- class must be annotated with
@SliceResource
, - object must be created using injector. All classes annotated with
@SliceResource
are bound toSliceResourceProvider
which is responsible for reading a current resource and invoking mapping to an object. Usually, you will be usingModelProvider
to specify from which resource to map, - class cannot be nested.
You don't need to invoke anything by yourself, if an object is injected and annotated with @SliceResource
it will be automatically mapped from current resource.
Mapping
The Mapper reads the resource provided by the framework (current resource) and writes its properties to corresponding fields in a Java object. The matching between properties and fields is done by names. It means that if a name of property is the same as a name of field, the value of the field will be set to the value of the property.
Mapping strategies
A class can define if all its fields should be mapped or only annotated. This can be done by specifying the MappingStrategy
as an argument of @SliceResource
:
@SliceResource(MappingStrategy.ANNOTATED)
or just@SliceResource
– only fields annotated by@JcrProperty
are mapped. This is a default.@SliceResource(MappingStrategy.ALL)
– all fields are mapped.
If a field is subject to mapping and there is no property in a resource which corresponds to a field, the field will be set to null
.
In some cases the default name matching between properties and fields is not enough, e.g. if you want to map jcr:title
property. In such cases you can specify a name of a property from which to read the value. You can do this specifying the String argument in @JcrProperty
which will instruct mapper to read a property of specified name.
Examples:
@SliceResource public class ExampleModel { @JcrProperty private String text; @JcrProperty("jcr:title") private String title; @JcrProperty("hideInNav") private boolean hiddenInNavigation; }
@SliceResource(MappingStrategy.ANNOTATED) //This is exactly the same as if we put @SliceResource public class AnotherModel { private String text; //this field won't be mapped @JcrProperty("jcr:title") private String title; @JcrProperty("hideInNav") private boolean hiddenInNavigation; }
@SliceResource(MappingStrategy.ALL) public class YetAnotherModel { private String text; //this field will be mapped private long number; //and this will be mapped, too @JcrProperty("jcr:title") private String title; }
Processors
In above examples you could see that some basic types of fields are mapped, including String, boolean, long... A piece of code which is responsible for mapping a given filed type(s) is called processor. The Mapper is build for extensions which means that you can register a number of processors, each one responsible for a given type. The architecture of mapper is based on visitor pattern. While mapping a resource to an object, the Mapper goes through all fields which should be mapped and for each of these fields it iterates through all register processors checking which one is willing to map the field. If a processor is willing to map the field, it do the actual mapping, meaning it stores a value in this field.
The diagram below depicts processing of an example class. Field text
is processed by the DefaultFieldProcessor
, similarily the number
field. hideInNav
field is processed by BooleanFieldProcessor
and other
by custom processor called FancyFieldProcessor
.
There are a number of predefined processors which can be used out of the box. But you can also implement yours and add them to your Mapper. The predefined processors include:
DefaultFieldProcessor
It is used when no other processor was willing to handle a field. It supports mapping of the basic JCR types including:
java.lang.String
- long,
java.lang.Long
- double,
java.lang.Double
java.util.Calendar
BooleanFieldProcessor
It is used to map to java.land.Boolean
or boolean
. The reason for having it separated is that it supports converting String properties to boolean values - this is needed because sometimes CQ writes boolean values as String in JCR repository.
ImageFieldProcessor
This is a CQ extension to Mapper. It supports String fields annotated with @com.cognifide.slice.mapper.annotation.ImagePath
or fields of com.day.cq.wcm.foundation.Image
type.
Depending on the field type you can have the following:
String
: a path to an image resource (set by SmartImage dialog) withsrc
selector added. If such a path is used in html img tag, it renders an image using CQ's image servletImage
: an Image object which allows you to manipulate the image and then output the src code using itsgetSrc()
method.
Slice CQ Addon
In 3.1.0 version of Slice the CQ related modules has been separated. If you would like to add them to your project's dependencies, please see Slice CQ Addons.
SliceResourceProcessor
This processor is used for recursive mapping of resources. If a field is of class annotated with @SliceResource
it will be mapped from a child resource named after the field.
It is especially useful for development of complex components, composed of another components - you can then easily reuse models of your simple components. Picture below presents an example:
SliceReferenceProcessor
It processes fields which are annotated with com.cognifide.slice.mapper.annotation.SliceReference
. It allows for mapping of models from a different resource (path) than a current resource
The @SliceReference
annotation takes a path as a value. The value can be either an absolute path (like /content/test/home/jcr:content/par
) or child path (like childResource/grandChildResource
) - then it will be resolved relatively to current resource. Bear in mind that parent resources (like ../../test
) are not supported.
The value of a path can contain placeholders in form of: ${placeholderName}
. This can be useful when the path is dynamic and changes depending on request, e.g. contains language. Instead of hardcoding the language in the path (like: /content/site/en/home
), you can put placeholder and have this path to be dynamically evaluated, e.g. /content/site/${language}/home
.
The placeholders are resolved using com.cognifide.slice.mapper.api.SliceReferencePathResolver
. It's an interface shipped with default implementation. Since it is intended to store a configuration of your placeholders, you must specify these placeholders. It can be done by some of your module providing the SliceReferencePathResolver object which have all placeholders added using its addReference
methods. A simple snippet of code which provides the object can look like this:
@Provides @ContextScope public SliceReferencePathResolver getSliceReferencePathResolver(Injector injector) { SliceReferencePathResolver resolver = SliceReferencePathResolverFactory.createResolver(injector); resolver.addPlaceholder("language", Language.class); return resolver; }
The addPlaceholder
method supports two kind of placeholders:
annotations (as in example - the Language.class is an annotation) - the specified annotation must be a BindingAnnotation and must be used for providing a String value by some module. Each placeholder stored using this method will be resolved by replacing it by a value returned by a provider of a String annotated with the specified annotation.
In order to resolve thelanguage
placeholder in above paths, some module must provideString
annotated byLanguage
:@Provides @Language public String getLanguage() { String language; //some logic to set the language variable return language; }
- String - specifying a String which will replace the placeholder directly.
Post processors
Post processors are dedicated mechanism for modifying values of mapped fields. It is useful when there is a need for modification of a value already mapped from a resource, e.g. to do some low-level conversion or encoding. Currently, there's only one post processor shipped with Slice but you can implement yours in order to do some arbitrary modifications. Post processors do NOT modify resources - they only modify values mapped from a resource to a field.
EscapeValuePostProcessor
When mapping a text property to a String field all HTML entities are escaped by default. The com.cognifide.slice.mapper.impl.postprocessor.EscapeValuePostProcessor
modifies a text value to unescape HTML so that it can be properly displayed on a page. It should be used when a property represents a part of HTML markup, e.g. text saved by richtext component.
To use this post processor, a field must be annotated by com.cognifide.slice.mapper.annotation.Unescaped
annotation, e.g.
@SliceResource public class ExampleModel { @JcrProperty @Unescaped private String text; }
InitializableModel
It's a common task to do some stuff after the model has been mapped and its fields hold values from underlying resource. You cannot do this in the constructor of your model because mapping is done after the object has been created, so all the fields which should be mapped are null
while executing constructor. Therefore, Slice introduces an interface called InitializableModel
which allows you to perform an arbitrary logic after the process of mapping has finished.
The InitializableModel
interface has only one, parameterless method: void afterCreated()
. Here's an example use:
@SliceResource public class ExampleModel implements InitializableModel { private final static Logger LOG = LoggerFactory.getLogger(ExampleModel.class); @JcrProperty private String text; @Override public void afterCreated() { if (text == null) { LOG.warn("There is no text property in the resource"); } } }