Skip to content

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:

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 fixed prepare → validate → execute → finish template, uniform skip handling, verbosity and encoding setup, SLF4J logging (getLogger()) and java.util.logging→Maven log bridging.
  • A JDK-toolchain–aware tool locator (JavaToolFinder, plus the toolchain accessors on the base mojo) so external tools like java, jdeps, jlink and jpackage are 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→PackageInfo map 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 --version and 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 the IOException.
  • 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 the MavenLogDispatcher on the JUL root logger and bind a MavenLogHandler for 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:

  1. The set of source files is taken either from explicit <filesets> or, by default, from the configured sourceDir (**/*.java). The sourceDir is required in either case; it is also the default directory of a fileset without one.
  2. For each fileset, the registered AbstractTentackleProcessors are initialized, then a CompilationTask is created with the source encoding (if set), the project classpath (added to both -classpath and --module-path), -Averbosity=… passed through to the processors, the compilerArgument and compilerArgs, and finally -proc:only — unless the arguments already contain a -proc: option. Subclasses may change that default via getDefaultProcOption() and how the task is run via runCompilationTask(...): the bundles goal of the check plugin uses -proc:none and attributes the sources with JavacTask.analyze() to learn the types of the expressions.
  3. Diagnostics are collected and formatted as file:line:col: message. An error is an analyze error if it was reported by a processor's Messager (javac's diagnostic code compiler.err.proc.messager, see isAnalyzeError), 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). With showCompileOutput, all diagnostics are logged. The logged messages are available via getCompileErrorLog().
  4. 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 the ProjectClassLoader; addProcessor(...) registers the processors to run.
  • Overridable hooks: createMissingDirs(), initializeProcessors(...) / initializeProcessor(...), cleanupProcessors(...) (per fileset), filterFileNames(...) (to skip files / opt out of processing), isAnalyzeError(...) and formatDiagnostic(...).
  • Results: getTotalErrors() (analyze errors) and getTotalCompileErrors().
  • 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 charset of 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 with conflicting encodings in parallel build: … requested, but … already in use. Set project.build.sourceEncoding once in the parent POM.
  • Logging. java.util.logging output is routed per thread by the MavenLogDispatcher, 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-transfer and maven-dependency-tree are not used.
  • Logging goes through SLF4J (getLogger()), which both Maven versions provide, instead of the deprecated AbstractMojo.getLog(). Messages use SLF4J's {} placeholders instead of string concatenation, so nothing is built for suppressed levels. Tests replace the logger by setLogger(...) 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 from Toolchain.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.