MyApp — A Guided Walkthrough of a Generated Tentackle Application¶
This document walks, end to end, through the example application produced by
tentackle-project-archetype. It is written for a developer who is new to
Tentackle and wants to understand how a real, runnable Tentackle application is laid out, how its layers fit
together, and where to look when adding features of their own.
The companion document tentackle-project-archetype.md explains the archetype
(the Velocity templates and how generation works). This document instead treats the generated project as a
reference application and reads it like production code.
The walkthrough follows the project generated with
-DartifactId=myapp -Dpackage=com.example.myapp -Dapplication=MyApp, i.e., the one the archetype's own integration test materializes undertentackle-project-archetype/target/test-classes/projects/myapp/project/myapp. Throughout, file paths are given relative to that generated project root (e.g.myapp-pdo/src/main/java/com/example/myapp/pdo/md/User.java).
1. What the application is¶
MyApp is a small but complete multi-tier business application. Out of the box it gives you:
- a domain of Users, User Groups, a User↔Group N:M link, an Organizational Unit base entity, and a Message log;
- a JavaFX desktop client with login, CRUD editors, search/finders, preferences, theming, an about box, an auto-updater, and a security-administration UI;
- a middle-tier server speaking TRIP (Tentackle's RMI/JOS replacement) that also hosts the software auto-update service;
- a headless console daemon that listens for new
Messages and logs them; - jlink/jpackage packaging for all three runnables;
- full JPMS module declarations, i18n in English and German, role-based security, and a TestNG test suite running against an in-memory H2 database.
It demonstrates the framework's central idea — the Persistent Domain Object (PDO) — together with the code generation, remoting, UI, and build machinery that surround it.
2. The module map¶
MyApp is a Maven reactor of eight library/application modules plus a jlink sub-aggregator. The root
pom.xml is a pom-packaging aggregator that imports
tentackle-bom and centralises all plugin configuration in
<pluginManagement> so the child modules stay tiny.
myapp (root pom — aggregator + pluginManagement + model defaults)
├── myapp-common cross-cutting types: Constants, Version, SessionInfo, DomainContext, Cryptor, Locale, Preferences
├── myapp-pdo PDO *interfaces* (entity + domain facet + persistence facet) + the embedded model + security + enums
├── myapp-domain domain-logic *implementations* (open module)
├── myapp-persistence persistence *implementations* + TRIP remote delegates (open module)
├── myapp-gui JavaFX RDC controllers, editors, finders, FXML/CSS, images (open module)
├── myapp-client the desktop FX client application
├── myapp-server the middle-tier TRIP server + update service
├── myapp-daemon the headless console client
└── jlink jlink/jpackage images (only built under the "jlink" profile)
├── client
├── server
└── daemon
Layering and JPMS¶
Every module has an explicit module-info.java. The dependency edges encode Tentackle's architecture and, crucially,
keep the persistence and domain layers from knowing about each other — they only share the PDO interfaces.
| Module | key requires |
open? |
Role |
|---|---|---|---|
…common |
transitive org.tentackle.pdo |
no | base types shared by everything |
…pdo |
transitive …common |
no | the PDO interfaces that define the model; exports …pdo, …pdo.md, …pdo.td, the .domain/.persist facet packages, and …pdo.security |
…domain |
transitive …pdo, transitive org.tentackle.domain |
yes | domain-logic implementations (UserDomainImpl, MessageDomainImpl, …) |
…persist |
transitive …pdo, transitive org.tentackle.persistence |
yes | persistence implementations + TRIP remote delegates |
…gui |
transitive …pdo, transitive org.tentackle.fx.rdc, org.tentackle.fx.atlanta |
yes | FX controllers, editors, providers, FXML/CSS |
…client |
transitive …gui, …persist, …domain, transitive org.tentackle.fx.rdc.update |
no | the desktop application |
…server |
transitive …persist, …domain, transitive org.tentackle.update |
no | the middle-tier server |
…daemon |
transitive …persist, …domain |
no | a headless console client |
The domain, persistence, and gui modules are open module because the framework reflects into them: TRIP
serialises PDO state, JavaFX/FXML instantiates controllers, and the proxy-based PDO assembly (below) needs reflective
access to the implementation classes.
Notice that …client, …server and …daemon each pull in both …persist and …domain. A running JVM needs both
implementation layers; only the compile-time code in …pdo is shared, and neither implementation module requires the
other. See Common / Services & Configuration and
the modules overview for the framework-level rationale.
The module-hook SPI¶
Every module declares
service/Hook.java is a minimal class implementing ModuleHook (it forwards getBundle(...) to
ResourceBundle.getBundle). This is the SPI entry point through which the framework's
ServiceFinder discovers and initializes each module —
the mechanism that lets the same code run modular or non-modular without change. When you add a module, copy this
one-class pattern.
3. The heart of it: the PDO pattern¶
Read alongside PDO — Persistent Domain Objects and PDO vs. ORM.
A PDO is a single logical entity that Java cannot express directly because it needs two independent
implementation inheritance chains — one for persistence, one for domain logic. Tentackle emulates that multiple
inheritance with a dynamic proxy that fronts a PersistentObject and a DomainObject working in tandem.
For each entity there are (at least) five Java types, plus generated SQL. Take Message:
| Artifact | File | Hand-written? |
|---|---|---|
| PDO interface | myapp-pdo/.../pdo/td/Message.java |
yes — and carries the model |
| Persistence facet | myapp-pdo/.../pdo/td/persist/MessagePersistence.java |
partly — generated accessors woven in |
| Domain facet | myapp-pdo/.../pdo/td/domain/MessageDomain.java |
yes — extra business methods |
| Persistence impl | myapp-persistence/.../persist/td/MessagePersistenceImpl.java |
yes + generated regions |
| Domain impl | myapp-domain/.../domain/td/MessageDomainImpl.java |
yes |
| Remote delegate (+impl) | myapp-persistence/.../persist/td/trip/MessageRemoteDelegate*.java |
generated |
| SQL/DDL & mapping | under target/wurbel/model |
generated |
The PDO interface is where you, the developer, live. It extends its persistence facet, its domain facet, and a shared base interface:
public interface Message extends TransactionData<Message>, MessagePersistence, MessageDomain {
...
}
MessagePersistence(extendsPersistentObject<Message>) gives you generated typed getters/setters (getMessageNumber(),setText(...), relation accessors) plus hand-declared persistence operations likenextMessageNumber()andfindBy(...).MessageDomain(extendsDomainObject) declares business operations:create(...),getRefersToPdo(),getFormattedRefersTo(),toDiagnosticString().TransactionData<T>/MasterData<T>(inmyapp-pdo) are thin project-wide marker bases extendingPersistentDomainObject<T>. They let you attach behavior or constraints to whole categories of entity. The split mirrors the two wizard profiles (masterdata, class ids ≥ 1000;transactiondata, class ids ≥ 2000).
How an instance is assembled¶
You never call a constructor. You ask the factory:
Pdo.create returns a proxy implementing Message. Calls that belong to the persistence facet are routed to a
MessagePersistenceImpl; calls on the domain facet go to a MessageDomainImpl. Inside an implementation, me()
returns the proxy (so a domain method can call persistence methods and vice-versa) and getDomainContext() yields the
ambient context. You can see both halves cooperating in MessageDomainImpl.create(...):
me().setMessageType(messageType);
…
me().setOrgUnit(owner);
me().setWhen(new Timestamp());
me().setMessageNumber(me().nextMessageNumber()); // persistence facet
return me().persist(); // write through
The implementations are wired to their PDO class by annotations the framework discovers via the service SPI:
@PersistentObjectService(Message.class) on MessagePersistenceImpl and @DomainObjectService(Message.class) on
MessageDomainImpl.
Aggregates are first-class¶
User and UserGroup both extends OrgUnit, which uses multi-table inheritance (inheritance := multi in the
model). User2Group is the N:M link, and User/UserGroup each expose it as a component list (relation =
component list). Because Tentackle models aggregates explicitly, a component always knows its root without a query and
DDD invariants are enforced by the persistence layer. See the User.java model block in §4 and the
persistence docs.
Remoting is transparent¶
Because the persistence layer is remoting-capable, every PDO method works in any JVM — directly connected to the
database (server) or remote over TRIP (client, daemon). There is no LazyInitializationException in Tentackle: a
relation accessor simply executes wherever it must. The …RemoteDelegate classes under each trip package are the
generated plumbing that carries method calls across the wire. This is why all three runnables can share the identical
contract:
@Override public User getUser(DomainContext context, long userId) {
return Pdo.create(User.class, context).selectCached(userId);
}
4. The embedded model — reading the entity comment blocks¶
Reference: Model definition and the persistence wurblets.
The model is authored in special comment blocks inside the PDO interface, and code generation reads it from there.
There are two kinds of block, both visible at the top of OrgUnit.java:
/*
* @{ <-- a "definitions" block: local variables for this file
* tablename = md.orgunit
* mapping = $model/$tablename.map
* @}
*/
/*
* @> $mapping <-- a "wurble into file $mapping" block: the actual model
*
* # Organizational Unit
* name := $classname
* table := $tablename
* inheritance := multi
* integrity := $integrity
*
* ## attributes
* [cached, tokenlock | +name]
* String($ou_name) name name short name [key, uc, normtext]
* String($ou_comment) comment ocomment optional comment [normtext]
*
* ## indexes
* unique index udk := name
*
* ## relations
*
* ## validations
* name: @NotNull(message="{ @('please enter the name') }")
* @<
*/
Things to note for a newcomer:
attributesare declared asJavaType(size) attributeName column_name description [modifiers]. Modifiers in brackets drive generation:key(part of the domain key),uc/normtext(uppercase / normalized search text),cached,tableserial,trimwrite,@NotNull(a bean-validation constraint),[MUTE](excluded from the PDO — see the password below), etc.$ou_name,$ou_comment,$msg_text… are column sizes defined centrally in the rootpom.xml(<wurbletProperties>), so the whole model's field widths are tuned in one place.extends := OrgUnitinUser.java/UserGroup.javaproduces the multi-table inheritance.relationsdeclare aggregation.User's block declares the component listnmLinksoverUser2Groupwith the N:M shortcutnm = UserGroup UserGroups, which is what givesUser.getUserGroups().@ClassIdassigns a stable numeric class id (1001User,1002UserGroup,1003User2Group,2001Message). Class ids are how PDOs are identified across the wire and in theMessage.refersToClassIdpolymorphic reference.
A subtle, instructive detail: User's password attribute is declared [MUTE], so the hash column exists in the
table but is not part of the User PDO. The hash is read/written only through dedicated persistence operations
(selectPasswordHash(), updatePasswordHash(...)), keeping it off the wire — see the login flow in §8.
@TableName(...) and @ClassId(...) carry /*@*/ … /*@*/ markers; the Inject wurblet keeps those literals in sync
with the model variables. The // @wurblet … comments are guarded regions — read on.
What the generator makes of it: the model comment¶
The model block above is your input. To see what the tooling understands from it, look just inside the interface
body, where the modelComment ModelComment wurblet weaves a read-only summary of the entity's place in the model. In
OrgUnit.java it expands to:
public interface OrgUnit<T extends OrgUnit<T>> extends MasterData<T>, OrgUnitPersistence<T>, OrgUnitDomain<T> {
// @wurblet(fold=expanded) modelComment ModelComment
//<editor-fold defaultstate="expanded" desc="code 'modelComment' generated by wurblet ModelComment">//GEN-BEGIN:modelComment
/*
* -----------------------------------------------------------------------------------------------------------------
*
* OrgUnit is referenced by:
*
* Message via orgUnitId as orgUnit [1:1]
*
*
* OrgUnit is a root entity
*
*
* OrgUnit is not referencing other entities
*
*
* Components of OrgUnit are not deeply referenced
*
*
* OrgUnit is extended by:
* ^ User
* ^ UserGroup
*
* -----------------------------------------------------------------------------------------------------------------
*/
//</editor-fold>//GEN-END:modelComment
This block is generated, not authored: the Wurbelizer resolves the whole model — including relations declared in
other files — and reflects each entity's incoming references, root/aggregate status, outgoing references, and
inheritance back into its own source. Here it tells you, at a glance, that OrgUnit is referenced by Message's
orgUnit, is an aggregate root referenced by nothing further, references nothing itself, and is the multi-table base
of User and UserGroup (the ^ arrows). Because it lives right where you read the code, it keeps the model's
cross-file structure visible without chasing it through the other PDO sources — and it is your first concrete sight of a
guarded region, the mechanism the next section is about.
5. Code generation: wurblets and guarded regions¶
Reference: Wurblets, persistence wurblets, pdo-select.
Tentackle does not generate code into separate files you must not touch. Instead it weaves generated code into
guarded regions inside your own source files. A guard looks like a comment marker; the
Wurbelizer replaces the region's body on each build, while leaving your hand-written
code between regions untouched. In MessagePersistenceImpl.java you can see the seams:
// @wurblet classVariables ClassVariables
// @wurblet columnNames ColumnNames
// @wurblet declare Declare
…
// @wurblet methods MethodsImpl
// @wurblet relations PdoRelations
Each // @wurblet <name> <Wurblet> invokes a named generator. So MessagePersistence (the facet interface) gets its
typed getXxx/setXxx and AN_*/CN_* constant names from AttributeNames/Methods/ColumnLengths wurblets, while
MessagePersistenceImpl gets the JDBC column plumbing — and you fill in the rest (nextMessageNumber(), findBy(...))
by hand, using framework helpers like Query, NormText and NumberSourceFactory.
The generators are selected per file-type by the <filesets> in the root pom.xml's
wurbelizer-maven-plugin configuration: entity interfaces, facet interfaces, implementations, and trip delegates each
get their appropriate wurblets. The build order that makes this work is:
- tentackle-maven-plugin
analyze/test-analyze— emit service descriptors and analysis metadata. - wurbelizer-maven-plugin
wurbel/test-wurbel— run the wurblets, weave the guarded regions, and emit the model undertarget/wurbel/model. - tentackle-sql-maven-plugin — turn the model into DDL for the
tdandmdschemas (with migration hints). - tentackle-check-maven-plugin
bundles/validations— verify i18n completeness (en_US, de_DE) and Groovy validation scripts. - tentackle-i18n-maven-plugin — i18n resource handling.
- maven-compiler-plugin — compile with the framework annotation processors
(
tentackle-core,tentackle-pdo,tentackle-fx,tentackle-fx-rdc) on the processor path. - tentackle-jlink-maven-plugin — build native images under the
jlinkprofile.
The same pipeline, with each step's own document, is summarized in
tentackle-project-archetype.md §"The Generated Build Pipeline".
6. The common module — cross-cutting plumbing¶
myapp-common holds the small classes every layer needs. Most are thin subclasses of framework defaults registered
as services (@Service(...)), so the framework picks them up automatically:
| Class | Extends / implements | Why it exists |
|---|---|---|
MyAppSessionInfo |
DefaultSessionInfo |
login credentials + connection params; here it forces usernames to uppercase |
MyAppSessionInfoFactory |
@Service(SessionInfoFactory) |
builds MyAppSessionInfo from username/password/properties |
MyAppDomainContext |
DefaultDomainContext |
the per-call context; narrows getSessionInfo() to MyAppSessionInfo |
MyAppDomainContextFactory |
@Service(DomainContextFactory) |
builds MyAppDomainContext |
MyAppCryptor |
@Service(Cryptor.class) |
app-specific symmetric encryption (salt + passphrase) for stored secrets |
MyAppLocaleProvider |
@Service(LocaleProvider.class) |
restricts supported locales to en (default) and de |
MyAppPreferences |
— | typed access to persisted system/user preferences (e.g. the help URL) |
Version |
— | reads version/date from a filtered Version.properties baked at build time |
Constants |
— | app constants such as the number-pool realm "MyApp" |
The SessionInfo / DomainContext pair is worth internalizing early:
- A
SessionInfodescribes who is connecting and how (user, password, backend properties).MyAppSessionInfouppercases usernames because the model declares theOrgUnit.namekey asuc— the two must agree. - A
DomainContextis the ambient handle threaded through every PDO call: it carries theSession(database or remote connection) and is what you pass toPdo.create(Clazz.class, context). The FX controllers obtain it viaApplication.getInstance().getDomainContext()(seeMainController.getDomainContext()).
MyAppCryptor deliberately obfuscates its salt/passphrase character-by-character and notes in comments that production
deployments should derive the passphrase from an external source. It is the same Cryptor the build uses to encrypt
passwords into the jlink images (§11).
7. Security — users, groups, and permissions¶
Reference: Security.
Security lives in myapp-pdo/.../pdo/security:
MyAppSecurityFactory(@Service(SecurityFactory.class)) plugs inMyAppSecurityManager extends DefaultSecurityManager, which implements the user→group role expansion.
When the framework needs to decide whether a grantee (the logged-in user) may do something,
determineGranteesToCheck(...) returns the user plus every UserGroup they belong to plus the “all” grantee
(0,0). Permissions granted to a group therefore apply to all its members. Two details show real-world care:
- The manager listens with
Pdo.listen(this::invalidate, User.class, UserGroup.class)and rebuilds itself whenever a user or group changes — so permission changes take effect live. evaluateImpl(...)short-circuits read permission onUser/UserGroup/User2Groupto “accepted”, which prevents an infinite recursion: expanding a user's groups must itself read users and groups.
The FX MainController consults this in configure() to enable/disable the Security Manager, Sessions and
Preferences menu items based on the current user's permissions, and the editors gate actions on
pdo.isEditAllowed(), isViewAllowed(), etc.
8. The three runnable applications¶
All three subclass a Tentackle application base class and share the getUser(...) contract from §3. Each main
just constructs the application and calls start(args); the base class drives configuration, login, the session
lifecycle and shutdown hooks.
8.1 Desktop client — MyAppFxClient¶
myapp-client/.../client/MyAppFxClient.java extends UpdatableDesktopApplication<MainController>. It declares:
getApplicationClass()→MyAppLoginApplication(the JavaFXApplicationthat shows the login dialog, with app-specificLogin.css/Login-dark.cssand a 30-second inactivity timeout);getMainControllerClass()→MainController;configureMainStage(...)to set the window title and route the window-close button throughMainController.exit()(which asks for confirmation);configurePreferences()to apply the logged-in user's preference policy (setSystemOnly,setReadOnly) and react to live changes of the help URL.
Being an Updatable… application, it wires in the
auto-updater (org.tentackle.fx.rdc.update), which checks the
server's update service on startup.
MainController (myapp-gui) is the menu-driven shell: it opens CRUD editors via
Rdc.displayCrudStage(on(clazz), …), search dialogs via Rdc.displaySearchStage(...), the preferences dialog, theme
switcher, password change, the security-administration dialog, and a server-sessions view. on(clazz) is the controller
helper that creates a PDO bound to the controller's domain context.
8.2 Middle-tier server — MyAppServer¶
myapp-server/.../server/MyAppServer.java extends ServerApplication. Its constructor names the service
("MyAppServer"), passes the build Version.RELEASE, and registers MyAppRemoteDbConnectionImpl as the connection
class. After the base startup it calls createUpdateService(), which — if updateService/updateURL properties are
set — starts the server-side update service so clients can
auto-update to the running version.
The server is the only tier directly connected to the database; clients and the daemon connect to the server over TRIP. This is the classic three-tier topology, and it is configured purely through properties (§10), not code.
8.3 Console daemon — MyAppDaemon¶
myapp-daemon/.../daemon/MyAppDaemon.java extends ConsoleApplication. It is the smallest end-to-end example of
event-driven Tentackle code:
latestMessageId = on(Message.class).selectMaxId();
Pdo.listen(this::logMessages, Message.class); // fire whenever a Message is committed anywhere
Pdo.listen(...) is the framework's change-notification primitive (the same one the security manager uses). Whenever a
Message is persisted — by any JVM connected to the server — every listener is notified, and the daemon prints the
new messages. It illustrates that PDO change events, like PDO methods, cross the remoting boundary transparently.
9. Remoting and the login lifecycle (TRIP)¶
The trip packages contain the connection/session classes that make remoting concrete:
myapp-pdo/.../pdo/MyAppRemoteSession— a marker interface for the app's remote session type.myapp-persistence/.../persist/trip/MyAppConnection,MyAppRemoteSession,MyAppRemoteSessionAdapter,MyAppRemoteSessionFactory— the client-side remote-session machinery.myapp-server/.../server/trip/MyAppRemoteDbConnectionImplandMyAppRemoteDbSessionImpl— the server-side endpoint.
The two server classes are where the application's authentication actually happens:
MyAppRemoteDbConnectionImpl.checkClientVersion(...)rejects clients whoseVersion.RELEASEdiffers from the server's — version skew is caught at connect time (VersionIncompatibleException).MyAppRemoteDbSessionImpl.verifySessionInfo(...)performs login: it looks up theUserby unique domain key (the uppercase username), comparesuser.hash(password)againstuser.selectPasswordHash()(recall the hash is[MUTE]d out of the PDO and fetched on demand), enforcesisLoginAllowed(), rejects duplicate logins of the same main session (AlreadyLoggedInException), sets the session locale, and logs aLOGINmessage.closeDb(...)mirrors that on logout / crash, writing aLOGOUTmessage (annotated ascrashed …if the session died). This is the producer side of the events the daemon listens for in §8.3.
Notice how login/logout auditing reuses the very same Message.log(...) PDO that the rest of the app uses — the
framework's pieces compose.
10. The GUI / RDC layer¶
Reference: FX RDC and tentackle-fx.
The desktop UI is built with RDC (Rich Desktop Client). For each entity the myapp-gui module follows a small,
repeatable trio of artifacts (look at the user, usergroup and message sub-packages):
- A
…GuiProviderannotated@GuiProviderService(Entity.class)— the framework's per-entity UI service. It says what graphic to show, whether an editor/finder exists, how to build them, how the entity appears in trees, and how drag-and-drop behaves.UserGuiProvider, for instance, accepts droppedUserGroups and adds them to the user. - A
…EditorextendingPdoEditor<Entity>annotated@FxControllerService, paired with an.fxmllayout and.propertiesbundles co-located insrc/main/java(a Tentackle convention so SceneBuilder finds them by relative path, the project's FXML/FX convention). Fields are wired with@FXMLand bound to the PDO with@Bindable+ the controller's binder.UserEditorshows a text field, checkboxes, a childUserGrouptable with a context menu, and a password button. - A
…Finder(e.g.MessageFinder) or the framework'sDefaultPdoFinder, plus aTableConfigurationdescribing the search-result columns (seeMessageGuiProvider.createTableConfiguration(), which uses the generatedAN_*/RN_*attribute-name constants for column ids and bundle keys).
MainController ties it together: menu items call edit(clazz) / search(clazz), which delegate to
Rdc.displayCrudStage(...) / Rdc.displaySearchStage(...); the RDC layer then looks up the right GuiProvider,
builds the editor or finder, and manages the stage. Theming (light/dark via org.tentackle.fx.atlanta), the about box,
preferences dialog, and password-change view round out the shell.
11. Internationalisation¶
Reference: i18n, bundles, internationalization.
Every module that emits user-facing text ships a *Bundle.java accessor and *Bundle.properties /
*Bundle_de.properties pairs (PdoBundle, DomainBundle, PersistenceBundle, ServerBundle, GuiBundle, plus the
per-view .properties). The MessageType enum even localizes itself via PdoBundle.getString(name()).
Completeness is enforced at build time: the tentackle-check-maven-plugin bundles goal checks that the
en_US, de_DE locales are in sync, and the i18n plugin handles the resources. MyAppLocaleProvider narrows the
runtime to those two languages with English as fallback. The classes deliberately avoid stray string constants that
would trip the i18n checker (see the comment in Version.java).
12. Configuration and properties¶
The runnables are configured by .properties files resolved as resources, selected with a --backend=<name>
(client/daemon) or --backend/service argument (server). Two patterns appear:
backend.properties(client/daemon) points the JVM at a data source. For tests the client usesurl=${dbService}(atrip://…server URL), and the daemon uses an H2 in-memory URL. Commented lines show optional auto-login / view-only setups.server.propertiesgives the server its JDBCurl/user/password, the TRIPserviceURL to publish, and atimeout.
The ${…} placeholders are Maven-filtered at build time from properties defined in the root pom.xml
(dbUrl, dbUser, dbPasswd, dbService, updateService, updateURL). Filtered resources live under
src/(main|test)/filtered-resources; verbatim ones under src/(main|test)/resources. This split is configured once in
the root POM's <resources>/<testResources> and inherited everywhere.
Secrets are never stored in clear text in the packaged images: the jlink build encrypts them with MyAppCryptor
(see §11 packaging).
13. Testing¶
Reference: persistence and the maven plugins.
The project demonstrates three distinct testing styles:
- Pure domain unit test, no database —
myapp-domain/.../domain/td/MessageTest.java.Pdo.create(Message.class)with no context gives a PDO backed by a persistence mock, so you can exercise domain logic (toDiagnosticString()) in isolation. The test's comments also show the Mockito alternative. - In-memory integration test —
myapp-server/.../server/MyAppTestBase.javaextendsAbstractPdoTest, which spins up an H2 database, runs the generated DDL, and letspopulateDatabase()seed a number pool. Subclass it to test against a real (in-memory) schema. - Runnable harnesses —
RunServer,RunClientandFillDatabaseare@Test-annotated launchers used during development:RunServerboots the actual server inside the test container (overridingisSystemExitNecessaryToStop()so it doesn't kill the JVM);RunClientlaunches the FX client (also in a German locale variant);FillDatabase(groupinitdb, viainitdb-testng.xml, run withmvn -Pinitdb test) creates the number pool and seed usersGONZO/KERMIT/DAEMONwith hashed passwords.
Tests run with the locale forced to US English (-Duser.language=en -Duser.region=US in the surefire config) so that
locale-sensitive assertions are stable.
FillDatabase.addUsersAndGroups() is also the clearest worked example of the persist API from the outside:
on(User.class), set fields, persist(), then updatePasswordHash(user.hash(...)) — including adding the user to a
group via the aggregate relation user.getUserGroups().add(group).
14. Packaging — jlink / jpackage¶
Reference: jlink/jpackage packaging.
The jlink aggregator builds self-contained runtime images for all three runnables, but only under the jlink
profile (mvn -Pjlink …). Highlights:
- Each leaf (
jlink/client,jlink/server,jlink/daemon) has<packaging>jlink</packaging>— switch a leaf tojpackageto produce a native installer instead. The client declares itsmainModule/mainClass(com.example.myapp.client/MyAppFxClient), bundleswithUpdater, and adds JDK modules likejdk.localedata. - The aggregator's
tentackle-maven-plugin:propertiesgoal encrypts the DB, certificate and daemon passwords with@org.tentackle.common.Cryptor(i.e.MyAppCryptor) into the filtered resources, so no clear-text secrets are shipped. - The
keytool-maven-plugingenerates/imports a TLS certificate; note the secure service URLs in the jlink profile usetrips://andtripe://(TLS-secured TRIP) rather than the plaintrip://used for local testing.
15. Build & run cheat-sheet¶
# full build (analyze → wurbel → sql → check/i18n → compile → test)
mvn clean install
# build the native images too
mvn -Pjlink clean install
# initialise a CI/dev database with the seed data (FillDatabase, group "initdb")
mvn -Pinitdb test
# run a single test / launcher
mvn -Dtest=RunServer test # boot the server
mvn -Dtest=RunClient#runClient test# boot the FX client
mvn -Dtest=MessageTest test # the no-DB domain unit test
A typical first run is: build (mvn install), point server.properties at a real PostgreSQL, generate the schema
DDL (produced under target/wurbel/by the sql plugin), seed it (-Pinitdb), start MyAppServer, then start
MyAppFxClient against, and log in as one of the seed users.
16. How to extend it¶
- Add an entity. The wizard plugin
is pre-configured in the root POM with
masterdata/transactiondata/operationprofiles (package conventions and class-id ranges) and FreeMarker templates undertemplates/. It scaffolds the PDO interface, facets, implementations, and remote delegate consistently with the examples. Then author the model block, runmvn install, and add aGuiProvider/Editorif it needs UI. - Add a business operation that isn't tied to one entity: use the operation profile and the operation pattern.
- Tune the schema: add an attribute
- add an entity: use the PDO wizard
17. Where to read next¶
| Topic in MyApp | Framework document |
|---|---|
The PDO concept, proxies, Pdo.create |
pdo.md · pdo-vs-orm.md |
| The model comment language | model-definition.md · tentackle-model.md |
| Code generation / guarded regions | wurblets.md · persistence-wurblets.md · pdo-select.md |
| Persistence impls, queries, relations | persistence.md · database.md · sql.md |
| Domain impls | domain.md |
| Remoting / TRIP | trip.md · session.md |
| The FX desktop UI | rdc.md · fx.md · atlanta.md |
| Auto-update | update.md · fx-rdc-update.md |
| Security | security.md |
| Services / config / bundles | common.md · services.md · i18n.md |
| Packaging | jlinkpackage.md |
| The architecture in the large | modules.md · tiers.md |
| The archetype itself | tentackle-project-archetype.md |