Fork me on GitHub

Tutorial

This is a pretty complete tutorial that leads you through the most important topics of interface implementation and binding it to the logic. It will implement an interface generator based on th reflection API of java.

An interface

As a first, quite complete example I decided to build an interface generator. The idea is to take a Class object, expected to be an interface, and create the source for it. Of course there are a number of restrictions. First there are only fields of type number or string supported, as it's impossible to figure out the construction code for any object. And comments are skipped as not available via reflection.

The challenges of this example are

  1. Decoupling of execution order.
    As the types to be imported have to be collected and the code should stay compact.
  2. White space handling.
    Is it possible to get a nicely formatted output and does this lead to a negative impact on the code?
  3. Escaping.
    How simple is it to re-escape the contents of the string?
  4. Solving the delimiter problem.
    This is a typical problem when working with templates. It some kind of logic, that's hard to get out of template.

So this example will guide through most of the parts of this engine. Now, to warm up a bit let's have a look at a typical interface definition in the Java programming language:

package org.jproggy.snippetory.examples.intfgen

import java.io.IOException

import javax.servlet.Filter;
import javax.servlet.Serlet;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

public interface FilteringServlet extends Servlet, Filter {
  int DEFAULT_RESULT = 200;
  String WHITE_SPACE = "\n\t ";
  double RATIO = 1.5;
  
  void handleRequest(ServletRequest request, ServletResponse response) throws IOException;
  boolean handeError(IOException erro, ServletRequest request, ServletResponse response);
}

It starts with a package definition. The next part are the imports. Those lists the qualified name of the types used by this interface. But only those that neither belong to the package java.lang nor to package defined in this file. The classes of those packages are available without declaration. The next line states the name of the interfaces and a list of parent interfaces. The next section are the fields of the interface. The last section are the methods.

No, that it's clear what I try to achieve some thoughts about the syntax. Snippetory ships with two of them. XML_ALIKE is default and certainly best guess for XML based languages. It allows XML editors to assist in closing the right tag which means keep your document consistent. It almost follows the rules of XML. The other syntax is HIDDEN_BLOCKS. It's based on XML comments and the /**/ block comments. The variants can be combined as needed to show or hide the content for the surrounding language. This is somewhat more complex, but allows compilable java, executable javaScript, and styling CSS. For this example I'll skip the decision and show both ways.

To templatify the result, the variable portions are replaced with location marks. These are newly defined names, which makes it a simple task. No syntactical hassle. Repetitive and optional sections have to be marked, too. In very rare cases additional attributes my help. At the moment there are about 10 and it's the same for location mark. In our case we use the delimiter attribute. This easily solves a typical template problem. Let's look at the template variants:

package {v:package};

<t:imports>
import {v:import};
</t:imports>

public interface {v:name}{v:parents prefix=' extends ' delimiter=', ' } {
<t:num_field>
  {v:type} {v:name} = {v:value};
</t:num_field>
<t:string_field>
  {v:type} {v:name} = "{v:value enc='string'}";
</t:string_field>

<t:method>  
  {v:return_type} {v:name}(<t:parameters delimiter=', '>{v:type} arg{v:i}){v:exceptions prefix=' throws ' delimiter=', ''};
</t:method>
}

And here with HIDDEN_BLOCKS syntax:

package /*$package(*/org.jproggy.snippetory.examples.intfgen/*)*/;

/* $imports{
import $import;
 }$ */

public interface $name/*${ extends /*$parent(delimiter=', ')}$*/ {
// $num_field{
  /*$type(*/int/*)*/ $name = /*$value(*/10/*)*/;
// }$
// $string_field{
  /*$type(*/String/*)*/ /*$name(*/NAME/*)*/ = "Needed"; //$value(backward='"(*)"' enc='string')
// }$

// $method{  
  /*$return_type(*/void/*)*/ $name(/*$parameters(delimiter=', '){$type arg$i}$*/)/* ${ throws $exceptions(delimiter=', '}$*/;
// }$
}

You will probably mention that using HIDDEN_BLOCKS syntax isn't a very good idea, as it makes the template less readable. This is true for such a minimal amount of template code and almost only markup. However if the ability to compile is very useful it might pay. On the imports, number field and the method I demonstrated that arbitrary region can be made invisible, so inside we don't have the full overhead of making everything compile-able. On String attribute I showed how Snippetory even is able to replace the content of a string that is fully mocked. We have seen HIDDEN_BLOCKS adds some value, but this syntax is not as handy as default.

Now that we have the templates, we need some code to bind the data. First the template is loaded. The Repo class provides a convenient way for this. Whenever a default configuration is needed or injection is desired the TemplateContext does the job.

A thing I really like on Snippetory is the fact that logic is organized in methods. Which means there is this kind of brief overview of the data bound and a simple way to navigate to the place I'm interested in. Maybe the filtering of fields? Only 3 steps away. In a JSP a similar look once took me over an hour because it was hidden in an import. That's just like binary search vs scanning all the spaghetti.

