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

No comments: