Source Level Syntax¶
Java comments, whether single line or block comments, are parsed by the wurbler for wurblet anchors, variables and here-documents. This applies to generated comments as well! Consequently, generated code may include new anchors, variables or documents that in turn are evaluated by the wurbler and so on. This is an important feature because it allows bootstrapping from a rudimentary template, similar to a software developer starting with a sketch (the finished result only makes full sense once you know how it came to be). The special significance of a character can be turned off by preceding it with a backslash. A line terminated by a backslash is concatenated with the following line (continuation line). The backslash itself is expressed by a double backslash.
Wurblet Anchor¶
All generated code, whether a single line or a large block, must be bound to a wurblet and optional variables or resources such as model information. The binding is done by a so-called wurblet anchor. There may be as many anchors in a comment block as desired. The wurblets are applied by the wurbler in the order of their anchors in the source file. The generated output is inserted into the source file right after the comment. Each output block is embraced by special comment lines that uniquely identify the block and allow the wurbler to associate it to the anchor in subsequent wurbelization passes. Once the code is generated, the anchor may be moved in the source file anywhere without losing the binding. Thus, the source file may be edited and anchors or the model information changed between wurbelization passes without getting out of sync.
An anchor is of the form:
where:
@wurblet: starts an anchor(wurbler-directive1 ... N): optional directives to the wurbler<tag>: is a tag uniquely identifying the anchor within the source file (tags of unnamed anchors need not be unique)-
<wurblet>: the class name of the wurblet. This is either a fully qualified class name or a basename to which the wurbler prepends the package names provided by the wurbletPaths configuration. -
<arg1>...: optional wurblet arguments. Usually these arguments carry model information, or point to where such information can be found.
Anchors that span more than one line must be concatenated by a trailing
backslash at the end of the lines to be continued. Arguments are separated
by whitespaces. If an argument contains whitespaces, it must be enclosed
in double quotes. Such arguments may span more than one line as well.
Variables within double quotes are substituted. Arguments may also be enclosed in
single quotes, which suppresses variable substitution and allows double quotes
inside, e.g. --orderby='" ORDER BY " + CN_ID + " DESC"'. A quote of the same kind
within a quoted argument, and the backslash itself, are escaped with a backslash.
Variables are substituted after the arguments have been separated, so a variable
value may contain whitespace or quotes without further escaping.
Arguments may contain $-signs denoting a variable. Variables may either
be declared in a variable section, can be an ant property, a property from
a wurb-file or an environment variable. The variable substitution process is
recursive, i.e. variables may contain strings referring to other variables and
so on.
Examples:
/**
* Principals are cached preloaded with a second index ’code’.
*
* @wurblet cache PdoCache --preload code
*/
// @wurblet updateByObjectTypeSerial DbUpdateBy --model=$mapfile \
// processed:=:null objectClassId objectId modType serial:<= | processed
Anchor and Guarded Block¶
After the first wurbel run, the generated code sits below the anchor, enclosed in the
guarded block. The following is a verbatim excerpt of
NumberPoolPersistence.java from the Tentackle framework, using the default
netbeans guard type:
public interface NumberPoolPersistence extends PersistentObject<NumberPool> {
// @wurblet fieldlengths ColumnLengths
//<editor-fold defaultstate="collapsed" desc="code 'fieldlengths' generated by wurblet ColumnLengths">//GEN-BEGIN:fieldlengths
/** maximum number of characters for 'name'. */
int CL_NAME = 30;
/** maximum number of characters for 'realm'. */
int CL_REALM = 80;
//</editor-fold>//GEN-END:fieldlengths
// @wurblet membernames AttributeNames
//<editor-fold defaultstate="collapsed" desc="code 'membernames' generated by wurblet AttributeNames">//GEN-BEGIN:membernames
/** relation name for 'component list of NumberRange numberRangeList via NumberRange#numberPoolId'. */
String RN_NUMBERRANGELIST = "numberRangeList";
/** attribute name for database column 'name'. */
String AN_NAME = "name";
...
Two things are worth noting. First, the tags fieldlengths and membernames are freely
chosen by the developer and appear in the guard comments, which is what binds the block
to its anchor across runs. Second, several anchors happily coexist in one file, each
generating one aspect of it — the whole file is a mixture of generated and
hand-written code, and it stays editable.
An entity in Tentackle typically carries a handful of such anchors, for example:
// @wurblet classVariables ClassVariables
// @wurblet fieldnames ColumnNames
// @wurblet declare Declare
// @wurblet methods MethodsImpl
// @wurblet selectByUniqueDomainKey PdoSelectUnique name
// @wurblet relations PdoRelations
// @wurblet selectByNameRealm PdoSelectUnique name realm
The two PdoSelectUnique anchors show why the tag exists: the same wurblet is applied
twice in the same file with different arguments, producing two different finder methods.
Lifecycle of a Guarded Block¶
The guarded block belongs to its anchor, and the wurbler keeps both in sync:
- The tag must be unique within the source file. A second anchor with the same tag is reported as an error.
- If an anchor is removed from the source, its guarded block is removed on the next run as well. Anchors that are skipped, for example because they belong to another phase or are excluded by the build tool, keep their blocks.
- If the wurblet fails, the guarded block receives the stack trace, enclosed in a
/******** ... ********/comment, so the error is visible right where the code is missing. An inline wurblet (see below) inserts{ <message> }instead. The wurbler stops processing a source file after the first pass with errors. - If the wurblet throws a
WurbelDiscardException(see Errorhandling), the guarded block is left unchanged.
Unnamed Anchor¶
If the <tag> in the wurblet anchor starts with one of the characters -, <, + or >,
it is considered as an unnamed wurblet anchor. Unnamed anchors are used for
so-called inline wurblets, which inject a small piece
of code into the code block directly preceding or following the anchor. The
code is injected between two consecutive empty block comments /**/. The
first character of the tag determines whether the preceding or following code
block is targeted by the wurblet:
-or<refer to the preceding block+or>refer to the following block
Any other tag, even one that doesn't start with a letter, denotes a regular, named anchor with a
guarded block. Unlike named anchors, the tags of unnamed anchors need not be unique, so the
same tag, such as <, can be used for all inline wurblets of a file.
Instead of /**/, the empty block comment may also be written as /*@*/. Both forms
are equivalent and are treated as code rather than as a comment. /*@*/ is the better
choice in Java sources: since Java 23 the compiler warns about a documentation comment
that is not attached to any declaration, and /**/ in front of a declaration can trip
that warning.
Example:
The wurbletInject (which is a
Tentackle wurblet, btw.)
inserts the string "modlog" into the @TableName annotation. The value is defined by
the variable $tablename.
Tentackle uses this pattern wherever a value from the model has to appear inside an
annotation, where a guarded block would not fit. The class ModificationLog binds both
its table name and its class ID this way:
@ClassId(/*@*/2/*@*/) // @wurblet < Inject $classid
@TableName(/*@*/"modlog"/*@*/) // @wurblet < Inject --string $tablename
@PreloadRemoteDelegate
public class ModificationLog extends AbstractDbObject<ModificationLog> {
Several injections may even be applied to a single annotation, one per line, each anchored to the code preceding it:
@TableName(value =/*@*/"md.orgunit"/*@*/, // @wurblet < Inject --string $tablename
mapSchema =/*@*/false/*@*/, // @wurblet < Inject $mapSchema
prefix =/*@*/""/*@*/) // @wurblet < Inject --string $tablePrefix
Note the difference between the two forms: --string wraps the value in double quotes,
so $tablename becomes "md.orgunit", while $mapSchema is injected verbatim as the
boolean literal false.
Unlike guarded blocks, inline sections are not guarded — the wurbler simply replaces whatever it finds between the two empty block comments, so manual edits there are silently overwritten by the next run.
Conditional Anchor¶
Wurblets may be invoked conditionally.
This is achieved by the test-directive.
Example:
Will invoke the wurblet Didoedeldu if the variable $planb equals the string yes.
Otherwise, the wurblet is skipped: an existing guarded block is left unchanged, and no
block is created if there is none yet.
Currently, only the operators == and != are supported.
Notice the following:
- Wurbler directives are separated by whitespace, so the condition must not contain blanks,
unless it is enclosed in double quotes, e.g.
@wurblet("test:$planb == yes"). - The operands are compared literally after variable substitution. A variable that can't be
resolved is compared as is, e.g.
$planb. - The wurblet is loaded even if the condition is not met, so it must exist.
Extra Indentation¶
The generated code may be indented by extra spaces with the indent wurbler directive. 0 means no extra indent, a negative value is interpreted as smart indent, which is the default for Java sources. Smart indent aligns the generated code with the comment block containing the wurblet anchor.
Example:
Editor Fold¶
An optional editor fold type for the generated code may be specified. Not all guard types support a fold type. Currently, editor folds are only supported by the Netbeans-IDE. By default, no editor fold will be generated. The following fold types are supported:
collapsed: the generated code will be collapsedexpanded: the generated code will be expandednone: the generated code will get no fold-handle (default)
Example:
This example is taken from Tentackle, where it is applied consistently to the
ModelComment wurblet. The reasoning is instructive: the project sets the fold type to
collapsed globally (via the foldtype wurblet property, see
Maven Integration), because generated
implementation code is noise most of the time. The model comment, however, documents how
the entity relates to the rest of the model, which is exactly what a developer opening
the file wants to read — so that one anchor overrides the default and stays expanded.
Notice that some IDEs automatically fold a guarded block (also known as custom code folding region) by default. IntelliJ does so, for example.
The default fold type for all anchors is set with the wurblet property foldtype
(collapsed, expanded or none), see Guard Types.
Execution Phase¶
The phase directive overrides the phase set by the
wurblet's @{phase}@ directive for a single anchor. Values less than 1 are treated as 1.
Example:
This is useful if a wurblet must see what other wurblets produced, but only in some of the source files it is applied to. See Processing Order and Phases.
Several wurbler directives can be combined, separated by whitespace:
Here Documents¶
Here-Documents are an elegant way to provide model information to the wurblets
or to create files in general. Whenever the wurbler detects a document
section within a comment block, it creates a file either on heap or in the
filesystem depending on the file’s name. Document sections start with @>
and terminate with @<.
Example (from the Wurbelizer tutorial):
/*
* @> .$filename
* String name the name
* long id the unique ID
* boolean enabled whether enabled or not
* @<
*/
Notice that the filename may be a variable again. If the filename starts with a dot, it will not be saved to the filesystem but instead kept in memory only. Furthermore, such heap files must be unique within the wurbler instance. If the wurbler is invoked from within maven (which usually is the case), the file is available during the whole wurbel lifecycle phase, to all wurblets in all files. This feature allows running wurblets in distinct phases, each phase creating some information for the next phase.
Here-Document Details¶
- The text of each line is taken from the column of the
@>onward, so the comment lead (for example*) is stripped as long as the lines are aligned with@>. \@within the document is unescaped to@.- By default, the wurbler prepends an origin line to the document text, consisting of
#@, the source file name and the line number of the here-document, for example#@ModificationLog.java:12. Wurblets reading the document should therefore ignore lines starting with#. The option--nosourceafter the filename suppresses the origin line: - The origin line lets the wurbler detect that the same file on disk is written by here-documents in two different source files, which is reported as an error. The same applies to heap files.
- Files on disk are only written if their contents change. If they do, the build tool starts the current phase over, so that all wurblets see the updated files (see Processing Order and Phases). Heap files are cleared at each restart.
Example: the model next to the code¶
In Tentackle, here-documents carry the entity model. The class ModificationLog
declares its whole persistent model in a comment at the top of the very file that
implements it:
/*
* @> $mapfile
*
* # modification table for async coupling
* name := $classname
* id := $classid
* table := $tablename
*
* [remote]
*
* ## attributes
* long objectId objectid object id
* int objectClassId classid object class id
* String(192) objectClassName classname object classname (if classid == 0)
* long txId txid transaction id (optional)
* String(64) txName txname transaction name (optional) [TRIMWRITE]
* ModificationType<char> modType modtype modification type
* Timestamp when modtime time of event
* long userId userid user id
* String message message optional informational or error message [NOMETHOD]
* Timestamp processed processed processing time [MAPNULL]
*
* ## indexes
* index next := processed, id
* index txid := txid
* index object := objectid, classid, processed
* index user := userid, processed
*
* @<
*/
The name $mapfile does not start with a dot, so the document is written to disk as a
real .map file. That is deliberate: other tools in the build (the schema generator, for
instance) read the same model files, and the wurblets of other source files reference
them by name. The variables come from a .wurb file next to the source (see
Variables), and $classname is supplied by the wurbler itself.
The payoff is that the model and the code it generates cannot drift apart — they are the same file, reviewed in the same diff.
Cross-file generation with heap files¶
Heap files come into their own when generated code has to be distributed over several source files. Tentackle's remote delegates are the canonical case.
When a persistence method is generated for an entity that is reachable remotely, three
pieces of code are needed in three different files: the local implementation, a
signature in the TRIP delegate interface, and a forwarding method in the delegate
implementation. The wurblet writes the latter two into heap files using the
@{to}@ directive:
RemoteIncludes genInc = new RemoteIncludes(this);
PrintStream implOut = genInc.getImplementationStream();
PrintStream remoteOut = genInc.getInterfaceStream();
RemoteIncludes creates these streams as heap files named after the delegate classes:
interfaceStream = new HeapStream(container, remoteInterface + "/methods");
implementationStream = new HeapStream(container, remoteImplementation + "/methods");
The delegate source files then pick their contents up with a single anchor:
public interface NumberPoolRemoteDelegate
extends AbstractPersistentObjectRemoteDelegate<NumberPool,NumberPoolPersistenceImpl> {
// @wurblet inclrmi Include --missingok .$classname/methods
//<editor-fold defaultstate="collapsed" desc="code 'inclrmi' generated by wurblet Include">//GEN-BEGIN:inclrmi
NumberPool selectByUniqueDomainKey(DomainContext context, String name);
NumberPool selectByNameRealm(DomainContext context, String name, String realm);
//</editor-fold>//GEN-END:inclrmi
}
Every detail of that anchor does some work:
- the leading dot in
.$classname/methodsmarks the reference as a heap file, so nothing is read from disk. (The heap file itself is registered without the dot; the dot is the convention on the reading side.) $classnameresolves to the name of the class being wurbeled,NumberPoolRemoteDelegate, which is precisely the name the producing wurblet used. So no explicit wiring between producer and consumer is needed — the naming convention is the contract.--missingoktolerates an absent heap file, which is what happens when the entity has no remote methods at all.- the two methods in the block were contributed by two different anchors in a
different source file,
NumberPoolPersistenceImpl.java.
For this to work, the producing files must be wurbeled before the consuming ones. Tentackle enforces the order with two filesets in the plugin configuration rather than with wurblet phases, because the ordering is between files, not within one (see Maven Integration).
Variables¶
Maven (or Ant) properties and environment variables are automatically declared
as variables by the wurbler. Additional variables may be declared either in extra
property-files (extension .wurb) or directly at the source level within any
comment block. Such a declaration section starts with @{ and ends with @}.
Example (from the Tentackle tutorial):
Variables can be used in comments or other variable definitions in two ways:
$<variable>: e.g.$tablename.mapwhere the dot automatically ends the variable name${<variable>}: e.g.${tablename}xxto add the stringxxto the tablename's value
Notice the recursion in the example above: mapping is defined in terms of $model and
$tablename, where $model is not declared in the source at all but comes from the
build tool — Tentackle sets it as a wurblet property in the POM.
Resolution Order¶
Variables are substituted recursively until no more variables are found. A variable
name ends at whitespace or at one of the characters / , ; : ( ) [ ] = ! ' ".
Substitution runs in two rounds:
- Java system properties, the extra properties provided by the build tool (see
Wurbler Properties) and the variables of
the source file, in this order. In this round, a dot is part of the variable name,
so that
$user.homerefers to the system propertyuser.home. - Environment variables and the variables of the source file. Now a dot ends the
variable name, so
$tablename.maprefers to$tablename.
Variables that can't be resolved are left as is. A $ preceded by a backslash
(\$) is not substituted, and the backslash is removed.
Built-in Variables¶
The wurbler provides the following variables for each source file:
| Variable | Value |
|---|---|
filename |
the pathname of the source file |
dirname |
the directory of the source file |
wurbname |
the pathname of the .wurb file, if any |
The wurblets additionally see the tag and the wurblet name of the current anchor as
guardname and wurbletname (see Wurbler Properties).
For Java sources, the wurbler additionally extracts the following variables from the code:
| Variable | Value |
|---|---|
packagename |
the package name |
classname |
the name of the class, interface or record (without package) |
interfacename |
the name of the interface, if the source is an interface |
recordname |
the name of the record, if the source is a record |
definition |
the rest of the type declaration following the name, up to the { |
extends_ |
the superclass of a class |
extends_0, extends_1, … |
the extended interfaces of an interface |
implements_0, … |
the implemented interfaces |
annotation_0, … |
the annotations of the type (single-line annotations only) |
modifiers_0, … |
the modifiers public, protected, private, abstract, final |
Only the first top-level type in the file is considered. For other kinds of source
files, no variables are extracted, see Source Types.
Wurblets extending AbstractJavaWurblet get the same information via methods such as
getClassName(), see Writing Wurblets.
Example: variables in a .wurb file¶
The alternative to an inline @{ @} block is a property file with the extension .wurb,
placed next to the source file. ModificationLog.java is accompanied by
ModificationLog.wurb with just three lines:
These three variables then feed everything else in the file: $mapfile names the
here-document holding the model, $tablename is injected into the @TableName
annotation and $classid into @ClassId. Changing the table name is therefore a
one-line edit that propagates through the annotations, the model file and every
generated SQL statement on the next build.
In a .wurb file, everything following a # is a comment. Besides variables, the
.wurb file may also set the guardtype for this particular source file, overriding
the global setting (see Guard Types).
Which of the two forms to prefer is mostly a matter of taste. An inline block keeps
everything in one file; a .wurb file keeps the source free of build metadata and is
easier to generate from a wizard or archetype.
Processing Order and Phases¶
Wurbeling is organized in phases, which span all source files of a build-tool run (i.e. an execution of the Maven goal or the Ant task):
- Phase 0 processes only the here-documents and variables of all source files, so that every model file on disk is up to date before any wurblet runs. The Maven plugin always starts with phase 0, the Ant task with phase 1.
- Then, all anchors whose wurblet phase is 1 are executed in
all source files, then those of phase 2, and so on. Only phases that actually occur
are processed. The Maven parameters
firstPhaseandlastPhaserestrict the range. - If a here-document on disk changes during a phase, the phase is started over for all files.
Within a phase, the source files are processed in the following order:
- With Maven, the filesets are processed in the order of their declaration. Within a
fileset, the files are sorted alphabetically, and a file matched by several patterns
(for example both via its
.javaand its.wurbfile) is processed only once. A file matched by more than one fileset, however, is processed again. If a fileset produces errors, the remaining filesets are skipped, because they may depend on it. - Within a source file, the wurbler runs the wurblets in the order of their anchors. If a comment contains several anchors, they are executed from the last to the first, so that the generated blocks appear below the comment in the order of the anchors. This is repeated until the file no longer changes, because generated code may contain new anchors, here-documents or variables.
Thus, if a wurblet depends on the output of another wurblet in a different file, the order of the files matters (see Cross-file generation with heap files). If it depends on another wurblet in the same file, a higher phase is the better choice.
Further reading¶
- Guard Types — the patterns that delimit the generated blocks
- Writing Wurblets — how a wurblet consumes anchors, here-documents and variables