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.
org.wurbelizer.wurblet.AbstractWurblet: base class for all kinds of wurblets. This is the default parent class if no@{extends}@directive is given.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()andisPrivate().
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.
setContainer(Wurbler): binds the wurblet to its container. The properties are available from now on. The container then loads the fixed text from the.serfile and sets the arguments, so both are available whenrun()is invoked.run(): generates the code. The class generated by the wurbiler implementsrun()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 streamout.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 parameteranalyzeDiror the Ant attributeanalyzedir. 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)andgetProperties(String nameSpace): access to the properties described below. Anullor 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 variablesWurbler.PROPSPACE_WURBLET("wurblet"): properties defined by the wurblerWurbler.PROPSPACE_EXTRA("extra"): extra namespace usually provided by the build tool (maven, ant, ...). Themaven-wurbelizer-pluginuses the wurbletProperties. Theorg.wurbelizer.ant.wurbel.AntWurblerwill 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 thrownWurbelExceptionis aFileNotFoundException. Reading a discarded heap file throws aWurbelDiscardException.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 thatheapNameis given without the leading dot.getStream()returns thePrintStreamto select with@{to}@,append(String)appends text, anddiscard()marks the file as incomplete (see Errorhandling). Since the wurbler repeats the passes over a source file until the file no longer changes, aHeapStreamremembers 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 viaget(name),create(name, text),createOrOverwrite(sourceName, name, text)anddelete(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()returnsfalseinAbstractWurblet. 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.wrblsource:
- Static fields and singletons (models, caches, factories) are shared only among the classes
of the same classloader. The Maven plugin loads the
wurbletDependenciesin 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, unlessfailonerrorisfalse). The generated output is replaced by the stacktrace enclosed in a comment block.org.wurbelizer.wurbel.WurbelDiscardException: same asWurbelExceptionbut 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:
Examples¶
A lot of real-world examples can be found in the Tentackle framework:
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
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.