Skip to content

Tentackle Wurblets — Interface-Level Code Generation

Overview and Motivation

tentackle-wurblets is the build-time code generation module that produces the interface side of a Tentackle application: the constants, accessor declarations, relation methods, unique-domain-key types, model documentation, and Data Transfer Objects that belong to a PDO interface or operation interface. Two of its wurblets, ColumnNames and DomainMethods, generate code for the implementation classes instead, because the code depends only on the model, not on SQL generation (see Where the wurblets go). Together with its sibling tentackle-persistence-wurblets — which generates the matching implementation side — it turns the application model into compilable Java.

The generators are wurblets: templates written for the Wurbelizer engine. A wurblet is a .wrbl file mixing literal output with embedded Java; at build time the wurbelizer-maven-plugin runs each wurblet (the compiled templates; "wurbiled" in Wurbelizer lingo), weaving the generated text into guarded regions of your handwritten source files. The author keeps full control of the surrounding class; the wurblet owns only the fenced block between its anchors, which it regenerates on every build.

A wurblet is invoked from an anchor comment inside the source file. The generated text is written into the guarded region that immediately follows it (delimited by NetBeans-style //GEN-BEGIN:<tag> … //GEN-END:<tag> fold markers) and re-derived on every build:

public interface CustomerPersistence extends PersistentDomainObject<Customer> {

  // @wurblet membernames AttributeNames
  //<editor-fold ... desc="code 'membernames' generated by wurblet AttributeNames">//GEN-BEGIN:membernames
  // ... generated AN_/RN_ constants appear here ...
  //</editor-fold>//GEN-END:membernames

  // @wurblet methods Methods
  //<editor-fold ... desc="code 'methods' generated by wurblet Methods">//GEN-BEGIN:methods
  // ... generated getter/setter declarations ...
  //</editor-fold>//GEN-END:methods
}

The first token after @wurblet is the tag (used to name the generated element and as the guard name); the second is the wurblet name (the template to run); the rest are options and arguments. The author writes only the anchor line — the fold markers and their contents are managed by the wurbelizer. A tag-less wurblet such as Inject instead uses the pseudo-tags < or > to fill an inline /*@*/…/*@*/ region, e.g. @TableName(/*@*/"customer"/*@*/) // @wurblet < Inject --string $tablename. The wurbelizer also accepts the older /**/…/**/ markers, but since Java 23 javac warns about them as dangling doc comments.

Arguments starting with $ are wurbelizer variables, such as $tablename or $classid. A PDO interface defines them as name = value lines in its leading comment block, next to the here-document holding the entity's model; others, such as $model, come from the wurbelizer configuration. The anchor itself may carry wurbelizer parameters in parentheses, e.g. // @wurblet(fold=expanded) modelComment ModelComment, which keeps the generated fold expanded in the IDE. See the Wurbelizer source syntax for the full anchor syntax.

Why generate the interfaces?

Tentackle's PDO pattern emulates multiple inheritance by splitting every entity into cooperating domain and persistence interfaces and their injected implementations. That gives a clean separation of concerns but produces a large amount of strictly mechanical, model-derived boilerplate: column-name constants, typed getters and setters, relation navigation methods, the unique-domain-key record, and a documentation comment describing how the entity relates to the rest of the model. Writing this by hand would be tedious and error-prone, and it would drift out of sync with the model. Wurblets keep it correct by construction and regenerate it on every build, while leaving the developer free to add custom code outside the guarded regions.

Design principles

  • Single source of truth. Everything a wurblet emits is derived from the model (and the data-type / backend information from tentackle-sql), so generated and handwritten code can never disagree about column names, sizes, or types.
  • Woven, not owned. Generated code lives inside the developer's own files within guarded regions, so a class can freely mix generated and bespoke members. The build re-derives only the fenced blocks.
  • Interface vs. implementation split. This module generates declarations that go into PDO/operation interfaces (and DTOs); tentackle-persistence-wurblets generates the executable persistence implementation. The split mirrors the runtime separation of the domain/persistence layers.
  • Backend-aware validation. Wurblets know which database backends the project targets and can refuse to generate code that uses a feature an active backend does not support.

How a Wurblet Is Built and Run

The module is itself produced by the Wurbelizer: the wurbelizer-maven-plugin's wurbile goal compiles every .wrbl under src/main/wurblets into a Java wurblet class, which is then compiled normally and shipped in the jar. At application build time, the same plugin (configured in the application's POM, usually via the Tentackle Maven plugin) loads these wurblets and runs them against the project sources.

Most wurblets share a small preamble pulled in from header.incl, which imports the model and SQL APIs and declares @{extends ModelWurblet} — making ModelWurblet the base class for the generated wurblet. The DTO and Include wurblets extend their own bases (DTOWurblet, IncludeWurblet) instead.

The module is packaged with an Automatic-Module-Name of org.tentackle.wurblet rather than an explicit module-info.java, because it is a build-time tool loaded on the wurbelizer plugin's classpath, not a runtime module of the application.

Phases

