Mapper - 4.3

Overview

Slice allows you to map a resource into plain Java object. It's annotation-driven, very easy to use and fully extensible, so you can write your own ways of mapping if a set of available features is not enough for your needs. Slice supports mapping of:

  • simple properties (String, Long, Boolean, etc) into primitives and objects
  • simple properties into enums.
  • multi-value properties into arrays or lists
  • child resources into a Java object or list of objects
  • nested resources/classes hierarchy

The following code snippet demonstrates all above features in one model. It's simple - just annotate a class with @SliceResource and its fields with @JcrProperty to get auto mapping of resource properties to class fields:

@SliceResource
public class ComplexTextModel {

    @JcrProperty
    private String text;

    @JcrProperty
    private String[] styles;

    @JcrProperty
    private AlignmentEnum alignment;

    @Children(LinkModel.class)
    @JcrProperty
    private List<LinkModel> links;

    @JcrProperty
    private ImageModel image;

    //... do whatever business logic you want in your model
}

public enum AlignmentEnum {
    LEFT, RIGHT, CENTER;
}

@SliceResource(MappingStrategy.ALL)
public class LinkModel {
    private String path;
    private String label;
    //...
}

@SliceResource(MappingStrategy.ALL)
public class ImageModel {
    private String imagePath;
    private String altText;
    //...
}

Deep dive into Mapper

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. After Guice instantiated an object of a class which is annotated with @SliceResource, it invokes mapping from current resource. Usually, you will be using ModelProvider 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.

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 a property is the same as a name of a 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") // you can still use the @JcrProperty to specify the name of property to read from
    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 performs 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) with src selector added. If such a path is used in html img tag, it renders an image using CQ's image servlet
  • Image: an Image object which allows you to manipulate the image and then output the src code using its getSrc() method.

EnumFieldProcessor

It allows you to map a value of a String property into enum value. The property value must be equal to one of values from specified enum. If no value is matched, then the enum field will be set to null.

@SliceResource
public class ComplexTextModel {
    @JcrProperty
    private AlignmentEnum alignment; //the alignment property of the resource must equal either LEFT, RIGHT or CENTER. Otherwise, null will be set

    //... 
}

public enum AlignmentEnum {
    LEFT, RIGHT, CENTER;
}

 

ListFieldProcessor

The processor is responsible for setting mutli-value properties into java.util.List. If a resource property is not a multi-value property, then the mapped list will contain only one element. If there is no property in the resource, then the List field will be set to null.

@SliceResource
public class ComplexTextModel {

    @JcrProperty
    private List<String> styles;
    
    //...
}

 

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:

ChildrenFieldProcessor

ChildrenFieldProcessor maps fields of array type or one of the following types java.util.List, java.util.Set, java.util.SortedSetjava.util.Collection which are annotated by com.cognifide.slice.mapper.annotation.Children. It reads all children of a resource indicated by @JcrProperty and maps them into a list of models. This allows you to easily go down into tree hierarchy and map child resources into collection of mapped models.

@SliceResource
public class ComplexTextModel {

    @Children(LinkModel.class)
    @JcrProperty
    private List<LinkModel> links;
    
    //...
}

@SliceResource
public class LinkModel {
    @JcrProperty
    private String path;

    //...
}

 

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 the language placeholder in above paths, some module must provide String annotated by Language:

    @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;
}

Initializable models

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");
	    }
	}
}

@PreMapping and @PostMapping annotations

Aside from InitializableModel there are method annotations introduced in Slice 4.3.

Having a model annotated with @SliceResource you can annotate method with @PreMapping or @PostMapping annotation. Such method will be called before or after mapping is done respectively.

@SliceResource
public class MyComponentModel {

  @JcrProperty
  private String property;

  @Inject
  private ModelProvider modelProvider;

  @PreMapping
  public void preMapping() {
    // modelProvider is already set; property is not set
  }

  @PostMapping
  public void postMapping() {
    // modelProvider and property are already set
  }
}

 

How to extend mapper?

Writing your own processor

Writing processor is as easy as implementing com.cognifide.slice.mapper.api.processor.FieldProcessor interface.

Below is an example of FieldProcessor that sets custom text to model field if there is no resource or the property we are mapping from is not set.

DefaultStringValueFieldProcessor.java
 public class DefaultStringValueFieldProcessor implements FieldProcessor {

   private static final String DEFAULT_STRING_VALUE = "NOT_EXISTING";

   @Override
   public boolean accepts(Resource resource, Field field) {
      return String.class.isAssignableFrom(field.getType());
   }

   @Override
   public Object mapResourceToField(Resource resource, ValueMap valueMap, Field field, String propertyName) {
      String result = DEFAULT_STRING_VALUE;
      if (valueMap != null) {
         String propertyValue = valueMap.get(propertyName, String.class);
         if (propertyValue != null) {
            result = propertyValue;
         }
      }
      return result;
   }
}

There are two methods to be implemented.

  • accepts - this method decides if the field can be mapped by this processor
  • mapResourceToField - this method returns a value which is set to specified field.

Registering your processor

 To register custom FieldProcessor insert following code in your implementation of com.google.inject.Module.

CustomProcessorModule.java
public class CustomProcessorModule implements Module {

   @Override
   protected void configure() {
      Multibinder<FieldProcessor> multibinder = Multibinder.newSetBinder(binder(), FieldProcessor.class);
      multibinder.addBinding().to(DefaultStringValueFieldProcessor.class);
   }
 
}

All registered processors are added at the beginning of the processors list. There is also a way to ensure the order of custom processors. In order to do so, use  com.cognifide.slice.mapper.api.processor.PriorityFieldProcessor interface when writing custom processor and implement getPriority method. Processors with higher priority are placed at the beginning of processors list.

All custom processors registered with Mulitbinder always take precedence over the standard ones that come with Slice.

Writing your own post-processor

Writing post-processor is essentially the same as writing processor. Instead of implementing com.cognifide.slice.mapper.api.processor.FieldProcessor you should implement com.cognifide.slice.mapper.api.processor.FieldPostProcessor.

Below you will find example of FieldPostPorcessor that reverses the text assigned to model's field.

ReverseTextFieldPostProcessor.java
public class ReverseTextFieldPostProcessor implements FieldPostProcessor {

   @Override
   public boolean accepts(Resource resource, Field field, Object value) {
      return value instanceof String;
   }

   @Override
   public Object processValue(Resource resource, Field field, Object value) {
      return StringUtils.reverse((String) value);
   }
}

Registering your post-processor

To register custom FieldPostProcessor insert following code in your implementation of com.google.inject.Module.

CustomPostProcessorModule.java
public class CustomPostProcessorModule implements Module {

   @Override
   protected void configure() {
      Multibinder<FieldPostProcessor> multibinder = Multibinder.newSetBinder(binder(), FieldPostProcessor.class);
      multibinder.addBinding().to(ReverseTextFieldProcessor.class);
   }
 
}

All registered post-processors will be added at the beginning of post-processors list. There is also a way to ensure the order of custom post-processors.  In order to do so, use  com.cognifide.slice.mapper.api.processor.PriorityFieldPostProcessor interface when writing custom post-processor and implement getPriority method.

 

Unlike PriorityFieldProcessor , the order of post-processors with regard to the ones that come with Slice differs and depends of priority value.

Custom post-processors with priority value greater or equal to 0 are placed on the list before built-in post-processors.

Custom post-processors with priority value lower than 0 are placed on the list after built-in post-processors.