Monday, May 18, 2015

Content Management Lifecycles in Nuxeo


Lifecycle Versus Workflow

Content lifecycle refers to the states or phases that content transitions through over time. A lifecycle can be visually represented using UML or a state transition diagram.  Content states act as identifiers that can be used by search.  They're also often used to control content access permissions.  Additionally, the change of content state can cause actions to be triggered.

Content lifecycles shouldn't be confused with workflows.  Workflows are used to manage the processes that occur within and between lifecycles.  They model the flow of tasks associated with a process.  Very often content will be attached to a workflow and acted on as part of the completion of the tasks that make up the workflow.

It's a best practice to cleanly separate lifecycle and workflow when implementing content management.  Workflow typically is the thing that enables content to move through the different states of its lifecycle.  But because lifecycles and workflows work closely together, it's easy to focus on building workflows without explicitly modeling the states of the lifecycle.

Very often lifecycle is something that is implicitly modeled in content management systems.  In that case, metadata schemas or content models will be extended to include a state variable to track the state of the content.  Workflows are then built to update state information as content moves through a process.

Nuxeo Content Lifecycle

In Nuxeo the concept of content lifecycles comes prewired as part of the Studio web-based configuration tool.  Within Studio you can define the states in the lifecycle and the transition paths between them.

Here is a screenshot from Studio of the default lifecycle state transition diagram.  For the default lifecycle, possible states include project, obsolete, deleted and approved.


Using Studio, let's now define a new lifecycle called ReviewCycle with states called Draft, Review, Reject, Available, Obsolete as follows:


This new lifecycle can then be associated with a new document type definition called ReviewType using Studio configuration forms.  All ReviewType documents on creation will automatically be associated with this new lifecycle definition.  Within Studio, in the definition of the document type, we reference the ReviewCycle Lifecycle as follows:

In this case our new document type is defined as inheriting from the standard File type.  We'll make no other changes, other than to define a different label and icons associated with the new document type.

To briefly see how our new document type and lifecycle with states work, we can create a simple demonstration using an Automation Chain that transitions a ReviewType document that is in the initial state Draft to the state Review.  Note that the name of the transition between these two states is called to_Review.

Next let's create a button in the UI that the user can press that we can program to change the state of the document.  To do that we create a User Action as follows:

Here we've uploaded a new icon of a right-pointing arrow that will be visible as a button to click on that will then run our Automation Chain.  Note that we specify that the button should be visible only when a document is in the lifecycle state of Draft.  At the bottom of the definition form we specify the Automation Chain that the button is associated with: setReviewLifeCycle.

Now let's run our test.  We first create our new document type called ReviewType (label is Review):


On the summary page for the document we can see that the state of the document is initially Draft.  Next to the permanent link icon on the document Title header we can see a new icon that we've added that when clicked on will automatically transition the state of the lifecycle to Review.


When we click on this transition icon, the summary page refreshes and the document is then in the Review state.  Note that the transition icon button is no longer displayed in the Title.


One more thing we can demonstrate is how easy it is to then programatically locate all documents that are in a specific lifecycle state.  We can power up the Nuxeo shell and run an NXQL query against the repository to find all documents that are in the Review state.  In our case, it will be just the one we uploaded and transitioned.

We can see that in the next screenshot where we issue an NXQL query.  The query finds the document (called Application) that we've just uploaded and transitioned into the Review state.

Select * from Document where ecm:currentLifeCycleState = 'Review'.


Saturday, May 9, 2015

Nuxeo Automation Scripting with Nashorn

In March, Nuxeo Platform Fast Track 7.2 was released.  One new feature of that release is Automation Scripting enabled by Java 8 and Nashorn.

Nashorn replaces the Rhino Javascript scripting engine in Java 8.  Nashorn is based on JSR 262 and provides better compliance with the ECMA normalized Javascript specification.  Compared to Rhino, the performance and memory usage of Nashorn is significantly better.

Thierry Delprat wrote a blog that introduces Nuxeo Automation Scripting with Nashorn.

I wanted to test out the new feature, so I thought that I'd apply scripting to a "drop folder" use case.  In a "drop folder"scenario, an action is triggered that processes and then files documents as they are created and dropped into a folder.

This is typically how imports from a capture product like Ephesoft are handled.  For example, in the Ephesoft case, scanned images are written to a repository folder using CMIS, and then a rule or action associated with the folder pushed to from Ephesoft further processes document metadata and ultimately files the document into a target folder.

The Drop Folder Scenario

I wanted to see how easy it would be to use Nuxeo Automation Scripting to create a "drop folder" script.

