Use a Custom Bulk Workflow - BloomReach Experience - Open Source CMS

This article covers a Hippo CMS version 11. There's an updated version available that covers our most recent release.

13-10-2016

Use a Custom Bulk Workflow

This feature is only available in Hippo DX.

Configuration options

For performance reasons the bulk operation is executed on a maximum of 1000 documents by default. If you happen to be in need of a higher limit, you can configure it at

/hippo:configuration/hippo:frontend/cms/cms-static/advancedSearchController

and setting the property (long)  query.limit to the desired maximum.

Add a Bulk Workflow

A custom bulk workflow operation can be added to the workflow menu. This requires a number of ingredients:

  • a workflow (java) interface

  • a workflow implementation

  • a workflow frontend plugin

  • a workflow category

The workflow interface

The workflow that will serve as an example will add a tag to all documents in the collection.

public interface CollectionTaggingWorkflow extends Workflow {

  void addTag(String tag)
    throws WorkflowException, RepositoryException, RemoteException;
}

Workflow Implementation

The implementation is a bit more involved:

import static com.onehippo.cms7.search.workflow.CollectionWorkflowReasons.DOCUMENT_NOT_FOUND;
import static com.onehippo.cms7.search.workflow.CollectionWorkflowReasons.INVALID_WORKFLOW;
import static com.onehippo.cms7.search.workflow.CollectionWorkflowReasons.WORKFLOW_FAILED;

public class CollectionTaggingWorkflowImpl extends WorkflowImpl
    implements CollectionTaggingWorkflow, InternalWorkflow {

  static final Logger log = LoggerFactory.getLogger(CollectionTaggingWorkflowImpl.class);

  private final Session session;
  private final Node subject;
  private final CollectionState state;

  public CollectionTaggingWorkflowImpl(
      Session userSession, Session rootSession, Node subject)
      throws RemoteException, RepositoryException {

    this.session = rootSession;
    this.subject =
        rootSession.getNodeByIdentifier(subject.getIdentifier());
    this.state = new CollectionState(this.subject);
  }

  @Override
  public void addTag(final String tag)
      throws WorkflowException, RepositoryException, RemoteException {

    final WorkflowContext context = getWorkflowContext();

    ReportWorkflow reportWorkflow =
        (ReportWorkflow) context.getWorkflow("collection-reports");
    if (state.isReady()) {
      if (state.getReport().hasFailures()) {
        throw new WorkflowException(
          "Cannot start workflow when existing failures have not been acknowledged");
      }
      reportWorkflow.cleanupReport();
    }

    reportWorkflow.createReport("addTag");
    Report report = state.getReport();
    Collection collection = state.getCollection();

    int succeeded = 0;
    int executed = 0;
    try {
      for (String handleId : collection.getDocumentIdentifiers()) {
        executed++;
        try {
          Node handle = session.getNodeByIdentifier(handleId);
          Workflow workflow =
                context.getWorkflow("default", new Document(handle));
          if (workflow instanceof DocumentWorkflow) {
            DocumentWorkflow documentWorkflow = (DocumentWorkflow) workflow;
            if (addtagToDocument(documentWorkflow, tag, handle.getPath())) {
              succeeded++;
            } else {
              report.addFailure(new Failure(handle, WORKFLOW_FAILED));
            }
          } else {
            report.addFailure(new Failure(handle, INVALID_WORKFLOW));
          }
        } catch (ItemNotFoundException e) {
          report.addFailure(new Failure(null, DOCUMENT_NOT_FOUND));
        } finally {
          report.setNumberOfProcessedDocuments(executed);
          session.save();
          session.refresh(false);
        }
      }
    } finally {
      reportWorkflow.finishReport(succeeded);
      session.save();
      session.refresh(false);
    }
  }

  private boolean addtagToDocument(
      DocumentWorkflow workflow, String tag, String handlePath)
          throws RepositoryException, WorkflowException, RemoteException {
    try {
      final Document document = workflow.obtainEditableInstance();
      final Node editableNode = document.getNode(session);

      Value[] values;
      if (editableNode.hasProperty("hippogogreen:tags")) {
        values = editableNode.getProperty("hippogogreen:tags").getValues();
      } else {
        values = new Value[0];
      }

      Value[] newValues = new Value[values.length + 1];
      System.arraycopy(values, 0, newValues, 0, values.length);
      newValues[values.length] = session.getValueFactory().createValue(tag);

      editableNode.setProperty("hippogogreen:tags", newValues);
      session.save();

      workflow.commitEditableInstance();
      return true;
    } catch (Exception e) {
      log.error("Failed to execute workflow on document at {}",
          handlePath, e);
      return false;
    }
  }

  @Override
  public Map<String, Serializable> hints() throws WorkflowException {
    Map<String, Serializable> hints = super.hints();
    hints.put("addTags", canStart());
    return hints;
  }

  private boolean canStart() {
    if (state.getWorkflowPhase() == CollectionState.Phase.INITIALIZED) {
      return true;
    }
    if (state.getWorkflowPhase() == CollectionState.Phase.COMPLETE) {
      Report report = state.getReport();
      return !report.hasFailures();
    }
    return false;
  }
}

