Tentackle Maven Support — The Shared Mojo Foundation¶
Overview and Motivation¶
tentackle-maven-support is a small, build-time-only library that provides the common base classes and
helpers shared by every Tentackle Maven plugin. It is not a plugin itself and defines no goals; instead it supplies
the AbstractMojo subclasses, the toolchain/tool-location plumbing, the annotation-processing harness, and the
assorted utilities that the actual plugins extend.
Seven plugins build on it:
tentackle-maven-plugin— the analyze phase, property/beaninfo/version generationtentackle-sql-maven-plugin— DDL generationtentackle-i18n-maven-plugin— i18n resource handlingtentackle-check-maven-plugin— model/consistency checkstentackle-jlink-maven-plugin— jlink/jpackage image buildingtentackle-wizard-maven-plugin— project wizardtentackle-mcp-maven-plugin— MCP server serving the model to coding agents
By factoring the recurring concerns into one place, each plugin only has to implement its own goal logic. The
module is packaged with an Automatic-Module-Name of org.tentackle.maven and is never shipped inside, nor used
by, the running application — its package-info simply reads "Support for Tentackle Maven Plugins."
What it provides¶
- A common mojo lifecycle (
AbstractTentackleMojo) with a fixedprepare → validate → execute → finishtemplate, uniform skip handling, verbosity and encoding setup, SLF4J logging (getLogger()) andjava.util.logging→Maven log bridging. - A JDK-toolchain–aware tool locator (
JavaToolFinder, plus the toolchain accessors on the base mojo) so external tools likejava,jdeps,jlinkandjpackageare found reliably across environments. - An annotation-processing harness (
AbstractTentackleAnnotationProcessingMojo) that runs the build-support analyze processors over a project's sources via the in-process Java compiler. - Resolver-based dependency handling (
ProjectDependencies,LayerDependencyResolver) that collects and resolves the dependencies of a project, or of a plain list of coordinates, via the Maven Resolver API only — the same code runs on Maven 3 and Maven 4 (see Maven 3 and Maven 4). - JPMS-oriented project introspection — a package scanner that builds a package→
PackageInfomap and detects split packages, which are illegal under the module system.
Dependencies¶
It depends on maven-core (which also provides SLF4J), the Maven Resolver API (maven-resolver-api),
maven-plugin-annotations, javax.inject and the Maven Shared file-management (filesets), on
Wurbelizer (for Verbosity/Constants/Settings) and on
tentackle-build-support (which transitively
pulls in tentackle-common for Settings, ToolRunner
and ToolFinder).
AbstractTentackleMojo — the common base mojo¶
AbstractTentackleMojo extends Maven's AbstractMojo
and is the root of all Tentackle mojos. Its central idea is a sealed execute() template: subclasses never
override execute() — they implement executeImpl() and may optionally hook prepareExecute() /
finishExecute().
@Override
public final void execute() throws MojoExecutionException, MojoFailureException {
installJavaLoggingHandler(); // route java.util.logging → Maven log (this thread)
try {
Lock lock = getExecutionLock(); // null: executions may run concurrently
if (lock == null) {
executeLifecycle();
}
else {
if (!lock.tryLock()) {
getLogger().info("waiting for concurrent execution to finish");
lock.lock();
}
try {
executeLifecycle();
}
finally {
lock.unlock();
}
}
}
finally {
MavenLogDispatcher.uninstall(); // unbind the log handler from this thread
}
}
private void executeLifecycle() throws MojoExecutionException, MojoFailureException {
prepareExecute(); // optional hook (no-op by default)
if (validate()) { // shared checks; may decide to skip
executeImpl(); // the goal's actual work
finishExecute(); // optional hook (no-op by default)
}
}
The execution lock is what makes the plugins safe for parallel builds, see Parallel builds.
Shared mojo parameters¶
All are injected by Maven, and the components are exposed via getters, so every plugin inherits them for free:
| Parameter | Purpose |
|---|---|
${project}, ${session}, ${mojoExecution} |
the Maven project, build session and execution |
${settings} + SettingsDecrypter |
settings.xml plus credential decryption (e.g. for servers) |
jdkToolchain + ToolchainManager |
per-plugin selection (or deselection) of the "jdk" toolchain |
RepositorySystem |
the Maven Resolver, see ProjectDependencies |
tentackle.verbosity |
default / info / debug (if unset, debug with Maven's -X; unknown → default) |
skip |
skip the goal; if unset, skipped for pom packaging in recursive executions |
charset (${project.build.sourceEncoding}) |
encoding for reading/writing files, applied JVM-wide (see Parallel builds) |
minLogLevel (default WARNING) |
threshold for the JUL→Maven log bridge |
Skip and validation logic¶
validate() runs after prepareExecute(): it determines the encoding (determineEncoding()) and verbosity
(determineVerbosity()), requires the project's base directory (getBaseDir(), the directory of the pom file),
and applies the skip-by-default rule (isSkippedByDefault()) — when skip is unset, a project with pom
packaging is skipped, unless the build is non-recursive (-N) or the goal is an aggregator
(isExecutionRecursive()). Subclasses override validate() to add their own preconditions (calling super
first), and return false to short-circuit the run. Notice that prepareExecute() runs even if the goal is
skipped, while finishExecute() only runs after executeImpl() succeeded.
Toolchain and Java-home resolution¶
getToolchain(...) honors an explicit <jdkToolchain> configuration (overriding the maven-toolchain-plugin),
an empty <jdkToolchain/> to deselect a configured toolchain, or otherwise falls back to the toolchain from the
build context. A configured selector that matches no toolchain fails the goal. getToolFinder() hands back a
JavaToolFinder for the "jdk" toolchain. getJavaHome(Toolchain) derives the java home of a "jdk" toolchain
from the location of its java tool and verifies that it is a directory.
Utility methods¶
A grab-bag of helpers the plugins rely on:
determineJavaToolVersion(File)/getMajorVersion(String)— run a tool with--versionand parse the Java version (skipping incubator-module noise), then reduce it to a major number.getPathRelativeToBasedir(String)— shorten absolute paths for readable log output.getResourceDirs(boolean)/getResourceDir(File, boolean)— the (test) resource directories of the project model, and the one a given file resides in.getCanonicalPath(File)wraps theIOException.getBaseDir()— the project's base directory, i.e. the directory of its pom file.createFileSetManager(boolean)/getIncludedFiles(FileSet)— expand Maven shared filesets.loadResourceFileIntoString(String)— read a UTF-8 classpath resource (relative to the mojo's class) as a string.toDescriptorName(File)— derive a clean descriptor/module name from an artifact jar filename (stripping the extension,-SNAPSHOT, version and platform classifiers).getHostName()— best-effort hostname (falls back to the loopback address).installJavaLoggingHandler()— install theMavenLogDispatcheron the JUL root logger and bind aMavenLogHandlerfor this mojo to the current thread.
Package scanning and split-package detection¶
createPackageMap(boolean resourceRoots) walks the compile-source roots (or non-filtered resource roots) of the
project — or of all the reactor's collected projects when run non-recursively — and builds a
Map<String, PackageInfo>. A package is a directory that contains files (hidden ones don't count) or is empty.
While scanning, it enforces a JPMS rule: a package containing files must not appear in more than one module.
A genuine split (two modules both contributing files to the same package) throws a MojoExecutionException.
Empty package directories are tolerated: a non-empty occurrence wins, and if all occurrences are empty, the others
are tracked as "empty duplicates" of the first one. The wizard
uses the map to locate the target packages of its profiles, the i18n plugin to locate the bundles.
AbstractTentackleAnnotationProcessingMojo — the analyze harness¶
AbstractTentackleAnnotationProcessingMojo
extends the base mojo to run Tentackle's
annotation analyze phase — the pass that
generates service descriptors and analyze-info files before wurbeling and the real compilation. It is the
superclass of the analyze mojos of the tentackle-maven-plugin and of the check mojos. The SQL and i18n mojos
extend AbstractTentackleMojo directly.
It implements executeImpl() to drive the in-process Java compiler (ToolProvider.getSystemJavaCompiler())
purely for annotation processing:
- The set of source files is taken either from explicit
<filesets>or, by default, from the configuredsourceDir(**/*.java). ThesourceDiris required in either case; it is also the default directory of a fileset without one. - For each fileset, the registered
AbstractTentackleProcessors are initialized, then aCompilationTaskis created with the source encoding (if set), the project classpath (added to both-classpathand--module-path),-Averbosity=…passed through to the processors, thecompilerArgumentandcompilerArgs, and finally-proc:only— unless the arguments already contain a-proc:option. Subclasses may change that default viagetDefaultProcOption()and how the task is run viarunCompilationTask(...): thebundlesgoal of the check plugin uses-proc:noneand attributes the sources withJavacTask.analyze()to learn the types of the expressions. - Diagnostics are collected and formatted as
file:line:col: message. An error is an analyze error if it was reported by a processor'sMessager(javac's diagnostic codecompiler.err.proc.messager, seeisAnalyzeError), any other error is a compile error. Both are counted separately — analyze errors are always logged and fail the build, while compile errors only warn (the sources have not been wurbeled yet, so some are expected). WithshowCompileOutput, all diagnostics are logged. The logged messages are available viagetCompileErrorLog(). - After all filesets, the processors'
cleanup()is invoked. A failing cleanup counts as an analyze error.
Configuration entry points for subclasses:
setMojoParameters(File sourceDir, List<String> classpathElements)— sets the source directory and creates theProjectClassLoader;addProcessor(...)registers the processors to run.- Overridable hooks:
createMissingDirs(),initializeProcessors(...)/initializeProcessor(...),cleanupProcessors(...)(per fileset),filterFileNames(...)(to skip files / opt out of processing),isAnalyzeError(...)andformatDiagnostic(...). - Results:
getTotalErrors()(analyze errors) andgetTotalCompileErrors(). - Mojo parameters:
showCompileOutput,encoding,compilerArgs,compilerArgument,filesets.
Parallel builds¶
All Tentackle goals are declared threadSafe, so a reactor build with mvn -T … runs without Maven's
"not thread-safe" warning. Maven loads each plugin into one class realm per build, however, so the static
state of a plugin — singletons such as the ModelManager, the BundleFactory or the ModificationTracker —
is shared by all projects built concurrently. Plugins that work on such state return a lock from
getExecutionLock(), and execute() holds it for the whole mojo lifecycle. Concurrent executions of these
goals wait for each other and log waiting for concurrent execution to finish.
| Plugin | Executions |
|---|---|
tentackle-maven-plugin |
concurrent |
tentackle-sql-maven-plugin |
serialized (model), except reveng, which runs concurrently |
tentackle-i18n-maven-plugin |
serialized (modification tracker bound to the goal's session) |
tentackle-check-maven-plugin |
serialized (bundle, validation and script factories) |
tentackle-jlink-maven-plugin |
concurrent |
tentackle-wizard-maven-plugin |
serialized (model, code factory, JavaFX platform) |
tentackle-mcp-maven-plugin |
serialized (model) |
Two more JVM-wide settings are relevant for parallel builds:
- Encoding. The
charsetof a goal is stored in static settings (Settings.setEncodingCharset). All projects of a parallel build must therefore use the same encoding; otherwise the goal fails withconflicting encodings in parallel build: … requested, but … already in use. Setproject.build.sourceEncodingonce in the parent POM. - Logging.
java.util.loggingoutput is routed per thread by theMavenLogDispatcher, so each record shows up in the log of the project that produced it.
The wurblets run by the wurbelizer-maven-plugin are thread-safe as well, see
Wurblets.
Maven 3 and Maven 4¶
All Tentackle Maven plugins are compiled against the Maven 3.9 plugin API and run unchanged on Maven 3.9 and Maven 4. The rules that make this work:
- Dependencies are collected and resolved only via the Maven Resolver API (
ProjectDependencies).maven-compat,maven-artifact-transferandmaven-dependency-treeare not used. - Logging goes through SLF4J (
getLogger()), which both Maven versions provide, instead of the deprecatedAbstractMojo.getLog(). Messages use SLF4J's{}placeholders instead of string concatenation, so nothing is built for suppressed levels. Tests replace the logger bysetLogger(...)to capture the output. - Injection uses
javax.inject, which both Maven versions honor. - The project's base directory is derived from the pom file (
getBaseDir()), the toolchain's java home fromToolchain.findTool("java").
A few APIs are marked deprecated by Maven 4 but their replacements exist only in the Maven 4 API, which is not
available on Maven 3. They are still fully supported by Maven 4 and are therefore kept, in one place each:
Toolchain/ToolchainManager, SettingsDecrypter (and its request/result types), MavenProject.getCompileSourceRoots()
(wrapped by AbstractTentackleMojo) and Artifact.getFile() of Resolver 2.0 (wrapped by ProjectDependencies.getFile).
The analyze mojos also still use the ${project.compileClasspathElements} parameter expression, which Maven resolves itself.
Supporting classes¶
JavaToolFinder¶
JavaToolFinder extends
ToolFinder and resolves a tool executable by name in a
robust order: the toolchain first (if one was supplied), then the directory of the currently running java
(via ProcessHandle.current(), safe regardless of JAVA_HOME/PATH), then $JAVA_HOME/bin, and finally the
inherited PATH search. It also appends OS-appropriate suffixes (.exe/.cmd/.bat on Windows, .sh
elsewhere). This is what lets the jlink plugin reliably find jlink, jpackage, jdeps, etc.
MavenLogHandler¶
MavenLogHandler is a java.util.logging.Handler that
forwards records at or above a minimum level to the mojo's SLF4J logger, mapping SEVERE→error, WARNING→warn,
INFO→info and everything else to debug. Because Tentackle's own code logs through java.util.logging by
default, this bridge keeps build-time framework logging visible in the Maven output without flooding it (default
threshold WARNING, configured by the minLogLevel parameter). The raw message of the record is forwarded:
Tentackle's logger formats the message before it creates the record.
MavenLogDispatcher¶
MavenLogDispatcher replaces the handlers of the
JUL root logger. The root logger is a JVM-wide singleton, so in a parallel build the log records of several
concurrently running mojos arrive at the same handlers. Each plugin's class realm installs its own dispatcher. The dispatcher therefore forwards each record to the
MavenLogHandler bound to the current thread (an InheritableThreadLocal, so threads started by a mojo log
to the same mojo), and the output ends up in the log of the project that produced it.
install(MavenLogHandler) adds the dispatcher once per realm — removing the default console handler, but keeping
the dispatchers installed by other plugins' class realms — and binds the handler to the thread; uninstall()
unbinds it. Records of threads without a binding are discarded. AbstractTentackleMojo.execute() does both, so plugins don't have to.
ProjectDependencies and LayerDependencyResolver¶
ProjectDependencies is the single place where Tentackle
plugins talk to the Maven Resolver (org.eclipse.aether). It converts POM dependencies to resolver dependencies
(toAetherDependency, honoring type, classifier, scope, optional flag, exclusions and system paths), collects the
dependency graph of a MavenProject (collect, including dependencyManagement and the project's repositories),
resolves it (resolve, getClasspathElements), restricted to the given scopes (RUNTIME_SCOPES, COMPILE_SCOPES,
applied via scopeFilter). toCoordinates formats an artifact as groupId:artifactId:version. It replaces
MavenProject.getArtifacts(), maven-artifact-transfer and maven-dependency-tree.
LayerDependencyResolver resolves a plain list of
coordinates independently of any project, all of them with runtime scope, for example the dependencies of a layer
of a layered application.
The RepositorySystem is injected once into AbstractTentackleMojo (getRepositorySystem()); the session is
getMavenSession().getRepositorySession().
ProjectClassLoader¶
ProjectClassLoader is a URLClassLoader that adds the
project's classpath elements on top of the plugin's own classloader, so the analyze processors can load and
inspect classes from the project being built. It delegates parent-first, i.e. the plugin's classes take precedence.
It exposes addURL publicly.
PackageInfo¶
PackageInfo is the value object produced by the base mojo's
package scan: the package name, the owning Maven project, and the directory path. isContainingFiles() underpins
split-package detection, and getEmptyDuplicates()/setEmptyDuplicates(...) track empty package directories that
occur in several modules (which cannot be used in the wizard's profiles, since the generator could not decide which to pick).
Hidden files are ignored by both the scan and isContainingFiles(), so a leftover directory holding nothing but
a .gitignore stays an empty package. PackageInfo.isHidden(File) decides this, treating a leading dot as hidden
on all platforms, since Windows only knows the DOS hidden attribute.
How It Fits Into the Build¶
tentackle-maven-support
┌────────────────────────────────────────────┐
│ AbstractTentackleMojo │ lifecycle, toolchains,
│ └─ AbstractTentackleAnnotationProcessing │ skip/verbosity, utilities
└────────────────────────────────────────────┘
▲ ▲ ▲
┌───────────────┘ ┌─────┘ ┌────┴───────────┐
tentackle-maven-plugin …-sql-… …-i18n-… / …-check-… / …-jlink-… / …-wizard-… / …-mcp-…
(analyze, properties, (DDL) (each implements executeImpl())
beaninfo, versions)
│
▼ runs
tentackle-build-support (AnalyzeProcessor / handlers → META-INF + analyze info)
The plugins supply the goals and bind to the lifecycle; tentackle-maven-support supplies the shared mojo
machinery; and the annotation processors it
drives produce the service descriptors and analyze-info files later consumed by the
wurblets.