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 quickstart 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. This is the default parent class if no @{extends}@ directive is given.
  2. org.wurbelizer.wurblet.AbstractJavaWurblet: base class for all wurblets that generate Java code. It adds accessors for the Java source file being wurbeled, derived from the built-in variables: getClassName(), getSuperClassName(), getPackageName(), isClass(), isInterface(), isRecord(), isAbstract(), isFinal(), isPublic(), isProtected() and isPrivate().

Wurblet Lifecycle

For each anchor and each pass over a source file, the wurbler creates a new instance of the wurblet class and calls the methods of the Wurblet interface in the following order. The constructor generated by the wurbiler already sets the phase and the configuration.

  1. setContainer(Wurbler): binds the wurblet to its container. The properties are available from now on. The container then loads the fixed text from the .ser file and sets the arguments, so both are available when run() is invoked.
  2. run(): generates the code. The class generated by the wurbiler implements run() as follows:
@Override
public void run() throws WurbelException {
  super.run();
  try {
    wurbel();
  }
  catch (Throwable t) {
    if (t instanceof WurbelException) {
      throw (WurbelException) t;
    }
    throw new WurbelException("wurblet " + this + " failed", t);
  }
}

Hence, every .wrbl file must declare a method wurbel(), which is where the template code goes. AbstractWurblet.run() initializes the protected fields out (the default output stream) and source (the fixed text), so a parent class that overrides run() must invoke super.run() first, as in the config example. Any exception other than a WurbelException is wrapped in one. 3. process(String generatedCode): post-processes the complete output of run() before it is inserted into the source file. The default implementation returns the code unchanged. Override it, for example, to reformat or filter the generated text. 4. cleanup(): invoked once after the source file is completely processed, on all wurblets that ran in the last pass. The default does nothing.

run() is only invoked if the wurblet's phase matches the current phase of the wurbler. Otherwise, the guarded block is left untouched.

AbstractWurblet.getGuardName() returns the tag of the anchor that invoked the wurblet.

The Container

Besides getArgs(), getLogger() and the properties, the Wurbler returned by getContainer() provides:

  • getPrintStream(): the default output stream out.
  • getSource(): the fixed text of the wurblet, as compiled by the wurbiler.
  • getInvocationCount(): the number of the current pass over the source file, starting at 1. The wurbler repeats passes until the file no longer changes.
  • getAnalyzeFile(String name): a file in the analyze directory, i.e. the Maven parameter analyzeDir or the Ant attribute analyzedir. Other build steps, such as an annotation processor, may leave information for the wurblets there.
  • getVerbosity(): the verbosity configured in the build tool.
  • getProperty(String nameSpace, String key) and getProperties(String nameSpace): access to the properties described below. A null or empty namespace refers to the Java system properties.

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. With Ant, the namespace "ant" can be used as a synonym for "extra".

Wurblet Namespace

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

  • WURBLET_FILENAME ("filename"): the pathname of the source file.
  • WURBLET_DIRNAME ("dirname"): the directory of the source file.
  • WURBLET_FILECONTENTS ("filecontents"): the original contents of the source file, as read before the first pass.
  • 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.

Furthermore, the wurblet namespace holds all variables of the source file: those declared in the .wurb file and in @{ ... @} blocks, and the built-in variables of the source type, such as classname or packagename.

Heap Files API

Heap files are in-memory files shared by all wurblets of a build-tool run. A wurblet can read, write and delete them via the following API in org.wurbelizer.wurbel:

  • WurbelHelper.openReader(String fileName): opens a reader for a heap file if the name starts with a dot, otherwise for a file on disk. If the file doesn't exist, the cause of the thrown WurbelException is a FileNotFoundException. Reading a discarded heap file throws a WurbelDiscardException.
  • WurbelHelper.deleteFile(String fileName): deletes a heap file or a file on disk, following the same naming convention.
  • HeapStream(Wurbler container, String heapName): creates the heap file, or appends to it if it already exists. Note that heapName is given without the leading dot. getStream() returns the PrintStream to select with @{to}@, append(String) appends text, and discard() marks the file as incomplete (see Errorhandling). Since the wurbler repeats the passes over a source file until the file no longer changes, a HeapStream remembers what the other source files have written so far: when the same source file is wurbeled again, its previous contributions are reverted instead of being appended once more.
  • HeapFileFactory: low-level access via get(name), create(name, text), createOrOverwrite(sourceName, name, text) and delete(name). Again, the names are given without the leading dot.

Thread safety

Build tools may run several wurblers concurrently, for example the Maven plugin in a parallel build (see Parallel builds). Each run is bound to its own org.wurbelizer.wurbel.WurbelContext. The static accessors of the Wurbelizer API, such as WurbelHelper.getExtraProperties(), HeapFileFactory, WurbelHelper.openReader() and Settings.getEncodingCharset(), refer to the context of the current run. Keep the following in mind:

  • Wurblets are not considered thread-safe by default: Wurblet.isThreadSafe() returns false in AbstractWurblet. Such wurblets are executed sequentially, i.e., no two of them run at the same time within the same JVM, even if the build tool wurbels several modules in parallel.
  • A wurblet that doesn't share any mutable state with other wurblet instances (no mutable static fields, no non-thread-safe libraries) can opt in to run concurrently by overriding isThreadSafe() in its superclass or in its .wrbl source:
@[
  \@Override
  public boolean isThreadSafe() {
    return true;
  }
]@
  • Static fields and singletons (models, caches, factories) are shared only among the classes of the same classloader. The Maven plugin loads the wurbletDependencies in a separate class realm for each execution, and an execution processes its sources one after another on one thread. So a wurblet with such state is still thread-safe, as long as its class was actually loaded by the realm of the current run:
@[
  \@Override
  public boolean isThreadSafe() {
    ClassLoader loader = WurbelContext.current().getWurbletClassLoader();
    return loader != null && getClass().getClassLoader() == loader;
  }
]@

getWurbletClassLoader() returns null if the run has no realm of its own, for example with Ant or the console, or if the wurblets are plugin <dependencies> instead of wurbletDependencies. In that case all runs share the same classes, and the wurblet falls back to sequential execution. The Tentackle wurblets use this approach: see org.tentackle.wurblet.WurbletThreadSafety, which is used by ModelWurblet, DTOWurblet and IncludeWurblet. - The context is bound to the current thread. Threads started by a wurblet don't see it, unless they are forked via java.util.concurrent.StructuredTaskScope. - The JVM shares cached jar files among all classloaders. When a run disposes its realm, the shared jar file may be closed, and a concurrent read in another realm fails with "zip file closed". This affects all code that runs during wurbeling and reads resources from jars via URL.openStream() or getResourceAsStream(), including libraries used by the wurblet, such as service finders or model loaders. Open the URLConnection with setUseCaches(false) instead, as java.util.ServiceLoader does:

URLConnection connection = url.openConnection();
connection.setUseCaches(false);
try (InputStream in = connection.getInputStream()) {
  ...
}

This applies even to wurblets that run sequentially, because realms are disposed outside the lock that serializes them.

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 (with Ant, unless failonerror is false). 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 logged as a warning and not counted as an error. The existing guarded block is left unchanged. 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.