3. Add a Document Type Characteristic UI Plugin
Previous
Add a Document Types Collector and Characteristic
Characteristic UI Plugin
The Relevance Module UI needs a characteristic UI plugin for rendering and editing a characteristic in the CMS. A characteristic UI plugin provides:
- a description (mandatory), like "has seen (document type)"
- an icon (optional) to display next to the description
- a visitor characteristic (optional), to show in the Visitor Analysis screen which data a visitor has for this characteristic (e.g. "has seen Blogpost Document")
- a custom renderer and/or editor for the target groups within a characteristic
The plugin for the characteristic 'documenttype' will be developed in three versions. Each version adds more parts (description + icon, visitor characteristic, and custom renderer + editor), so the plugin will become more and more complex in each version.
Version 1: Characteristic UI Plugin With a Description and Icon
The simplest version of the 'documenttype' characteristic plugin extends the existing TermsFrequencyCharacteristicPlugin, and only adds its own description of the characteristic. The end result of the steps described below will be that the characteristic ' documenttype' is visible as a visitor characteristic in the Visitor Analysis screen, in the list of characteristics in the Characteristics tab, and in the segment editor in the Segments tab:
Visitor Analysis
|
Characteristics |
Segments |
First, using the Console, add the following JCR node to instantiate the new ' documenttype' characteristic plugin:
/hippo:configuration/hippo:frontend/cms/hippo-targeting: /characteristic-documenttype: jcr:primaryType: frontend:plugin collector: documenttypes characteristic: documenttype plugin.class: com.example.DocumentTypeCharacteristicPlugin
Second, add a Java class to the 'cms' module for the new characteristic plugin:
cms/src/main/java/com/example/DocumentTypeCharacteristicPlugin.java:
package com.example; import com.onehippo.cms7.targeting.frontend.plugin.TermsFrequencyCharacteristicPlugin; import org.apache.wicket.request.resource.ResourceReference; import org.apache.wicket.request.resource.PackageResourceReference; import org.hippoecm.frontend.plugin.IPluginContext; import org.hippoecm.frontend.plugin.config.IPluginConfig; public class DocumentTypeCharacteristicPlugin extends TermsFrequencyCharacteristicPlugin { public DocumentTypeCharacteristicPlugin(IPluginContext context, IPluginConfig config) { super(context, config); } @Override protected ResourceReference getIcon() { return new PackageResourceReference(DocumentTypeCharacteristicPlugin.class, "documents-icon.png"); } }
Third, add the icon image documents-icon.png to the same package as the plugin, but in the resources directory of the cms module. A reference to this icon is returned by the overridden method getIcon.
cms/src/main/resources/com/example/documents-icon.png:
Fourth, add the resource bundle of the plugin class, again in the same package in the resources directory. The .properties file must contain at least two keys: 'characteristic-description' and 'characteristic-subject'.
cms/src/main/resources/com/example/DocumentTypeCharacteristicPlugin.properties:
characteristic-description=has seen {0} characteristic-subject=(document types)
The '{0}' in the 'characteristic-description' value will either be replaced by the 'characteristic-subject' to create a generic description of the characteristic (e.g. "has seen (document types)"), or by the name of a target group (e.g. "has seen myproject:newsdocument").
To deploy the plugin code, stop your running project, rebuild it, and restart it.
Version 2: Characteristic UI Plugin With a Description, Icon, and Visitor Characteristic
Version 1 used the visitor characteristic of the inherited TermsFrequencyCharacteristicPlugin, which shows a comma-separated list of all property names of a target group. In case of the document type characteristic, it shows the raw JCR types of the viewed documents. Let's say that instead, the visitor segment should show all document types without the JCR namespace myproject:
Start with the .java and .properties file of Version 1. First change the Java class a bit:
DocumentTypeCharacteristicPlugin.java:
package com.example; import com.onehippo.cms7.targeting.frontend.plugin.CharacteristicPlugin; import org.apache.wicket.Component; import org.apache.wicket.markup.head.JavaScriptHeaderItem; import org.apache.wicket.markup.head.IHeaderResponse; import org.apache.wicket.request.resource.ResourceReference; import org.apache.wicket.request.resource.JavaScriptResourceReference; import org.apache.wicket.request.resource.PackageResourceReference; import org.hippoecm.frontend.plugin.IPluginContext; import org.hippoecm.frontend.plugin.config.IPluginConfig; import org.wicketstuff.js.ext.util.ExtClass; @ExtClass("Example.DocumentTypeCharacteristicPlugin") public class DocumentTypeCharacteristicPlugin extends CharacteristicPlugin { private static final JavaScriptResourceReference DOCTYPE_JS = new JavaScriptResourceReference(DocumentTypeCharacteristicPlugin.class, "DocumentTypeCharacteristicPlugin.js"); public DocumentTypeCharacteristicPlugin(IPluginContext context, IPluginConfig config) { super(context, config); } @Override protected ResourceReference getIcon() { return new PackageResourceReference(DocumentTypeCharacteristicPlugin.class, "documents-icon.png"); } @Override public void renderHead(final Component component, IHeaderResponse response) { super.renderHead(component, response); response.render(JavaScriptHeaderItem.forReference(DOCTYPE_JS)); } }
The plugin now extends the base class CharacteristicPlugin, and specifies the associated Javascript class in the ExtClass annotation. The Javascript source file is included as a header contribution, and will be looked up relative to the DocumentTypeCharacteristicPlugin.java file.
Second, add the Javascript file DocumentTypeCharacteristicPlugin.js. Like the .properties file in Version 1, we can place it in the resources directory of the cms module. However, it may make more sense to place it next to the .java class in the java directory (i.e. cms/src/main/java/com/example/DocumentTypeCharacteristicPlugin.js). The latter option requires the following addition to the <build> section of the CMS pom.xml, so .js files in src/main/java and all files below src/main/resources are included in the created war.
cms/pom.xml:
<build> <resources> <resource> <filtering>false</filtering> <directory>${basedir}/src/main/java</directory> <includes> <include>**/*.js</include> </includes> </resource> <resource> <filtering>false</filtering> <directory>${basedir}/src/main/resources</directory> <includes> <include>**/*</include> </includes> </resource> </resources> ... </build>
Dev Tips
-
JRebel can automatically pickup changes in Javascript files by configuring IntelliJ.
-
While debugging, check the Javascript log messages of the Relevance Module UI in the Chrome Developers Tools console or the Firebug console.
The Javascript source defines two classes: one for the characteristic plugin, and one for the 'visitor characteristic' of the characteristic plugin:
cms/src/main/java/com/example/DocumentTypeCharacteristicPlugin.js:
Ext.namespace('Example'); Example.DocumentTypeCharacteristicPlugin = Ext.extend(Hippo.Targeting.CharacteristicPlugin, { constructor: function(config) { Ext.apply(config, { visitorCharacteristic: { xtype: 'Example.DocumentTypeCharacteristic' } }); Example.DocumentTypeCharacteristicPlugin.superclass.constructor.call( this, config); } }); Example.DocumentTypeCharacteristic = Ext.extend(Hippo.Targeting.VisitorCharacteristic, { isCollected: function(targetingData) { console.log(targetingData.termFreq); return !this.isEmptyObject(targetingData.termFreq); }, isEmptyObject: function(object) { var isEmpty = true; if (!Ext.isEmpty(object)) { Ext.iterate(object, function() { isEmpty = false; return false; }) } return isEmpty; }, getTargetGroupName: function(targetingData) { var names = []; Ext.iterate(targetingData.termFreq, function(term) { var indexAfterNamespace = term.indexOf(':') + 1; names.push(term.substring(indexAfterNamespace)); }); return names.join(', '); }, getTargetGroupProperties: function(targetingData) { var properties = []; Ext.iterate(targetingData.termFreq, function(term) { properties.push({ name: term, value: '' }) }); return properties; } }); Ext.reg('Example.DocumentTypeCharacteristic', Example.DocumentTypeCharacteristic);
It is good practice to namespace all custom Javascript classes. In this example we use the namespace 'Example' (line 1).
In line 6-10, the xtype of the visitor characteristic class (' Example.DocumentTypeCharacteristic') is provided in the ' visitorCharacteristic' configuration property of the plugin. ExtJS uses this xtype to instantiate the associated class at runtime. In this example the xtype is the same as the class name; this is not mandatory (the xtype can be any string), but good practice and used throughout the Relevance Module's Javascript code. The xtype is registered in line 56.
The class Example.DocumentTypeCharacteristic inspects a targetingData object, which is the JSON representation of the serialized TargetingData object of the collector of the characteristic. Since the 'documenttypes' characteristic is backed by a com.onehippo.cms7.targeting.collectors.DocumentTypesCollector, the targetingData object is a com.onehippo.cms7.targeting.data.TermsFrequencyTargetingData. That object has a method getTermFreq that returns a Map<String, Integer> of all terms collected during a visit, and their frequencies. In case of the document types collector, each term is a JCR type name (i.e. myproject:newsdocument). The TermsFrequencyTargetingData object of the selected visitor is serialized to JSON and given to the functions of the class Example.DocumentTypeCharacteristic. So, the structure of the 'targetingData' Javascript object depends on the JSON representation of the TargetingData Java object of the collector.
In the example, the function isCollected returns true when document types visitor data has been collected for the selected visitor. It calls a helper method isEmptyObject that returns true when the given JavaScript does not contain any properties. The function getTargetGroupName returns the document types without the namespace prefix (i.e. after the ':'). The function getTargetGroupProperties returns an array of property objects, each with their name set to a the document type. The target group name and properties are used to save the displayed visitor characteristic as a new target group in the 'has seen (document type)' characteristic.
Version 3: Characteristic UI Plugin with a Description, Icon, Visitor Characteristic, and Target Group Renderer and Editor
Version 1 and 2 of the characteristic plugin used the default target group renderer and editor. In the 'Characteristics' tab, the target groups of the 'has seen (document types)' characteristic will therefore be shown as a comma-separated list of JCR types. Instead of having to type in all these types by hand, it would be nice to select them from a predefined list. Version 3 of the characteristic plugin will therefore have a custom renderer and editor for 'has seen (document type)' target groups.
The final version 3 of the 'has seen (document types)' characteristic plugin is part of the Relevance Module as com.onehippo.cms7.targeting.frontend.plugin.documenttype.DocumentTypeCharacteristicPlugin. The screenshots below show what the renderer and editor of this plugin type characteristic plugin looks like.
The renderer shows the list of i18n names of each document type instead of the JCR property name (e.g 'News' instead of myproject:newsdocument):
The editor shows a checkbox group with a checkbox for each document type:
To create this version 3, we first modify the Java class of version 2.
cms/src/main/java/com/example/DocumentTypeCharacteristicPlugin.java:
package com.example; import java.util.ArrayList; import java.util.List; import org.apache.wicket.Component; import org.apache.wicket.markup.head.IHeaderResponse; import org.apache.wicket.markup.head.JavaScriptHeaderItem; import org.apache.wicket.request.resource.JavaScriptResourceReference; import org.apache.wicket.request.resource.PackageResourceReference; import org.apache.wicket.request.resource.ResourceReference; import org.hippoecm.frontend.plugin.IPluginContext; import org.hippoecm.frontend.plugin.config.IPluginConfig; import org.json.JSONException; import org.json.JSONObject; import org.wicketstuff.js.ext.util.ExtClass; import com.onehippo.cms7.targeting.frontend.plugin.CharacteristicPlugin; @ExtClass("Example.DocumentTypeCharacteristicPlugin") public class DocumentTypeCharacteristicPlugin extends CharacteristicPlugin { private static final JavaScriptResourceReference DOCTYPE_JS = new JavaScriptResourceReference(DocumentTypeCharacteristicPlugin.class, "DocumentTypeCharacteristicPlugin.js"); public DocumentTypeCharacteristicPlugin(IPluginContext context, IPluginConfig config) { super(context, config); } @Override protected ResourceReference getIcon() { return new PackageResourceReference(DocumentTypeCharacteristicPlugin.class, "documents-icon.png"); } @Override public void renderHead(final Component component, final IHeaderResponse response) { super.renderHead(component, response); response.render(JavaScriptHeaderItem.forReference(DOCTYPE_JS)); } @Override protected void onRenderProperties(final JSONObject properties) throws JSONException { super.onRenderProperties(properties); List<JSONObject> documentTypes = new ArrayList<JSONObject>(); documentTypes.add(createDocumentType("News", "myproject:newsdocument")); documentTypes.add(createDocumentType("Simple Content", "myproject:contentdocument")); properties.put("documentTypes", documentTypes); } JSONObject createDocumentType(String name, String jcrType) throws JSONException { JSONObject documentType = new JSONObject(); documentType.put("type", jcrType); documentType.put("name", name); return documentType; } }
Note that the list of available document types and their i18n names is hardcoded. The DocumentTypeCharacteristicPlugin in the Relevance Module has configuration options to specify which document types to include, and looks up the i18n names automatically.
Second, add an additional key to the .properties file for the error message used by the editor.
cms/src/main/resources/com/example/DocumentTypeCharacteristicPlugin.properties:
characteristic-description=has seen {0} characteristic-subject=(document types) error-select-at-least-one-document-type=Select at least one document type
Third, extend the JavaScript class.
cms/src/main/java/com/example/DocumentTypeCharacteristicPlugin.js:
Ext.namespace('Example'); Example.DocumentTypeCharacteristicPlugin = Ext.extend(Hippo.Targeting.CharacteristicPlugin, { constructor: function(config) { this.documentTypeMap = new Hippo.Targeting.Map(config.documentTypes, 'type', 'name'); Ext.apply(config, { visitorCharacteristic: { documentTypeMap: this.documentTypeMap, xtype: 'Example.DocumentTypeCharacteristic' }, editor: { documentTypes: config.documentTypes, resources: config.resources, xtype: 'Example.DocumentTypeTargetGroupEditor' }, renderer: this.renderDocumentTypeNames, scope: this }); Example.DocumentTypeCharacteristicPlugin.superclass.constructor.call( this, config); }, renderDocumentTypeNames: function(properties) { var result = []; Ext.each(properties, function(property) { var type = property.name, name = this.documentTypeMap.getValue(type); if (!Ext.isEmpty(name)) { result.push(name); } }, this); return result.join(', '); } }); Example.DocumentTypeCharacteristic = Ext.extend(Hippo.Targeting.TermsFrequencyCharacteristic, { constructor: function(config) { Example.DocumentTypeCharacteristic.superclass.constructor.call( this, config); this.documentTypeMap = config.documentTypeMap; }, getTermName: function(term) { return this.documentTypeMap.getValue(term); } }); Ext.reg('Example.DocumentTypeCharacteristic', Example.DocumentTypeCharacteristic); Example.DocumentTypeTargetGroupEditor = Ext.extend(Hippo.Targeting.TargetGroupCheckboxGroup, { constructor: function(config) { var checkboxes = []; Ext.each(config.documentTypes, function(documentType) { checkboxes.push({ boxLabel: documentType.name, name: documentType.type }); }); Example.DocumentTypeTargetGroupEditor.superclass.constructor.call( this, Ext.apply(config, { allowBlank: config.allowBlank || false, blankText: config.resources['error-select-at-least-one-document-type'], columns: config.columns || 2, items: checkboxes, vertical: true })); } }); Ext.reg('Example.DocumentTypeTargetGroupEditor', Example.DocumentTypeTargetGroupEditor);
The .java class creates an array of objects, each with a 'type' and a 'name' property. The .js class wraps this array in a utility object Hippo.Targeting.Map that makes it easy to lookup the name by type and vice versa.
The constructor of the DocumentTypeCharacteristicPlugindefines an editor and a renderer. The renderer is just a function that gets a 'properties' object and converts it to a string shown in the UI. The document type renderer only uses the 'name' of each property object, which contains a JCR type of a document. An example property object would be:
{ name: 'myproject:newsdocument', value: '' }
The renderer function looks up the i18n name of each JCR type in the documentTypeMap created in the constructor, and returns a list of comma-separated i18n'd document type names.
The document type target group editor is a Hippo.Targeting.TargetGroupCheckboxGroup with a checkbox for each document type. The documentTypes array created in the Java class is passed to the constructor of the editor. The editor iterates over this array to create an array of checkboxes to render. The label shown next to each checkbox is the i18n name of a document type, the underlying value that will be written to the properties of the target group is the JCR type.
Note that we also simplified the Example.DocumentTypeCharacteristic by extending Hippo.Targeting.TermsFrequencyCharacteristic. We now only have to override the function getTermName to provide the name of each term that's displayed in the list of visitor characteristics.
Next
Add a Document Type Widget to the Visitor Analysis Screen