Adding a workflow to Hippo CMS editor toolbar
Minos Chatzidakis
2015-04-20
Quite often I see this question popping up in the Hippo world, that is, how one goes about extending the Hippo workflows. Questions vary; workflow is one of the most loaded terms in the CMS world. In this blog entry I’d like to focus on the document workflow, specifically I’d like to show how easy it is to design a process in the Hippo CMS that is user triggered, painlessly integrated in the UI and primarily, does something to a document. Solutions involving custom workflows usually go way far, playing important roles in integrations, in applying document policies, in marking and rating documents and in many other useful scenarios. Let’s make something simple here, the use case will be creating a mechanism to add a comment on a document, let's call it the Comment editing plugin.
The Scenario
I won’t try to explain the bare minimums needed to create the plugin. I will elaborate; we’ll make a visible button in the editor toolbar that opens up a dialog, allows the editor to enter a comment and saves this to the document when she presses ok. I intend to present the wicket code for this, but keep in mind this is just example code. I hope this entry can serve as a valuable supplement to our existing documentation on workflows. I’ll show how to plug all things together.
What is this entry not about? It is not about the new SCXML-based document workflow in Hippo. This means we won’t be changing nor plugging in to the behaviour of the existing document workflow, that is the ‘Edit’, ‘Save’, the translating process and all operations found under the ‘Document’ and ‘Publication’ menus on the editor toolbar. Plugging in to the document workflow has also recently become extremely easy, but it does deserve an entry on its own.
Building the Solution
Let’s dive into this by first creating the core of our functionality, a repository workflow. The repository workflow is the class that will actually perform changes to the document (save the comment), after the user has pressed ‘ok’ in our dialog. Creating a repository workflow can be handy as it means it can be called from anywhere, so not necessarily from frontend plugins in the CMS. A scenario, for instance, could be a service logging-in to a running repository and invoking such a workflow on a specific document.
We need an interface and an implementation class, and to keep it simple we packaged both in the cms module of an essentials generated 7.9.7 project. Here’s the interface for our workflow, at project relative path: myhippoproject/cms/src/main/java/org/example/workflow/CommentEditingWorkflow.java
package org.example.workflow; public interface CommentEditingWorkflow extends org.hippoecm.repository.api.Workflow { public void comment(String comment) throws javax.jcr.RepositoryException; }
And this is the implementation class, at path myhippoproject/cms/src/main/java/org/example/workflow/CommentEditingWorkflowImpl.java
package org.example.workflow; import java.rmi.RemoteException; import javax.jcr.Node; import javax.jcr.RepositoryException; import org.hippoecm.repository.ext.WorkflowImpl; public class CommentEditingWorkflowImpl extends WorkflowImpl implements CommentEditingWorkflow { private static final long serialVersionUID = 1L; public CommentEditingWorkflowImpl() throws RemoteException {} @Override public void comment(final String comment) throws RepositoryException { Node documentHandleNode = getCheckedOutNode(); if(! documentHandleNode.isNodeType("hippostd:relaxed")) { documentHandleNode.addMixin("hippostd:relaxed"); } documentHandleNode.setProperty("comment", comment); documentHandleNode.getSession().save(); } }
We chose to store our comment as a non-namespaced property on the hippo:handle node of the document. But in order to be able to do this we either need to define this property to the CND definition of hippo:handle, or make the node hippostd:relaxed. We chose the second approach, but please keep in mind that this may not be desired in general (adding the relaxed mixin) and that in any case, the fact that we’re selecting the handle for saving our comment is arbitrary and only done here to keep the implementation simple.
If one would like to create unit tests, then definitely this would be the class to write the tests against.
CMS Frontend Plugin
We proceed with the frontend code, starting with the CMS plugin that shows a button on the toolbar. Clicking this button should open a dialog and when the 'ok' button is pressed, the plugin invokes our repository workflow. The coupling of the repository class with the frontend plugin is done via configuration, see next paragraph. This is how the frontend plugin looks, created at path: myhippoproject/cms/src/main/java/org/example/frontend/CommentEditingFrontendPlugin.java.
package org.example.frontend; import org.apache.wicket.model.StringResourceModel; import org.apache.wicket.request.resource.PackageResourceReference; import org.apache.wicket.request.resource.ResourceReference; import org.example.frontend.dialog.CommentEditingDialog; import org.example.workflow.CommentEditingWorkflow; import org.hippoecm.addon.workflow.StdWorkflow; import org.hippoecm.frontend.dialog.IDialogService; import org.hippoecm.frontend.plugin.IPluginContext; import org.hippoecm.frontend.plugin.config.IPluginConfig; import org.hippoecm.frontend.plugins.reviewedactions.AbstractDocumentWorkflowPlugin; import org.hippoecm.repository.api.Workflow; public class CommentEditingFrontendPlugin extends AbstractDocumentWorkflowPlugin { private static final long serialVersionUID = 1L; public CommentEditingFrontendPlugin(final IPluginContext context, final IPluginConfig config) { super(context, config); add(new StdWorkflow( "action", new StringResourceModel("my-button-label", this, null, new Object[0]), context, getModel()) { public String comment; @Override protected IDialogService.Dialog createRequestDialog() { return new CommentEditingDialog(this); } @Override protected String execute(Workflow wf) throws Exception { CommentEditingWorkflow workflow = (CommentEditingWorkflow) wf; workflow.comment(comment); return null; } public String getSubMenu() { return "top"; } protected ResourceReference getIcon() { return new PackageResourceReference(this.getClass(), "my-icon.png"); } }); } }
Workflow Registration
The workflow and its frontend class need to be bootstrapped into the repository. To do this we add an entry in hippoecm-extension.xml that loads an xml with the registration of the workflow under hippo:workflows. Here’s the xml that accomplishes that, at relative path myhippoproject/cms/src/main/resources/org/example/comment-editing-workflow.xml:
<?xml version="1.0" encoding="UTF-8"?> <sv:node sv:name="comment-editing-workflow" xmlns:sv="http://www.jcp.org/jcr/sv/1.0"> <sv:property sv:name="jcr:primaryType" sv:type="Name"> <sv:value>hipposys:workflowcategory</sv:value> </sv:property> <sv:property sv:multiple="true" sv:name="jcr:mixinTypes" sv:type="Name"> <sv:value>hippo:translated</sv:value> </sv:property> <sv:node sv:name="comment-editing-workflow"> <sv:property sv:name="jcr:primaryType" sv:type="Name"> <sv:value>frontend:workflow</sv:value> </sv:property> <sv:property sv:name="hipposys:classname" sv:type="String"> <sv:value>org.example.workflow.CommentEditingWorkflowImpl</sv:value> </sv:property> <sv:property sv:name="hipposys:display" sv:type="String"> <sv:value>Comment Editing Workflow</sv:value> </sv:property> <sv:property sv:name="hipposys:nodetype" sv:type="String"> <sv:value>hippo:handle</sv:value> </sv:property> <sv:property sv:name="hipposys:subtype" sv:type="String"> <sv:value>hippo:document</sv:value> </sv:property> <sv:property sv:name="hipposys:privileges" sv:type="String"> <sv:value>hippo:author</sv:value> </sv:property> <sv:node sv:name="frontend:renderer"> <sv:property sv:name="jcr:primaryType" sv:type="Name"> <sv:value>frontend:plugin</sv:value> </sv:property> <sv:property sv:name="plugin.class" sv:type="String"> <sv:value>org.example.frontend.CommentEditingFrontendPlugin</sv:value> </sv:property> </sv:node> <sv:node sv:name="hipposys:types"> <sv:property sv:name="jcr:primaryType" sv:type="Name"> <sv:value>hipposys:types</sv:value> </sv:property> </sv:node> </sv:node> </sv:node>
And finally these are the entries needed in hippoecm-extension.xml to load all the above. As can be seen, we simply create a new workflow category under /hippo:configuration/hippo:workflows, and also register the workflow under cms-preview:
<sv:node sv:name="comment-editing-workflow-registration"> <sv:property sv:name="jcr:primaryType" sv:type="Name"> <sv:value>hippo:initializeitem</sv:value> </sv:property> <sv:property sv:name="hippo:sequence" sv:type="Double"> <sv:value>30000.3</sv:value> </sv:property> <sv:property sv:name="hippo:contentresource" sv:type="String"> <sv:value>org/example/comment-editing-workflow.xml</sv:value> </sv:property> <sv:property sv:name="hippo:contentroot" sv:type="String"> <sv:value>/hippo:configuration/hippo:workflows</sv:value> </sv:property> </sv:node>
<sv:node sv:name="cms-preview-workflows-enable-comment-editing-workflow"> <sv:property sv:name="jcr:primaryType" sv:type="Name"> <sv:value>hippo:initializeitem</sv:value> </sv:property> <sv:property sv:name="hippo:sequence" sv:type="Double"> <sv:value>30021</sv:value> </sv:property> <sv:property sv:name="hippo:contentroot" sv:type="String"> <sv:value>/hippo:configuration/hippo:frontend/cms/cms-preview/workflowPlugin/workflow.categories</sv:value> </sv:property> <sv:property sv:name="hippo:contentpropadd" sv:type="String"> <sv:value>comment-editing-workflow</sv:value> </sv:property> </sv:node>
External Calls
Having done the registration of the workflow means that you can now call your repository workflow from any application that can connect to the Hippo repository. For example, for an application running in the same VM as Hippo CMS:
HippoRepository hippoRepository = HippoRepositoryFactory.getHippoRepository("vm://"); HippoSession session = (HippoSession) hippoRepository.login("admin", "admin".toCharArray()); Node documentNode = session.getNode(...); HippoWorkspace hippoWorkspace = (HippoWorkspace) session.getWorkspace(); CommentEditingWorkflow myCommentEditingWorkflow = (CommentEditingWorkflow) hippoWorkspace.getWorkflowManager().getWorkflow("comment-editing-workflow", documentNode); myCommentEditingWorkflow.comment("Wonderful article");
Button labels and UI
Now we proceed with the html and properties files for the frontend plugin. These are used for the button markup and label:
File: myhippoproject/cms/src/main/java/org/example/frontend/CommentEditingFrontendPlugin.html
<html xmlns:wicket="http://wicket.apache.org/"> <wicket:panel> <div wicket:id="action">[ACTION]</div> </wicket:panel> </html>
File: myhippoproject/cms/src/main/java/org/example/frontend/CommentEditingFrontendPlugin.properties
my-button-label: Comment
Don’t forget to add a resources configuration to the build section of your CMS pom.xml!
<resources> <resource> <filtering>false</filtering> <directory>src/main/java</directory> <includes> <include>**/*.properties</include> <include>**/*.html</include> </includes> </resource> <resource> <filtering>false</filtering> <directory>src/main/resources</directory> </resource> </resources>
Frontend Dialog
We need one last Java class, this is the wicket component that will be used as the dialog. The class lives at path: myhippoproject/cms/src/main/java/org/example/frontend/dialog/CommentEditingDialog.java
package org.example.frontend.dialog; import org.apache.wicket.markup.html.form.TextArea; import org.apache.wicket.model.IModel; import org.apache.wicket.model.PropertyModel; import org.apache.wicket.model.StringResourceModel; import org.apache.wicket.util.value.IValueMap; import org.hippoecm.addon.workflow.AbstractWorkflowDialog; import org.hippoecm.addon.workflow.StdWorkflow; import org.hippoecm.frontend.dialog.DialogConstants; public class CommentEditingDialog extends AbstractWorkflowDialog { public CommentEditingDialog(StdWorkflow action) { super(null, action); TextArea<String> commentArea = new TextArea<>( "commentArea", new PropertyModel<String>(action, "comment")); commentArea.setOutputMarkupId(true); add(commentArea); } @Override public IModel getTitle() { return new StringResourceModel("my-dialog-title", this, null, new Object[0]); } @Override public IValueMap getProperties() { return DialogConstants.SMALL; } }
As seen above, all coupling between this dialog and the plugin class is done via defining a wicket PropertyModel on the property "comment" of the StdWorkflow object. You may notice that the dialog can only save a new comment (overwriting an existing one). To add support for editing of existing comments, you just need to initialise the property "comment" of the StdWorkflow object in the plugin. The existing comment is available there since the document node can be retrieved via getModel().getNode(). Furthermore, handling the document property as a multivalued one would be all needed to support multiple comments.
Dialog labels and UI
Here’s the HTML file of this wicket dialog, please note we are using minimal styling. File: myhippoproject/cms/src/main/java/org/example/frontend/dialog/CommentEditingDialog.html
<html xmlns:wicket="http://wicket.apache.org/"> <wicket:extend> <div style="padding: 20px 10px"> <wicket:message key="my-dialog-label">auto</wicket:message><br/> <textarea wicket:id="commentArea" style="width: 80%; height: 100px; margin-top: 16px"></textarea> </div> </wicket:extend> </html>
And a properties file from which labels will be retrieved, at path: myhippoproject/cms/src/main/java/org/example/frontend/dialog/CommentEditingDialog.properties
my-dialog-title: Comment Editing plugin my-dialog-label: Please enter your comment
Complete!
These were all the files we needed. Next we build the project with mvn clean install, and run it with mvn -Pcargo.run. Here’s how it looks!
Button on the toolbar
As you may notice we’re missing the icon for our button. Looking at the CMS frontend class, we can see it expects an icon named "my-icon.png" on the same path as the class itself. This icon’s dimensions should be 16x16 pixels.
Dialog
Try it yourself
- Clone the demo project at https://github.com/onehippo/labs/tree/master/workflowbutton
- Build and run the project
Conclusion
Developing CMS frontend plugins and repository workflows has become a very straightforward process in Hippo 7.9 and beyond. Few, but admittedly delicate key points in our code couple the dialog to the plugin, and the plugin to our repository workflow, while the registration is such that we can reuse the repository workflow from outside of the CMS environment. The out of the box Hippo APIs, like WorkflowImpl, StdWorkflow, AbstractDocumentWorkflowPlugin and AbstractWorkflowDialog, provide transparent usage of built-in UI components and automatic wiring with the repository workflows. These conventions and mechanisms really make our lives easier when plugging all things together. I hope I’ve managed to clearly demonstrate this in this blog entry.