        intfTpl.set("name", intf.getSimpleName());
        intfTpl.set("package", intf.getPackage().getName());

        for (Class<?> parent : intf.getInterfaces()) {
            registerType(parent);
            intfTpl.append("parents", parent.getSimpleName());
        }

        for (Field field : intf.getDeclaredFields()) {
            renderField(intfTpl, field);
        }

        for (Method method : intf.getDeclaredMethods()) {
            renderMethod(intfTpl.get("method"), method);
        }

        renderImports(intfTpl, types);

However, besides those brief overview we can't see to much in this method. Only some of the top level values are assigned via the set method. Another thing to mention is the fact, that rendering the imports is the last step even though they appear at the top of the document. This decoupling is achieved by the abstraction layer which separates template and binding logic. Now I'll step into the renderField method.

    private void renderField(Template intfTpl, Field field) throws Exception {
        Class<?> type = field.getType();
        registerType(type);

        String fieldTplName = fieldTplName(type);
        if (fieldTplName != null) {
            Template fieldTpl = intfTpl.get(fieldTplName);
            fieldTpl.set("name", field.getName());
            fieldTpl.set("type", type.getSimpleName());
            fieldTpl.set("value", field.get(null));
            fieldTpl.render();
        }
    }

When rendering a field we have to deal with the differences between the string and the number variants. As the differences it-selves are handled by the template and the abstraction layer, logic only selects the name, acquires the template fragment from the integrated repository, binds the data via the set() method and finally renders the template. Even though usage on the definition place is most typical usage, this is not the only way. You're always free, to store a snippet where it's most convenient for maintenance. And use it where you need it. I skip the name selection. This is pretty much reflection. And step over to rendering the method. Here we can see how to deal with loops. There are 2 different forms. The throws definition is rendered by a simple location mark via append calls. The attributes prefix, suffix and delimiter allow great control on the result. This form is very simple, but it only works for a single result. Whenever more than one attribute is to be rendered, the sub template will be used. This leads to a typical get-set-render-sequence as we can see on the parameters.

    private void renderMethod(Template methodTpl, Method method) {
        Class<?> returnType = method.getReturnType();
        registerType(returnType);
        methodTpl.set("name", method.getName());
        methodTpl.set("return_type", returnType.getSimpleName());

        for (int i = 0; i < method.getParameterTypes().length; i++) {
            Class<?> type = method.getParameterTypes()[i];
            registerType(type);

            Template parameter = methodTpl.get("parameters");
            parameter.set("type", type.getSimpleName());
            parameter.set("i", i);
            parameter.render();
        }

        for (Class<?> ex : method.getExceptionTypes()) {
            registerType(ex);
            methodTpl.append("exceptions", ex.getSimpleName());
        }
        methodTpl.render();
    }

Finally you might want to have a look on the entire class. I think this is a nice example to present the power of the abstraction layer to clearly separate presentation from the logic. It keeps the logic free from presentation issue. The templates are lean and expressive while logic is concentrated somewhere else. Even though, on first sight, it seems like removing logic from the template is not a big deal, comparing the templates will show whether it is... However, saving a few characters is not the really big win. To me it's having more focus. When I work on the template I can fully focus on HTML, JavaScript and CSS. That's pretty enough to fill my mind. The only addition is a very simple markup. Putting the logic here would require much more constructs. Once I switch over to logic I use the language I'm used to, with the tool set I'm used to. The only addition is a very simple API. I like simple things and Snippetory is one.

Bernd Ebertz

Head, Founder and chief technology evangelist of
jproggy.org
Java, and all Java-based marks are trademarks or registered trademarks of Oracle.
This site is not affiliated in any way with Oracle.