Check Maven Plugin
The Tentackle Checker Maven Plugin¶
The tentackle-check-maven-plugin performs build-time consistency checks that a normal compiler
cannot catch. It verifies two kinds of string references that are only resolved at runtime and would
otherwise fail silently — or worse, in front of the user — when a translation or a script is missing:
- that every resource-bundle key referenced from the Java sources actually exists in the
corresponding
*.propertiesbundle, for all checked locales, and - that every annotation-based validation is well-formed: its validator can be instantiated, its scripts compile, and its (possibly i18n-scripted) message resolves for every checked locale.
Both checks report every problem they find, each with its location, and then fail the build with a
summary count. This turns a class of runtime MissingResourceExceptions into build-time errors.
The goals share the prefix tentackle-check, so they can be invoked directly from the command line,
e.g., mvn tentackle-check:bundles.
How It Works¶
Both goals pass the project's sources to the in-process Java compiler. The validations goal runs
it with -proc:only and lets an annotation processor inspect the annotations of the elements. Errors
reported by the processor fail the build. The bundles goal runs it with -proc:none, attributes
the sources (JavacTask.analyze(), no class files are written), and scans the attributed syntax
trees for getString invocations. Compilation errors in the sources are only logged as warnings
(see showCompileOutput).
The checks themselves load the project's compiled output: the annotation and validator classes,
the *.properties bundles copied to target/classes, and the META-INF/bundles files written by the
analyze goal of the tentackle-maven-plugin. They therefore run in the process-classes phase and
use a classloader built from the project's compile classpath, which includes the project's own
output directory.
Because the checks operate on the source tree, classes that must not be checked have to be excluded from the file set rather than annotated.
Prerequisites¶
The plugin requires the same JDK and Maven versions as the rest of the framework (JDK >= 25, Maven >= 3.9.10, Maven 4 is supported as well). It is platform independent. The goals declare compile-scope dependency resolution, so Maven resolves the compile classpath for them.
The plugin ships no scripting language. If validators use scripts, the language's provider, such as
tentackle-script-groovy, must be added as a dependency of the plugin (see the example below).
Overview of Goals¶
| Goal | Default phase | Purpose |
|---|---|---|
bundles |
process-classes |
Verify that all getString(...) bundle keys found in the sources exist in the bundles, for all checked locales. |
validations |
process-classes |
Verify that all @Validation-based validators instantiate and that their value/condition/message scripts compile and resolve. |
help |
— | Print plugin usage information (generated by the maven-plugin-plugin). |
Neither goal is bound to a Tentackle lifecycle by default — you add the executions you need to the modules that contain bundles or validations.
Common Configuration¶
Like all Tentackle mojos, the goals inherit the common parameters verbosity, skip, charset,
jdkToolchain and minLogLevel
(see the tentackle-maven-plugin docs).
As annotation-processing mojos, they also accept the compiler parameters of the analyze goal:
showCompileOutput, encoding, compilerArgs and compilerArgument.
On top of that, AbstractCheckMojo adds the parameters shared by both check goals:
sourceDir(tentackle.sourceDir): directory holding the sources to process. Defaults to${project.build.sourceDirectory}. Usefilesetsinstead if a subset must be checked or excluded.filesets: explicit file sets to process instead ofsourceDir. A file set without a directory defaults tosourceDir.locales: a comma/semicolon/whitespace separated list of locales to check the bundles and validation messages against (e.g.en, de). Both theen_USand theen-USnotation are accepted. The fallback locale should come first, because thebundlesgoal compares the other locales against it. If omitted, or if none of the given locales is usable, the build's default locale is used (and a warning is logged). The parameter has no user property, i.e. it can only be set in the POM;-Dlocales=…on the command line has no effect.skipTests(${skipTests}): skips the check, so the standard-DskipTestsswitch disables it together with the unit tests.scriptingLanguage: the default scripting language of the validator scripts, e.g.groovy. It is required if the scripts use the shorthand{…}rather than naming their language, as in#!groovy{…}(see CompoundValue). An unknown language fails the goal. If the parameter is not set, there is no default language, even if another module of the same build configured one.
A typical configuration registers both checks on a module that ships bundles and validations, with Groovy as the language of the validator scripts:
<plugin>
<groupId>org.tentackle</groupId>
<artifactId>tentackle-check-maven-plugin</artifactId>
<version>${project.version}</version>
<executions>
<execution>
<id>check-bundles</id>
<goals>
<goal>bundles</goal>
</goals>
</execution>
<execution>
<id>check-validations</id>
<goals>
<goal>validations</goal>
</goals>
</execution>
</executions>
<configuration>
<locales>en, de</locales>
<scriptingLanguage>groovy</scriptingLanguage>
</configuration>
<dependencies>
<dependency>
<groupId>org.tentackle</groupId>
<artifactId>tentackle-script-groovy</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</plugin>
The bundles Goal¶
Scans the sources for every invocation of <reference>.getString(<key>) and checks that
the referenced key exists in the matching bundle, loaded for each configured locale. The <key> must
be a compile-time constant: a string literal such as "foo", a constant variable such as
static final String FOO = "foo" (of any class, including those on the classpath), or a
concatenation of those, such as PREFIX + "title". The bundle a reference belongs to is derived
from the <reference>:
- a bundle class name, e.g.
BlahBundle.getString("foo"), is checked against theBlahBundleproperties. The class is resolved via a single-type import or, if there is none, the package of the invoking class. Wildcard imports and fully qualified references are not resolved; - a method call such as
getBundle().getString("foo")is checked against the bundle named after the enclosing class (Blah.propertiesfor classBlah); - a variable or constant reference such as
resources.getString("foo")is treated the same way — the bundle name is that of the enclosing class.
A reference whose last name starts lowercase or is all uppercase counts as a variable or constant. Any other name is taken as a bundle class. Nested, local, and anonymous classes use the bundle of their outermost class.
Since the sources are attributed, only invocations that really refer to a bundle are checked: a
static getString(String) of a bundle class, such as BlahBundle.getString("foo"), or a getString
invoked on a java.util.ResourceBundle (or a subclass). A bundle class is annotated with @Bundle or
another annotation that registers bundles, such as @FxControllerService (any annotation whose
@Analyze handler is a BundleAnalyzeHandler). Other calls, such as ResultSet.getString("name") or
the static getString of a class without such an annotation, are ignored and listed with verbosity set to debug. If the invoked method
or the type of the reference cannot be determined, for example because of compile errors, the
invocation is checked anyway and logged as a warning, unless suppressed with @SuppressWarnings("bundle").
To narrow down the checked sources, use filesets. Unlike sourceDir, a file set is not limited to
Java sources, so the include is required:
<configuration>
<filesets>
<fileset>
<includes>
<include>**/*.java</include>
</includes>
<excludes>
<exclude>**/noi18n/**</exclude>
</excludes>
</fileset>
</filesets>
</configuration>
Only referenced bundles are loaded: the goal first collects the used bundle names, then loads those
among the bundles registered via @Bundle/@FxControllerService annotations, which the analyze goal
lists in META-INF/bundles. This keeps the check fast even with many bundles on the classpath.
A referenced bundle that is not registered is an error, reported as bundle … not registered by
annotation.
Some classes only look like bundles: they load an ordinary resource bundle via
getBundle().getString(…), but must not be registered with @Bundle, because registered bundles are
pushed to and pulled from the database by the
i18n Maven plugin.
An example is the Version class generated by the project archetype, which reads the build
properties. Annotate such a class with @NoBundle: its invocations are ignored like those not
referring to a bundle, so neither errors nor warnings are reported.
Beyond checking individual keys, the goal also compares the complete key sets of each referenced
bundle across the configured locales. This includes keys accessed with non-literal arguments. The
keys of the first locale serve as the reference: a key missing in another locale is reported as
key '…' missing in …, and a key that only another locale defines as
key '…' for … not defined in all locales of …. This catches translations that would throw a
MissingResourceException at runtime.
What is not covered¶
The check is deliberately limited to compile-time constant keys, because only those can be resolved statically. The following must be covered by ordinary unit tests instead:
- non-constant keys, e.g.
getBundle().getString(text), as frequently found in enums. Each such bundle invocation is logged as a warning (cannot verify getString() argument, not a constant), prefixed with its source location. Once a unit test covers the keys, annotate the method, the class, or any other enclosing declaration with@SuppressWarnings("bundle")to silence the warning; PdoBundles, i.e., translations for@Singular/@Plural(seePdoUtilitiesTest);ValidationBundles, i.e., translations inside validators — those are covered by thevalidationsgoal instead.
Note: due to the fallback nature of resource bundles, a key missing from
bundle_de.propertiesbut provided by the fallbackbundle.propertiesis not reported, because it would still resolve at runtime. The cross-locale diff only reports keys that would actually cause aMissingResourceException.
The validations Goal¶
Scans for validator annotations (those meta-annotated with
@Validation), instantiates the validator
for each occurrence, and verifies its three compound-value parameters — value, condition and
message:
- a value or condition script is compiled once, catching syntax errors at build time;
- a message script is evaluated against the class holding the annotation for every configured
locale. This resolves the translations of an i18n-script (see
@MessageScriptConverter), so a missing translation is reported as an error, as a single line without a stack trace, rather than surfacing later as a runtime exception.
A validator that cannot be created, e.g. because its class cannot be loaded or has no public no-argument constructor, is reported by the annotation processor and fails the build before any script is checked. Otherwise, the goal reports the number of annotations and scripts checked, where message scripts count once per locale, and fails the build if any script fails to compile or resolve.
Use scriptingLanguage if your validators use the {…} shorthand for scripts in the default
language, and add the language's provider as a plugin dependency.
How It Fits Into the Build¶
Both goals run in process-classes, i.e., after compilation but before tests and packaging, so a
broken translation or validation script breaks the build early — locally and in CI alike. Because
they share the skipTests switch with the unit tests, a mvn install -DskipTests skips them too,
which keeps fast inner-loop builds fast while still guarding the full build and the CI pipeline.
A common arrangement is to enable the bundles goal in every module that carries UI- or
message bundles, and the validations goal in the domain modules that define validators, listing the
production locales (fallback first) in the shared plugin configuration.
Both goals are thread-safe, but configure JVM-wide singletons (the classloaders of the bundle and
validation factories, and the default scripting language). In a parallel build (mvn -T …),
concurrent executions are therefore serialized; see
Parallel builds.
Further Reading¶
- Validation — the annotation-based validation framework whose validators this plugin checks
- Tentackle Maven Plugin — the companion plugin for service- and Java metadata generation, and the source of the common mojo parameters
- Tentackle i18n Maven Plugin — tooling for managing the translation bundles themselves
- Plugin reference (generated)