The wurbelizer processes a source file in phases. Phase 0 extracts the here-documents, the wurblets run from phase 1 on. Some wurblets declare @{phase 2} (e.g. ModelComment and UniqueDomainKey). Wurblets run in phase order, so phase-2 wurblets see the results of phase-1 generation. This matters when a generated comment or nested type needs constants or members emitted earlier in the same file.

Parallel builds

The wurbelizer-maven-plugin runs only wurblets that declare themselves thread-safe concurrently (Wurblet.isThreadSafe()). All other wurblets run one at a time within the JVM. The Tentackle wurblets keep their state in singletons, most notably the model, which is not thread-safe. They are safe to run concurrently anyway, as long as each wurbel run loads them in its own classloader:

  • If the wurblets are declared via <wurbletDependencies> (as in the archetype and all Tentackle modules), the plugin creates a separate class realm for each execution. Each execution therefore gets its own model, caches and factories. Because an execution processes its sources sequentially, no two threads ever share them. The wurblets run concurrently in mvn -T builds.
  • If the wurblets are loaded by the plugin's own classloader instead (for example, declared as plugin <dependencies>), all executions of the build share the singletons. The wurblets then fall back to sequential execution.

The check is implemented by WurbletThreadSafety.isIsolated(Class), which the base classes ModelWurblet, DTOWurblet and IncludeWurblet use in isThreadSafe(). See the Wurblet API for custom wurblets.


The Wurblets

All templates live under src/main/wurblets/org/tentackle/wurblet. Each carries its own usage documentation in its leading @{comment ...}@ block.

Wurblet Generates Notable options
AttributeNames AN_* attribute-name and RN_* relation-name constants for the entity. --noif (add public static final when not inside an interface)
ColumnNames CN_* database column-name constants, handling multi-column and backend-specific data types and single-table inheritance.
ColumnLengths CL_* constants giving the maximum character length of sized String columns. --noif
Methods Getter/setter declarations for every (non-muted, non-hidden) attribute, plus the selectByUniqueDomainKey declaration. --noudk (omit the UDK select method)
Relations Relation navigation methods (getters/setters, n:m link methods) according to each relation's type, scope and read/write rules.
DomainMethods The unique-domain-key methods of the domain implementation (isUniqueDomainKeyProvided, getUniqueDomainKeyType, findByUniqueDomainKey, get/setUniqueDomainKey). Nothing if the entity has no UDK or inherits it unchanged.
UniqueDomainKey The <Entity>UDK record (Serializable, Comparable) for root entities whose unique domain key spans several attributes. A single-attribute key needs no type and gets only an explanatory comment. (phase 2)
ModelComment A Javadoc comment documenting the entity's referencing, composite and inheritance relationships. (phase 2) For a navigable, hyperlinked rendering of the same relationships, see tentackle-sql:document.
DTO A complete Data Transfer Object from a property model. see DTO below
Include Inlines another file into the source. --comment, --missingok, --translate (expand $variables), --delete
Inject Injects literal text from the argument list — a tag-less wurblet that fills the gap between two /*@*/…/*@*/ comment markers. --string (wrap in double quotes)

Where the wurblets go

The PDO wizard and the archetype place the anchors for you. A PDO named Message gets this layout (the persistence-wurblets anchors are shown for completeness):

Source file Anchors
Message.java — the PDO interface Inject for @TableName and @ClassId, ModelComment, UniqueDomainKey
MessagePersistence.java — persistence API AttributeNames, ColumnLengths, Methods, Relations
MessageDomainImpl.java — domain impl. DomainMethods
MessagePersistenceImpl.java — persist. impl. ColumnNames, plus ClassVariables, Declare, MethodsImpl, PdoRelations, PdoCache, … from tentackle-persistence-wurblets

The column names live in the persistence implementation because only the SQL layer needs them. All other code refers to attributes and relations by the AN_*/RN_* constants of the persistence interface.

Low-level persistent objects that are not PDOs (for example DbModification in tentackle-database) use the same wurblets with an explicit model, e.g. // @wurblet fieldnames ColumnNames --model=$mapfile.

The DTO wurblet

A dedicated deep-dive — the two input modes, the @RecordDTO build-time analysis, and a mode-by-mode table of what is generated — lives in The DTO Wurblet and @RecordDTO. This section is the quick reference.

DTO is the most feature-rich generator and the only one not tied to a persistent entity. It produces a Data Transfer Object from a small property model, usually written as a here-document (a "heap file" named .<filename>) inside the leading comment block of the DTO source file, or inferred directly from a Java record annotated with @RecordDTO. Each model line is <type> <name> <comment>, with single-character prefixes selecting per-property behavior:

Prefix Meaning
^ property is passed up to the super entity's constructor (non-builder mode)
= mutable property — a setter is generated
~ mutable and transient
! builder mode: required property — build() throws if it was never set (implies immutable)
+ generate a from<Name>(...) method returning a modified copy (a "frommer"; "wither" with --with)

