wissel.net

Usability - Productivity - Business - The web - Singapore & Twins

Fun with {{Mustache}} and Notes Forms


Creating output from your objects is a never ending story. In XPages we use Expression Language, in classic Notes forms (including $$ViewTemplates). For the JavaScript front-end developers there is an ever growing selection and there's the good old String concatenation. On the JavaScript side I like AngularJS and Mustache.
The big question with templating is: how much logic should go into the template. Mustache is one of the logic-less approaches that expects most of the logic to be presented by the controller. Logic is limited to repeats for collections and conditional rendering if a element is there or not. I like Mustache, because it is polyglot and can be used in more than one language. The creator of the Java version is Sam Spullara who has a nice explanation on Mustache logic-less, so go and read it. Being logic-less reduces the temptation to break the MVC pattern.
Mentioning "view" always draws the mental picture of a User Interface (with access to an controller), but there are other use cases: generating a report or transforming one source code into another. I like to do these activities using XSLT (Come and see that in action next week) when the result is XML (including HTML), but that approach is not suitable when the outcome would be mainly plain text. Mustache makes that task very easy: Create a sample file and then replace the sample data with {{variables}}.
So I was looking how to apply that for XPages. The sample template is rather simple - more useful templates are subject to later posts:

Test Result
Class: {{.}}
Form: {{form}}
Fields:
{{#fields}}
{{fieldName}}, {{fieldType}} {{#multiValue}}, Multivalue {{/multiValue}}
{{/fields}}
The interesting part is the Java component. With a small helper I extract the fields from a form (from an On Disk Project) and render the result. You could generate custom XPages out of existing forms, or generate data Objects to bind to (instead of binding to a document directly. You can generate reports. There's no limit for ideas (actually there is still the 86400 seconds/day limit ). My helpers look like this:

package com.notessensei.domistache;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import com.github.mustachejava.DefaultMustacheFactory;
import com.github.mustachejava.Mustache;
import com.github.mustachejava.MustacheFactory;

public class CommandLine {

 public static void main(String[] args) throws IOException {
  if (args.length < 3) {
   System.out.println("Usage: domistache DXLSource template output");
   System.exit(1);
  }

  CommandLine cl = new CommandLine(args[0], args[1], args[2]);
  cl.convert();
  System.out.println("Done!");

 }

 private final String sourceName;
 private final String outputName;
 private final String templateName;

 public CommandLine(String sourceName, String templateName, String outputName) {
  this.sourceName = sourceName;
  this.templateName = templateName;
  this.outputName = outputName;
 }

 public void convert() throws IOException {
  File source = new File(this.sourceName);
  File target = new File(this.outputName);
  File template = new File(this.templateName);

  InputStream templateStream = new FileInputStream(template);
  InputStream sourceStream = new FileInputStream(source);
  OutputStream out = new FileOutputStream(target);

  CoreConverter core = new CoreConverter(this.getTemplate(templateStream));
  FormConverter form = new FormConverter(sourceStream);

  form.convert(core, out);

  sourceStream.close();
  templateStream.close();
  out.close();
 }

 private Mustache getTemplate(InputStream in) {
  BufferedReader r = new BufferedReader(new InputStreamReader(in));
  MustacheFactory mf = new DefaultMustacheFactory();
  return mf.compile(r, "template");
 }
}
You can see from the FormConverter and CoreConverter signatures, that they don't depend on anything file system specific, but use Streams. This allows to also use them in web renderings (HTML notifications are a good candidate for that) or other places where information comes from other sources than a File().

package com.notessensei.domistache;

import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

public class FormConverter {

 private final Document    form;
 private final Collection<FieldInfo> fields = new ArrayList<FieldInfo>();

 public FormConverter(InputStream in) {
  Document d = null;
  DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
  factory.setValidating(false); // Will blow if set to true
  factory.setNamespaceAware(true);
  InputSource source = null;
  try {
   source = new InputSource(in);
   DocumentBuilder docb = factory.newDocumentBuilder();
   d = docb.parse(source);
  } catch (Exception e) {
   e.printStackTrace();
   d = null;
  }
  this.form = d;
 }

 public boolean convert(CoreConverter converter, OutputStream out) {
  this.populateFields(this.form, this.fields);
  return converter.convert(this, out);
 }

 public final Collection<FieldInfo> getFields() {
  return this.fields;
 }

 public final Document getForm() {
  return this.form;
 }

 private NodeList getFieldList(Document doc) {
  Object exprResult = null;
  XPath xpath = XPathFactory.newInstance().newXPath();
  // We need that otherwise the transformations fail!
  MagicNamespaceContext nsc = new MagicNamespaceContext();
  // this makes sure our XPath works
  xpath.setNamespaceContext(nsc);

  try {
   exprResult = xpath.evaluate("//d:field", doc, XPathConstants.NODESET);
  } catch (XPathExpressionException e) {
   System.err.println("XPATH failed for //d:field");
   System.err.println(e.getMessage());
  }

  return (exprResult == null) ? null : (NodeList) exprResult;

 }

 private void populateFields(Document d, Collection<FieldInfo> fic) {
  NodeList nl = this.getFieldList(d);
  for (int i = 0; i < nl.getLength(); i++) {
   Element e = (Element) nl.item(i);
   String type = e.getAttribute("type");
   String name = e.getAttribute("name");
   String multiValue = e.getAttribute("allowmultivalues");
   FieldInfo fi = new FieldInfo(name, type, "true".equals(multiValue));
   fic.add(fi);
  }
 }
}
Finally, the CoreConverter which is just a very light wrapper around the parsing operation. In real live situations you probably would use the template with a multitude of source files (e.g. a report for all forms), so the core converter has the template in its constructor

package com.notessensei.domistache;

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;

import com.github.mustachejava.Mustache;

public class CoreConverter {

 private final Mustache template;

 public CoreConverter(Mustache template) {
  this.template = template;
 }

 public boolean convert(Object data, OutputStream out) {
  boolean result = true;

  try {
   PrintWriter p = new PrintWriter(out);
   this.template.execute(p, data).flush();
   p.flush();
   p.close();
  } catch (IOException e) {
   e.printStackTrace();
   result = false;
  }

  return result;
 }

}

As usual YMMV

Posted by on 12 March 2014 | Comments (0) | categories: Software

Comments

  1. No comments yet, be the first to comment