For this test scenario, I created two Workspaces in Nuxeo called "Drop Folder" and "Target Folder".



Under Target Folder, I created the following three folders:


The idea for the test script is that documents will be uploaded into the "Drop Folder".  Based on the mimetype of the document, it will be moved into the "Target Folder" area and filed under "PDF Files" if the document is PDF, under "Word Files" if the mimetype is Word, or otherwise filed under "Other Files".

In addition, the description field for the document will be updated with information stating that the document was autofiled and the time the file was made.

Implementation

To implement this, in Nuxeo Studio, I first created an Automation Script called "OnImportScript" and an Event Handler called "OnDocImport".


The event handler tracks the "Document created" events and is triggered when items are created in the folder "/default-domain/workspaces/Drop Folder".  The operation that is run when the event is triggered is called OnImportScript.

Here is the screen for configuring the event handler.


Next I filled in the Javascript code that runs when the event triggers.  The Javascript code for the OnImportScript is shown below.  In that code, the run() method will be called to start the processing.

The run() method identifies the location of Target Folder and moves the document based on the mimetype.


var WORD_MIMETYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
var PDF_MIMETYPE = "application/pdf";

var WORD_FOLDER = "Word Files";
var PDF_FOLDER = "PDF Files";
var OTHER_FOLDER = "Other Files";
var TOP_TARGET_FOLDER = "Target Folder";

//  Get the parent node for the current node
function getParentFolder(node)
{
    // Get the parent reference -- org.nuxeo.ecm.core.api.DocumentRef
  var parent = node.getParentRef();
  return Repository.GetDocument(node, { 'value': parent.toString() });  
}

// Return a child of a folder by name
function getChildByName(fldr, childName)
{
  if(! fldr.getDocumentType().isFolder() ) return null;
  
  var children = Document.GetChildren(fldr, {});

  if(children==null) return null;
  
  for each(var child in children)
  {
      if(child.getName().equals(childName))  return child;
  }

  return null;
}

function getFolderChild(fldr, childName)
{
  var child = getChildByName(fldr, childName);
  if(child)  return child;
  
  // Create the child folder if it doesn't exist
  if(! fldr.getDocumentType().isFolder() ) return null;
  return Document.Create(fldr, { 'type': 'Folder', 'name': childName });

}

//  ctx --  java.util.HashMap
//  input -- org.nuxeo.ecm.core.api.impl.DocumentModelImpl
//  params --  java.util.HashMap
function run(ctx, input, params) {
  
  // Event is instance of org.nuxeo.ecm.core.event.impl.EventImpl
  // If the event isn't "Create Document", then return
   if(!ctx.Event.getName().equals("documentCreated")) return;
   
  //  If the input isn't a document, then return
  if( input===null || !input.getClass().getName().equals("org.nuxeo.ecm.core.api.impl.DocumentModelImpl") ) return;
  
  // If the input is a folder and not a document, ignore it
  if( input.getDocumentType().isFolder() ) return null;
  
  // Collect some information about the document
  var doc = input;
  var docBlob = Document.GetBlob(doc, {});
  var mimeType = docBlob.getMimeType();
  
  var dropFolderNode   = getParentFolder(doc);
  var dropFolderParent = getParentFolder(dropFolderNode);
  var targetFolder =  getFolderChild(dropFolderParent, TOP_TARGET_FOLDER); 
  
  // File the incoming document based on its mimetype
  var targetFolderSub;
  if(mimeType.equals(PDF_MIMETYPE))
  {
      targetFolderSub = getFolderChild(targetFolder, PDF_FOLDER);
  }
  else if(mimeType.equals(WORD_MIMETYPE))
  {
      targetFolderSub = getFolderChild(targetFolder, WORD_FOLDER);
  }
  else
  {
     targetFolderSub = getFolderChild(targetFolder, OTHER_FOLDER);
  }
  
  //  File the document by moving it to the target folder
  if(targetFolderSub != null)
  {
     // Move the document to the correct target folder
     Document.Move(doc, { 'target': targetFolderSub.getId()} );
     var now = new Date();
     Document.SetProperty(doc,{"xpath":'dc:description', "save":true, "value":"Autofiled from folder '" + dropFolderNode.getName() + "' at " + now.toLocaleString()  });
  }

} 

Results

To test, I created documents in the Drop Folder and saw that the Document created event successfully triggered and filed the documents to the correct target folders based on the mimetype.

I was happy with the results, although my testing was minimal.  The purpose of this was just to see how Nuxeo Automation Scripting works and to validate that the approach could be used for creating Javascript-based folder rules, although the approach isn't limited to just putting rules on folders.