How To Hook Into A Default Workflow Action Using Customization

You can customize the workflow by overriding the default workflow actions.  In your customized workflow you can also extend the default workflow in order to add additional functionality.  This is useful in case you want to check preconditions, override an action, etcetera.  In case you only want additional actions to be taken after the default workflow, the workflow events are a much more elegant solution.

Introduction

This example shows how you can get a custom workflow action automatically triggered when a certain standard workflow action (e.g. publish or take offline) is executed. Our use case is a “notification workflow” that prints a message whenever a document is published. While this is a trivial use case, the method described here can be used to trigger any kind of custom workflow functionality, such as sending email notifications, interfacing with external processes, etc.

The example described here is available for download. It’s a Maven project containing two modules:

  • workflow1-addon: the custom workflow code to be added to Hippo CMS

  • workflow1-cms: the custom CMS with the workflow1-addon module added

To build the project run the following maven command from the projects main directory:

mvn install

To run the CMS, run the folling Maven command from the 'cms' subdirectory:

mvn jetty:run-war

The example is based on Hippo CMS 7.3 release 2.12.20.

Creating a custom notification worklfow

Let’s start with our custom workflow functionality. We want to implement a “notification workflow” that provides a “notify” feature. Our workflow might have the following interface:

public interface NotificationWorkflow extends Workflow {

    public void notify(String event, String documentId, String userId);
}

And this interface might be implemented as follows:

public class NotificationWorkflowImpl extends WorkflowImpl implements NotificationWorkflow {

    public NotificationWorkflowImpl() throws RemoteException {
        super();
    }

    public void notify(String event, String documentId, String userId) {
        System.out.println("User " + userId + " performed action " + event + " on document " + documentId);
    }

}

This implementation just prints a message, but it’s not hard to imagine more useful variations, such as sending out an email message, or notifying an external process.

The Hippo workflow system uses JDO to map workflow objects to nodes in the repository. Our workflow class does not have any properties so we just add the package and class to the package.jdo file:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE jdo PUBLIC "-//Sun Microsystems, Inc.//DTD Java Data Objects Metadata 2.0//EN" "http://java.sun.com/dtd/jdo_2_0.dtd">
<jdo>
  <package name="org.onehippo.examples.workflow">
    <class name="NotificationWorkflowImpl" detachable="true" identity-type="datastore" table="documents">
      <datastore-identity strategy="native"/>
    </class>
  </package>
</jdo>

Finally, to make our notification workflow available, we need to add it to the workflow configuration in the repository. We create a new category ‘notification’ under /hippo:configuration/hippo:workflows. In this category, we define our workflow class, the node type to which it applies, the required privileges, and a display name. The notification-workflow.xml file below contians the required node structure, and can be imported under /hippo:configuration/hippo:workflows:

<?xml version="1.0" encoding="UTF-8"?>
<sv:node xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
  xmlns:hipposys="http://www.onehippo.org/jcr/hipposys/nt/1.0"
  xmlns:frontend="http://www.onehippo.org/jcr/frontend/nt/2.0" xmlns:sv="http://www.jcp.org/jcr/sv/1.0"
  sv:name="notification">
  <sv:property sv:name="jcr:primaryType" sv:type="Name">
    <sv:value>hipposys:workflowcategory</sv:value>
  </sv:property>

  <!-- default for all hippo:document nodes -->
  <sv:node sv:name="hippo-document">
    <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.onehippo.examples.workflow.NotificationWorkflowImpl</sv:value>
    </sv:property>
    <sv:property sv:name="hipposys:display" sv:type="String">
      <sv:value>Notification workflow</sv:value>
    </sv:property>
    <sv:property sv:name="hipposys:nodetype" 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="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>

To have the repository import notification-workflow.xml at startup, we add it to hippoecm-extension.xml:

  <sv:node sv:name="notification-workflow">
    <sv:property sv:name="jcr:primaryType" sv:type="Name">
      <sv:value>hippo:initializeitem</sv:value>
    </sv:property>
    <sv:property sv:name="hippo:contentresource" sv:type="String">
      <sv:value>notification-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:property sv:name="hippo:sequence" sv:type="Double">
      <sv:value>1000.0</sv:value>
    </sv:property>
  </sv:node>

