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. 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.

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

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.


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 Domain-side unique-domain-key methods (isUniqueDomainKeyProvided, getUniqueDomainKeyType, findByUniqueDomainKey). --noif
UniqueDomainKey The unique-domain-key type (a nested record) for root entities that define one. (phase 2)
ModelComment A Javadoc comment documenting the entity's referencing, composite and inheritance relationships. (phase 2)
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 empty /**/.../**/ comment markers. --string (wrap in double quotes)

The DTO wurblet

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. 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) and --nott (no Tentackle dependency in the generated code).


Key Concepts

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;
  • 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.

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.