Showing posts with label kaleo engine. Show all posts
Showing posts with label kaleo engine. Show all posts

Saturday, November 19, 2011

10 things a programmer should know about Liferay Kaleo Workflow Engine

Liferay developers decided to create their own workflow engine called Kaleo 2 years ago. It's a flexible lightweight workflow engine similar to other open-source engines out there. It is a plugin built on Liferay's ServiceBuilder and it needs to be deployed before you can use it (it is not a part of portal).

The principle consists in defining workflow for resources as an xml definition containing states, tasks, conditions, forks & joins and timer. Apart from timer, all of them contains transition nodes (where to go next). And apart from condition, fork&join all of them may contain actions (what to do) and hereby orchestrate a sequence of events and actions. It is especially useful for content reviewing, validation, approving and quality evaluation.


It is quite easy to use and design workflows in xml directly. But there are definitely use cases that makes you investigate its internal working and modify it or add something that you need.

Following summary lists a few things you should be aware of as a developer :
  • Liferay has it's own simple messaging implementation that is meant only for internal usage, it doesn't allow remote messaging. Kaleo uses it from two reasons :
    • Because it is an external plugin, portal context needs to communicate with it via messaging
    • Because messaging is a suitable choice for workflow engine implementation
  • A workflow definition is always associated with a corresponding resource. There are 2 key entities. For instance, in case of document library :  KaleoDefinition (definition itself) and WorkflowDefinitionLink (association between the definition and folder and file types that will be "workflow aware").
  • You cannot undeploy a definition if such an association exists. So that you first delete the link ( this corresponds to removing workflow from a resource in administration) and then you can deactivate it and undeploy it.
  • When you deploy a workflow definition, it is parsed in XMLWorkflowModelParser into an object model and for each node type (mentioned in 2. paragraph) there is a NodeExecutor type with methods enter, execute and exit
  • Everytime you add a resource, that has a workflow definition link, a workflow instance is created and its execution context is associated with it.
  • Consequently KaleoSignaler ( class that sends messages about entering, executing and exiting ) either call a corresponding NodeExecutor in case of timer node or sends a message to liferay/kaleo_graph_walker destination about entering or leaving a node (transitions). The destination is associated with DefaultGraphWalker, that steps into target nodes and call corresponding NodeExecutors. The methods are implemented differently based on type of the node.
  • Execution context contains so called Instance Token that is carrying information about current position within the workflow node model and and its state. It also holds a reference to workflow and service contexts.
  • Task nodes contain assignment element that determines recipients (content reviewers for instance) based on Role, User or just an email address. You can even dynamically determine this in runtime in scripted-assignment, which is a matter of setting a user or a role that you want in the scripting context. I wrote a blog post about improving scripting experience.
  • Actions are either notifications or whatever you need to be done. For a java developer the obvious choice would be writing groovy scripts.
  • KaleoLog entity contains information like duration, user comments etc. 

I hope this post helped you with getting grasp of Liferay Kaleo Engine's internal working. Cheers

Wednesday, November 9, 2011

Liferay Workflow Kaleo Engine tuning

It's been a pleasure to work with Liferay Workflow Kaleo Engine. I'm not going to introduce it here, you can read about it in the documentation reference. I just wanted to mention one thing that I needed to improve.
    When I was designing complex workflow definitions that were containing a lot of groovy scripts I just didn't want to write them directly in workflow xml definition. Instead, I created a many groovy script files and put ${references} into workflow definitions. During deployment I just expand them.
    With this setup I can profit from IDE support and I don't have to maintain hundreds of lines of code in a workflow definition. I just keep them basic scripts that has up to 15-20 lines. Imagine all those java import declarations, you wouldn't even know that you have that stuff on classpath.

This code takes care of it. I didn't use Liferay DOM API because it is missing a few key methods from classes that extend org.dom4j.Node and I didn't want to work that around. You just change those IllegalStateExceptions to your System ones or something.

  
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentFactory;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.SAXReader;
import org.dom4j.io.XMLWriter;
import org.jaxen.JaxenException;
import org.jaxen.SimpleNamespaceContext;
import org.jaxen.XPath;
import org.jaxen.dom4j.Dom4jXPath;

public class WhatEver {

 public byte[] expandScripts(String workFlowPath) {
  ClassLoader cl = Thread.currentThread().getContextClassLoader();

  Document document;
  try {
   document = new SAXReader().read(cl.getResource(workFlowPath));

   List<Element> scripts = findAllElements("script", document.getRootElement());

   expandScripts(cl, scripts);
  } catch (DocumentException e) {
   throw new IllegalStateException("Writing dom4j document failed", e);
  }

  return toByteArray(document);
 }

 private List<Element> findAllElements(String name, Element rootElement) {
  String ns = rootElement.getQName().getNamespaceURI();

  Map<String, String> map = new HashMap<String, String>();
  map.put("x", ns);

  List<Element> scripts;
  try {
   XPath xpath = new Dom4jXPath("//x:" + name);
   xpath.setNamespaceContext(new SimpleNamespaceContext(map));

   scripts = xpath.selectNodes(rootElement);
  } catch (JaxenException e) {
   throw new IllegalStateException("Xpath search for: '" + name + "' failed", e);
  }

  return scripts;
 }

 private void expandScripts(ClassLoader cl, List<Element> scripts) {
  DocumentFactory docFactory = DocumentFactory.getInstance();

  for (Element oldScriptElement : scripts) {
   String text = oldScriptElement.getText();

   if (!isCDATA(oldScriptElement) && text != null && text.startsWith("$")) {
    String scriptPath = text.substring(2, text.length() - 1);

    Element result = docFactory.createElement("script", oldScriptElement.getParent().getQName().getNamespaceURI());

    String script;

    try {
     script = getString(cl.getResourceAsStream(scriptPath));
    } catch (IOException e) {
     throw new IllegalStateException("Reading script: '" + scriptPath + "' failed", e);
    }
    result.addCDATA(script);

    replaceNode(oldScriptElement, result);
   }
  }
 }

 private byte[] toByteArray(Document document) {
  ByteArrayOutputStream baos = new ByteArrayOutputStream();

  XMLWriter writer;
  try {
   writer = new XMLWriter(baos, OutputFormat.createPrettyPrint());

   writer.write(document);
   writer.close();
  } catch (IOException e) {
   throw new IllegalStateException("Writing dom4j document failed", e);
  }

  return baos.toByteArray();
 }

 private boolean isCDATA(Element node) {
  for (Node n : (List<Node>) node.content()) {
   if (Node.CDATA_SECTION_NODE == n.getNodeType()) {
    return true;
   }
  }
  return false;
 }

 private void replaceNode(Element oldNode, Element newNode) {
  List parentContent = oldNode.getParent().content();

  int index = parentContent.indexOf(oldNode);

  oldNode.detach();

  parentContent.set(index, newNode);
 }

 private String getString(InputStream is) throws IOException {
  final char[] buffer = new char[0x10000];
  StringBuilder out = new StringBuilder();
  Reader in = new InputStreamReader(is, "UTF-8");
  int read;
  do {
   read = in.read(buffer, 0, buffer.length);
   if (read > 0) {
    out.append(buffer, 0, read);
   }
  } while (read >= 0);

  return out.toString();
 }
}

Gist