Our workflow is now fully configured and ready to use. The next step is the hook it up with the publish workflow action.

Extending the default “reviewed actions” workflow

Currently, Hippo CMS provides no standardized way to hook into the default workflow. It is therefor necessary to extend the workflow class containing the method you want to hook in to. In this example, we want to hook into the default publish action, which is implemented in the FullReviewedActionsWorkflowImpl class’ ‘publish’ method. So we extend this class in a new workflow class called MyFullReviewedActionsWorkflowImpl:

public class MyFullReviewedActionsWorkflowImpl extends FullReviewedActionsWorkflowImpl
        implements FullReviewedActionsWorkflow {

    private static final long serialVersionUID = 1L;

    public MyFullReviewedActionsWorkflowImpl() throws RemoteException {
        super();
    }
}

You might notice that MyFullReviewedActionsWorkflowImpl explicity implements the FullReviewedActionsWorkflow interface, even though it is already implemented by the parent class (and therefor, also implicitly by our class). This is required by the workflow manager: this way you can override workflow and hide existing workflow methods and replace them with your improved versions, even if they have different arguments. If the interface is not explicitly defined, the workflow manager will return null when looking up this workflow.

To provide our notification workflow with the required information, we need to get hold of the user ID and the document ID. Luckily, the user ID is already defined in our parent class, as the property userIdentity. For the document ID, we define a new property documentId. We will map this property to the document node’s jcr:uuid property in the JDO mapping later on.

public class MyFullReviewedActionsWorkflowImpl extends FullReviewedActionsWorkflowImpl
        implements FullReviewedActionsWorkflow {

    private String documentId;

    public MyFullReviewedActionsWorkflowImpl() throws RemoteException {
        super();
    }

}

Now that we have extended the FullReviewedActionsWorkflowImpl class, we can override its publish method and trigger our notification workflow:

public class MyFullReviewedActionsWorkflowImpl extends FullReviewedActionsWorkflowImpl implements
        FullReviewedActionsWorkflow {

    private String documentId;

    public MyFullReviewedActionsWorkflowImpl() throws RemoteException {
        super();
    }

    @Override
    public void publish() throws WorkflowException, MappingException {
        super.publish();

        try {
            Workflow workflow = getWorkflowContext().getWorkflow("notification");

            if (workflow instanceof NotificationWorkflow) {
                NotificationWorkflow nwf = (NotificationWorkflow) workflow;

                nwf.notify("publish", documentId, userIdentity);
            }
        } catch (RepositoryException e) {
            throw new WorkflowException("Error calling notification workflow", e);
        }
    }

}

Obviously we still want our documents to be published, so first we call super.publish(). Then we retrieve our notification workflow through the workflow context. By specifying the “notification” category, the workflow manager will look up the corresponding workflow class in the configuration, and instantiate it.

Once we have the notification workflow object available, we simply call its notify() method with the appropriate parameters.

Like our custom notification workflow, we have to map MyFullReviewedActionsWorkflowImpl in package.jdo. We add the class to the already defined package, and we map the documentId field to the jcr:uuid property:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE jdo PUBLIC "-//Sun Microsystems, Inc.//DTD Java Data Objects Metadata 2.0//EN" "http://java.sun.com/dtd/jdo_2_0.dtd">
<jdo>
  <package name="org.onehippo.examples.workflow">
    <class name="MyFullReviewedActionsWorkflowImpl" detachable="true" identity-type="datastore" table="documents">
      <datastore-identity strategy="native"/>
      <field name="documentId" column="jcr:uuid"/>
    </class>
    <class name="NotificationWorkflowImpl" detachable="true" identity-type="datastore" table="documents">
      <datastore-identity strategy="native"/>
    </class>
  </package>
</jdo>

Finally, we need to modify the reviewed actions workflow configuration and replace the default class with our extended version. We need to modify the hipposys:classname property of the node /hippo:configuration/hippo:workflows/default/reviewedactions. The myreviewedactions-workflow.xml file below contains the modified configuration node:

<?xml version="1.0" encoding="UTF-8"?>
<sv:node xmlns:hipposys="http://www.onehippo.org/jcr/hipposys/nt/1.0"
  xmlns:frontend="http://www.onehippo.org/jcr/frontend/nt/2.0" xmlns:sv="http://www.jcp.org/jcr/sv/1.0"
  sv:name="reviewedactions">
  <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.onehippo.examples.workflow.MyFullReviewedActionsWorkflowImpl</sv:value>
  </sv:property>
  <sv:property sv:name="hipposys:display" sv:type="String">
    <sv:value>Reviewed actions workflow</sv:value>
  </sv:property>
  <sv:property sv:name="hipposys:nodetype" sv:type="String">
    <sv:value>hippostdpubwf:document</sv:value>
  </sv:property>
  <sv:property sv:name="hipposys:privileges" sv:type="String">
    <sv:value>hippo:editor</sv:value>
  </sv:property>
  <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:name="org.hippoecm.repository.reviewedactions.PublishableDocument">
      <sv:property sv:name="jcr:primaryType" sv:type="Name">
        <sv:value>hipposys:type</sv:value>
      </sv:property>
      <sv:property sv:name="hipposys:classname" sv:type="String">
        <sv:value>org.hippoecm.repository.reviewedactions.PublishableDocument</sv:value>
      </sv:property>
      <sv:property sv:name="hipposys:display" sv:type="String">
        <sv:value>PublishableDocument</sv:value>
      </sv:property>
      <sv:property sv:name="hipposys:nodetype" sv:type="String">
        <sv:value>hippostdpubwf:document</sv:value>
      </sv:property>
    </sv:node>
    <sv:node sv:name="org.hippoecm.repository.reviewedactions.PublicationRequest">
      <sv:property sv:name="jcr:primaryType" sv:type="Name">
        <sv:value>hipposys:type</sv:value>
      </sv:property>
      <sv:property sv:name="hipposys:classname" sv:type="String">
        <sv:value>org.hippoecm.repository.reviewedactions.PublicationRequest</sv:value>
      </sv:property>
      <sv:property sv:name="hipposys:display" sv:type="String">
        <sv:value>PublicationRequest</sv:value>
      </sv:property>
      <sv:property sv:name="hipposys:nodetype" sv:type="String">
        <sv:value>hippostdpubwf:request</sv:value>
      </sv:property>
    </sv:node>
  </sv:node>
  <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="browser.id" sv:type="String">
      <sv:value>${browser.id}</sv:value>
    </sv:property>
    <sv:property sv:name="editor.id" sv:type="String">
      <sv:value>${editor.id}</sv:value>
    </sv:property>
    <sv:property sv:name="plugin.class" sv:type="String">
      <sv:value>org.hippoecm.frontend.plugins.reviewedactions.FullReviewedActionsWorkflowPlugin</sv:value>
    </sv:property>
  </sv:node>
</sv:node>

To automatically replace the default configuration with the new one in myreviewedactions-workflow.xml, we add it to hippoecm-extension.xml:

  <sv:node sv:name="myreviewedactions-workflow">
    <sv:property sv:name="jcr:primaryType" sv:type="Name">
      <sv:value>hippo:initializeitem</sv:value>
    </sv:property>
    <sv:property sv:name="hippo:contentresource" sv:type="String">
      <sv:value>myreviewedactions-workflow.xml</sv:value>
    </sv:property>
    <sv:property sv:name="hippo:contentdelete" sv:type="String">
      <sv:value>/hippo:configuration/hippo:workflows/default/reviewedactions</sv:value>
    </sv:property>
    <sv:property sv:name="hippo:contentroot" sv:type="String">
      <sv:value>/hippo:configuration/hippo:workflows/default</sv:value>
    </sv:property>
    <sv:property sv:name="hippo:sequence" sv:type="Double">
      <sv:value>499.0</sv:value>
    </sv:property>
  </sv:node>

Testing

Now you can build your project using Maven, start your CMS, and test your custom workflow by publishing a document. You should see a message like this on standard output:

 

User johndoe performed action publish on document 988eebef-c07c-4a2c-b36b-8566cc34c46d

Hippo Europe: +31 (0)20 5224466
Hippo North America: +1 (707) 773-4646