A number of utilities are used in this code, those are also recommended for other customizations. In particular, note the use of

  • ReportWorkflow This workflow handles the lifecycle of the report for the collection

  • CollectionState This wrapper around JCR provides access to the Collection and Report objects, removing the need to do this with plain JCR operations

  • Collection The collection of documents

  • Report The report, containing the failures that occurred. When the action is complete, the report should be finished.

Workflow Plugin

The last piece of Java code needed is for making the workflow available in the CMS:

public class CollectionTaggingWorkflowPlugin extends RenderPlugin {

  public CollectionTaggingWorkflowPlugin(final IPluginContext context,
                      final IPluginConfig config) {
    super(context, config);

    WorkflowDescriptorModel model = (WorkflowDescriptorModel) getModel();
    final ISearchContext searcher = context.getService(
          ISearchContext.class.getName(), ISearchContext.class);

    add(new StdWorkflow<CollectionTaggingWorkflow>
             ("addTag", Model.of("Add Tag"), context, model) {

      @Override
      protected String execute(CollectionTaggingWorkflow workflow)
            throws Exception {

        if (searcher != null) {
          searcher.saveCollection();
        }
        workflow.addTag("tag");

        return null;
      }
    });
  }
}

The search context is invoked ( ISearchContext#saveCollection) in order to populate the collection with the search results.

Unfortunately, building wizards like the one for the PublicationWorkflowPlugin is quite involved. This is mainly due to technical constraints such as the (managed) plugin lifecycle. The reusable parts of the implementation have been made available in the advanced search frontend-api module. See the BulkWorkflowWizard and the parts that it is composed of.

Workflow Configuration

To tie it all together, we need to create a workflow category. We'll use 'collection-tags' as the name. Import the following into /hippo:configuration/hippo:workflows

<?xml version="1.0" encoding="UTF-8"?>
<sv:node sv:name="collection-tags" 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:node sv:name="collections">
    <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>
        com.onehippo.hgge.search.workflow.CollectionTaggingWorkflowImpl
      </sv:value>
    </sv:property>
    <sv:property sv:name="hipposys:display" sv:type="String">
      <sv:value>Collections workflow</sv:value>
    </sv:property>
    <sv:property sv:name="hipposys:nodetype" sv:type="String">
      <sv:value>hippocollection:collection</sv:value>
    </sv:property>
    <sv:property sv:multiple="true" 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: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>
          com.onehippo.hgge.search.workflow.CollectionTaggingWorkflowPlugin
        </sv:value>
      </sv:property>
    </sv:node>
  </sv:node>
</sv:node>

And to get the category to be used by the advanced search perspective, we'll need to add the 'collection-tags' category to the /hippo:configuration/hippo:frontend/cms/cms-advanced-search/workflowPlugin configuration. To set the menu name, add a property "collection-tags" with the value "Tags" to /hippo:configuration/hippo:translations/hippo:workflows/en either by editing in the console directly or by bootstrapping a new initialize item as explained in repository resource bundles.

The end result is a new menu in the workflow bar:

Did you find this page helpful?
How could this documentation serve you better?
On this page
    Did you find this page helpful?
    How could this documentation serve you better?