Skip to content

Writing Wurblets

Because the wurblet-level code is Java, you can use all of its features and its ecosystem. An object-oriented design in particular helps to keep the .wrbl files small and readable.

Remember that wurblets can generate any kind of code, not only Java code.

When designing your own wurblets, always keep in mind that wurblets should generate only a certain aspect or code fragment, for example a single method or a few methods that semantically belong together. You can have as many wurblet anchors in your source files as needed. For larger contexts it is better to use several specialized wurblets than a single one that does the whole job. Furthermore, it's perfectly possible to combine the wurbelizer with traditional template engines such as Velocity or Freemarker. For example, the Tentackle Wizard first generates the source files which contain the wurblet anchors and the wurbelizer later generates the implementation according to the model. See the Tentackle tutorial for a sample project.

Wurbelizer API

All wurblets must implement the interface org.wurbelizer.wurblet.Wurblet. The most important method for the wurblet to access its runtime environment is getContainer() which returns an instance of the interface org.wurbelizer.wurbel.Wurbler.

There are 2 abstract classes that already provide some basic implementations and that can easily be extended via the @{extends}@ directive.

  1. org.wurbelizer.wurblet.AbstractWurblet: base class for all kinds of wurblets.
  2. org.wurbelizer.wurblet.AbstractJavaWurblet: base class for all wurblets that generate Java code.

Wurbler Properties

The wurbler provides a set of properties for the wurblet. Properties are categorized into namespaces as follows:

  • Wurbler.PROPSPACE_ENV ("env"): namespace holding the environment variables
  • Wurbler.PROPSPACE_WURBLET ("wurblet"): properties defined by the wurbler
  • Wurbler.PROPSPACE_EXTRA ("extra"): extra namespace usually provided by the build tool (maven, ant, ...). The maven-wurbelizer-plugin uses the wurbletProperties. The org.wurbelizer.ant.wurbel.AntWurbler will store all ant-properties here.

Wurblet Namespace

The wurblet namespace applies to the current wurblet and defines the following properties:

  • WURBPROP_FILENAME ("filename"): the pathname of the source file.
  • WURBLET_DIRNAME ("dirname"): the directory of the source file.
  • WURBLET_WURBNAME ("wurbname"): the optional pathname of the .wurb-file (additional properties), null if none.
  • WURBLET_GUARDNAME ("guardname"): the name of the guarded block.
  • WURBLET_WURBLETNAME ("wurbletname"): the name of the wurblet.

Skeleton extending AbstractJavaWurblet

The following code skeleton is a good starting point for writing your own wurblet.

@{package com.example.wurblets}@
@{import java.util.*}@
@{import java.io.*}@
@{import org.wurbelizer.wurbel.*}@
@{import org.wurbelizer.wurblet.*}@
@{import org.wurbelizer.misc.*}@
@{extends AbstractJavaWurblet}@
@{args}@
@{comment
Describes what's generated, the wurblet usage and access to the model.
This is javadoc!
}@
@[
  private void wurbel() throws WurbelException {

    ... template code goes here ...

  }
]@

Errorhandling

A wurblet should throw one of the following exceptions when code generation fails:

  • org.wurbelizer.wurbel.WurbelException: is counted as a build error and terminates the build process at the end of the corresponding build phase. The generated output is replaced by the stacktrace enclosed in a comment block.
  • org.wurbelizer.wurbel.WurbelDiscardException: same as WurbelException but instructs the wurbler to discard the generated source. The error is counted as a warning and is treated as if there was no wurblet at all. Useful if a wurblet fails, but it's smarter to leave the existing code unchanged and continue with the build process.
  • org.wurbelizer.wurbel.WurbelTerminationException: terminates the build process immediately! Wurblets should throw this exception if the error is severe enough that it doesn't make sense to execute any further wurblets, because they would fail as well.

Tentackle's Include wurblet shows the everyday case — an error that is only an error depending on an option. It wraps I/O failures in a WurbelException, but when the caller passed --missingok and the cause is a missing file, it emits a comment instead and lets the build continue:

    catch (IOException ex) {
      throw new WurbelException("including " + filename + " failed", ex);
    }
    catch (WurbelException ex)  {
      if (missingOk && ex.getCause() instanceof FileNotFoundException) {
]@
        // no such file '@(filename)@' (ok)
@[
      }
      else {
        throw ex;
      }
    }