Comments may carry per-property annotations or an access scope in square brackets ([@Bindable(...)] [protected]), and a leading [...] line defines global annotations applied to all properties (removable per property with -@Anno). Major options include --builder/--builderScope/--builderPrefix (builder pattern), --equals, --hashCode, --validate (requires Validateable; integrates with validation), --names (name constants), --from/--with (copy factories), --canonical (emit @Canonical* annotations for TRIP), --nofinal (serialization-friendly non-final fields plus a protected no-args constructor) and --nott (no Tentackle dependency in the generated code).


Key Concepts

The classes summarized in this section are documented class by class — lifecycle, options, helper methods, grammar rules and error handling — in The Wurblet API.

ModelWurblet — the shared base

ModelWurblet extends the Wurbelizer's AbstractJavaWurblet and is the workhorse behind every model-driven wurblet. Its responsibilities:

  • Locate the target. From the source file it figures out whether the anchor sits in a PDO or an operation, an interface or an (abstract) class, by inspecting service annotations (@DomainObjectService, @PersistentObjectService, @DomainOperationService, @PersistentOperationService), interface extends clauses, and class definitions — exposed via getPdoClassName(), isPdo(), isOperation(), isInterface(), isGenerified(), isPartOfInheritanceHierarchy().
  • Load the model. In run() it reads the current module's model directory plus any dependency models (otherModels), applies the shared modelDefaults and entityAliases, registers the target backends for validation, and resolves the Entity the wurblet operates on — turning model errors into wurbel termination so they don't clutter the generated sources.
  • Parse options. Arguments beginning with -- become options (getOption, getOptionArgs); the rest are positional getWurbletArgs(). Common options understood by all model wurblets are --method, --model, --remote and --noremote.
  • Provide template helpers. A large toolkit used inside the .wrbl Java blocks: column-name/constant resolution (getColumnName, getColumnNameConstant, getEffectiveDataType), relation method-name builders, accessor-path code generation (createAccessorCode), doc-comment helpers, id/serial checks, and assertSupportedByBackends(...) to fail the build when a chosen feature isn't supported by every active backend.

Wurblet arguments — the query mini-language

Several generators (above all the persistence-side select/update/delete wurblets, but the grammar is defined here) take a compact expression describing a WHERE clause, sorting, attribute updates, and eager-loading joins. The parsing lives in WurbletArgumentParser and WurbletArgument. An expression is a tree of operands joined by AND (the default), OR and NOT, with parentheses for grouping, followed by optional sorting and load-join terms:

@wurblet selectUpTo PdoSelectList --remote   processed:=:null or processed:>= +id *address

Each WurbletArgument has one of four types:

  • property condition — an attribute (optionally reached through a relation path, optionally a single column of a multi-column type) constrained by a relational operator and value; a ?<boolean> suffix makes the condition optional;
  • property update — an attribute to be assigned;
  • property sorting — + ascending / - descending;
  • load join — * marks a relation to eagerly load; join paths can themselves be filtered.

The expression model is supported by WurbletArgumentExpression, WurbletArgumentOperand, WurbletArgumentOperator and WurbletRelation, while Join, JoinPath and JoinPathFactory model the eager-loading joins (including joins reaching into aggregate components, described by ComponentInfo).

Supporting classes

Class Role
DTOWurblet Base class for the DTO wurblet — parses the property model and options.
IncludeWurblet Base class for the Include wurblet (overrides run()).
TentackleWurbletsModel @Service(Model.class) replacement that understands heap files and defers model-loading exceptions to the entity they relate to.
ModelCommentSupport Helper logic for the ModelComment wurblet.
AnnotationOption Parses the annotation modifiers carried by model attributes.
CodeGenerator Functional interface for emitting nested structures.

For the complete class reference — including Join/JoinPath/JoinPathFactory, the argument grammar and the error-handling conventions — see The Wurblet API.


How It Fits Together

   model (*.map / entity definitions)        tentackle-sql (DataType, Backend)
                 │                                       │
                 └───────────────┬───────────────────────┘
                                 ▼
                tentackle-wurblets  ──►  PDO / operation *interfaces*,
                (this module)            unique-domain-key types, DTOs,
                                         AN_/RN_/CN_/CL_ constants, model docs
                                 │
   tentackle-persistence-wurblets ──►  PDO / operation *implementations*
                                         (selects, updates, deletes, caching, remoting)

During an application build the Tentackle Maven plugin arranges for the wurbelizer plugin to run both wurblet sets over the sources. Because both read the same model and the same DataType/Backend definitions, the generated interfaces, implementations, and the DDL produced by the SQL Maven plugin all agree on the schema.


Module Dependencies

  • Wurbelizer (org.wurbelizer:wurbelizer) — the template engine providing AbstractJavaWurblet, AbstractWurblet, the Wurbler runtime and the weaving of guarded regions.
  • tentackle-model — the model API (Entity, Attribute, Relation, ModelDefaults, …) every model wurblet reads.
  • tentackle-build-support — shared build-time utilities.
  • Transitively, tentackle-sql for DataType and Backend, used to resolve column types, sizes, and backend capabilities.

This is a build-time only module — it is on the wurbelizer plugin's classpath and is never shipped inside the running application.