Beanery – how to harden Snippetory’s type scheme

One of the things Snippetory doesn’t do better than most other engines is the string based
coupling between the template and the Java code. Compared to untyped scripts
it at least doesn’t break only because the data model changes. It’s an explicit interface
between the template and the binding code and will only break once this interface
is changed. The small namespaces allow short and expressive names, minimizing the
risk of mistakes as well as the effort to maintain a template. Nevertheless,
tooling could definitely ease writing and maintaining the binding as well as the template
code. Now, for sake of simplicity I’ll concentrate on writing and maintaining the binding
code. The idea is to generate more bean-like interface classes. This could help in several
areas:

  1. First and most important to write setTitle(title) is better
    supported by IDEs than set("title", title). Same for getRegion()
    instead of get("region").
  2. Further more such an interface could help to clarify the model-like nature of Snippetory
    template to people who are new to this concept. Most people seem to expect a template to
    be a script.
  3. And, last but not least, the code gets more concise and a little bit more friendly to the eye.

On the other hand using a tool for code generation has several challenges and draw
backs. Changes in template code need to be reflected in the generated classes instantly.
The generated code has to be available while packaging or debugging on systems
where no templates are edited. And to support use of different template fragments
by the same binding logic some kind of compatibility would be nice. (Of course, the
mentioned fragments might be entire files, too.) Even tough each problem has its
solution, setting up a project with Snipperory would get more complicated.
However, the first step is to write a generator to get an idea how this could feel.
I already did that and even though it’s not a really useful tool for now, I’d like to share it
here.
As always with Snippetory it consists of a template and some java code. And as the template
might give you the better idea how it should work I’ll start with that.

//Syntax:C_COMMENTS
package /*${package*/org.jproggy.snippetory.toolyng.beanery/*}*/;

import org.jproggy.snippetory.Template;
import org.jproggy.snippetory.spi.TemplateWrapper;
// ${ class

/*${i}*/public class /*${name case="camelizeUpper"*/Bean/*}*/ extends TemplateWrapper {
/*${i}*/	public /*${name case="camelizeUpper"*/Bean/*}*/ (Template template) {
/*${i}*/			super(template);
/*${i}*/	}
//${ region prefix="\n"
/*${i}*/	public /*${name case="camelizeUpper"*/Bean/*}*/ get/*${name case="camelizeUpper"}*/() {
/*${i}*/		return new /*${name case="camelizeUpper"*/Bean/*}*/(get("/*${name}*/"));
/*${i}*/	}
// region }
//${ location prefix="\n"
/*${i}*/	public /*${parent case="camelizeUpper"*/Bean/*}*/ set/*${name case="camelizeUpper"}*/(Object value) {
/*${i}*/		set("/*${name}*/", value);
/*${i}*/		return this;
/*${i}*/	}
// location }
/// use region with full line mark up to avoid additional line breaks
//${ classes
//   classes }
/*${i*/	/*i}*/}
// class }

The first thing you might have mentioned is the strange location simply call ‘i’ at the beginning
of each line. To understand this you have to be aware, the nested regions of the template are
modelled by nested inner classes of the generated interface class. And ‘i` is the indentation
of the inner classes. The case format normalizes the names to java naming convention. Of
course, this simple approach will cause errors if names like ‘class’ are used in the template.
However, this shows the Snippetory Abstraction Layer in action. The different presentations
of the same data are handled by the template, using formats. Thus presentation logic is separated
from binding logic. (No, it can’t be repeated often enough ;-)
Now, let’s look at the java side.

	public Template boil(Template subject, String packageName, String className) {
		target.set("package", packageName);
		Template classTpl = target.get("class");
		generate(classTpl, subject, className, "");
		classTpl.render();
		return target;
	}
	protected void generate(Template classTpl, Template subject, String regionName, String i) {
		classTpl.set("name", regionName).set("i", i);
		for (String location : subject.names()) {
			classTpl.get("location").set("name", location).set("parent", regionName).set("i", i).render();
		}

		for (String region : subject.regionNames()) {
			classTpl.get("region").set("name", region).set("i", i).render();
			Template regionTpl = target.get("class");
			generate(regionTpl, subject.get(region), region, i + indent);
			regionTpl.render(classTpl, "classes");
		}
	}

The binding code doesn’t provide too much special. The template tree is traversed recursively
and there isn`t much data to bind.
Each region responds with a get-method and a type, each location creates a set-method.
Here you can have a look at the complete code for now. The main method allows building a start
configuration in your IDE, So one can play arround with minor convenience.

package org.jproggy.snippetory.toolyng.beanery;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;

import org.jproggy.snippetory.Repo;
import org.jproggy.snippetory.Template;

public class Beanery {
	private Template target = Repo.readStream(getClass().getResourceAsStream("Bean.java")).parse();
	private String indent;

	public static void main(String[] args) throws IOException {
		if (args.length != 3) {
			System.out.println("Usage: templatePath targetDir qualifiedClassName");
			return;
		}
		new Beanery().generate(args[0], args[1], args[2]);
	}

	public Beanery() {
		indent = target.get("class","i").toString();
	}

	public void generate(String templatePath, String targetDir, String qualifiedClassName)
			throws IOException {
		Template template = Repo.readFile(templatePath).parse();
		String[] parts = qualifiedClassName.split("\\.");
		StringBuilder packageHelper = new StringBuilder();
		for (int i = 0; i < parts.length - 1; i++) { 			if (i > 0) packageHelper.append('.');
			packageHelper.append(parts[i]);
		}
		Template result = boil(template, packageHelper.toString(), parts[parts.length - 1]);
		File targetPath = new File(targetDir, qualifiedClassName.replace(".", "/") + ".java");
		targetPath.getParentFile().mkdirs();
		OutputStreamWriter out = new OutputStreamWriter(new FileOutputStream(targetPath), "utf-8");
		result.render(out);
	}

	/**
	 * Build an Object-oriented Interface Layer. This layer consists of a get-method for each
	 * region, a class for each region used as return type of the getter, and a set method
	 * for each location. This construct allows convenient navigation of templates by
	 * the syntax completion feature available in most IDEs.
	 * @param subject The template that shall get the interface layer
	 * @param packageName
	 * @param className
	 * @return
	 */
	public Template boil(Template subject, String packageName, String className) {
		target.set("package", packageName);
		Template classTpl = target.get("class");
		generate(classTpl, subject, className, "");
		classTpl.render();
		return target;
	}
	protected void generate(Template classTpl, Template subject, String regionName, String i) {
		classTpl.set("name", regionName).set("i", i);
		for (String location : subject.names()) {
			classTpl.get("location").set("name", location).set("parent", regionName).set("i", i).render();
		}

		for (String region : subject.regionNames()) {
			classTpl.get("region").set("name", region).set("i", i).render();
			Template regionTpl = target.get("class");
			generate(regionTpl, subject.get(region), region, i + indent);
			regionTpl.render(classTpl, "classes");
		}
	}
}

Any comments and ideas are highly appreciated.

Bernd Ebertz

Head, found and chief technology evanelist of jproggy.org

About these ads