The distinction is used deliberately across the framework: a missing heap file for an entity that simply has no remote methods is normal, whereas a missing model file is a build error.

WurbelDiscardException covers a subtler situation. A heap file collected by an earlier, failed wurblet may be incomplete; generating from it would silently produce wrong code. Heap files can therefore be marked as discarded (HeapStream.discard()), and a wurblet reading such a file throws WurbelDiscardException so that the existing source is left untouched and the real error — the one that broke the producer — is what gets reported.

WurbelTerminationException is reserved for setup problems. Tentackle throws it, for example, when neither the templateDir nor the projectRoot wurblet property is set, because in that case no wurblet in the entire build could possibly succeed.

Logging

The wurbler provides access to the logger of the build tool (Maven, Ant, etc...) via Wurbler.getLogger().

It provides the following logging levels:

  • debug
  • info
  • warning
  • error

Example:

    getContainer().getLogger().error("didoedeldu failed", ex);

Examples

A lot of real-world examples can be found in the Tentackle framework:

Tentackle wurblets

Tentackle persistence wurblets

Tentackle generates its whole persistence layer with the Wurbelizer and is thus a good place to see how the techniques described here play out at scale. Its wurblets are distributed over two modules — tentackle-wurblets for model-level, storage-independent code and tentackle-persistence-wurblets for SQL and remote-delegate code — and they range from a dozen lines to a couple of thousand.

A guided tour

If you want to read real wurblets, this is a reasonable order, from the simplest technique to the most involved:

Wurblet Lines Illustrates
Inject 50 an inline wurblet; scans the arguments, prints one value, no model at all
ColumnLengths 27 the minimal model-driven wurblet: iterate over attributes, emit a constant each
AttributeNames 36 the same, plus a boolean option (--noif) altering the emitted code
MethodCache 23 escaping @ in generated annotations (\@Override)
Include 65 reading a file or heap file; error handling with an optional --missingok
Methods 180 branching on many model options; generating javadoc along with the code
ModelComment 189 @{phase 2}@; generating a comment rather than code
DbUpdateBy 111 @{config}@, shared .incl fragments and three output streams at once
PdoRelations 2063 how big a wurblet can get before you should reconsider the design

Note the proportions. The bulk of Tentackle's wurblets is small, because each one generates a single aspect; the large ones are the exception, not the model to follow.

The parent-class pattern

The single most useful structural idea to take from Tentackle is that almost no logic lives in the .wrbl files. Each wurblet begins with

@{include $currentDir/header.incl}@

and that shared header sets the imports and, crucially, the base class — ModelWurblet or DbModelWurblet, both ordinary Java classes in the same module. They parse the model, resolve the entity for the current source file, evaluate wurblet arguments and offer the result as methods such as getEntity(), getMethodArguments() or createStatementId().

What remains in the .wrbl file is close to a pure template. The benefit is not only brevity: Java in a .java file can be unit-tested, debugged and refactored by the IDE, whereas the same code inside @[ ... ]@ cannot be nearly as well. As a rule of thumb, if a wurblet contains a loop whose body is not output, that loop probably belongs in the base class.

The same reasoning applies one level down, to .incl files: logic that several wurblets need but that does produce output — the SQL WHERE-clause generation in genwhere.incl, for example — is factored out as an include instead of being copied.

Generating into several files

DbUpdateBy and its siblings are worth studying for one more reason: a single anchor in one source file makes them emit code into three. They obtain two additional streams backed by heap files and select them with the @{to}@ directive, while a later pass over the delegate sources includes what was collected. The mechanism is described end to end in Cross-file generation with heap files.

This is the Wurbelizer's answer to a problem template generators usually solve by owning whole files: the generated method, its remote signature and its forwarding implementation are three views of one decision, so they are written by one wurblet — but each lands in the file where it belongs, next to hand-written code, in a guarded block.