Writing Maven Plugins
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Apache Maven 3.10.0 with Maven Plugin Tools 3.16.0 (the Maven Plugin Plugin and the annotations),
the Maven Plugin Testing Harness 3.5.1 and the Maven Invoker Plugin 3.10.1. Every example was run on JDK 26 on 2026-10-16 in a
scratch directory with its own local repository, so that the example plugin never touched the real one. The plugin is compiled
with release 21 against the Maven 3.9.16 API, which makes the same JAR run on Maven 3.9.16, 3.10.0 and Maven 4.0.0-rc-7
(a release candidate, not a final release); the build of the plugin itself and the consumer builds were run on all three, and
the differences are stated. Paths in console output were shortened to /work/writing-plugins/... and $MAVEN_HOME; the three
banner lines of each project, blank [INFO] lines and download lines were left out, lines longer than about 200 characters end in
[...], and the long lists of artifacts that the Invoker Plugin copies were filtered out and say so. The scenario is the
tidepool project used across this section, here as a data-quality goal for the data/stations.csv file that the other
modules of tidepool read: the same check that
Writing Custom Ant Tasks builds as an Ant task.
Almost everything that Maven does is done by a plugin, and a plugin is not a special kind of thing: it is a JAR with a
descriptor, built by an ordinary Maven project, and stored in an ordinary repository. That is why writing one is a small
exercise: a Java class per goal, a few annotations, one packaging value in the POM. The page walks through all of it, builds
tidepool:check-stations step by step, runs it, breaks it on purpose to collect the real error messages, tests it three ways,
and finishes with the Maven 4 plugin API. How to use plugins is on
Plugins & Their Configuration; the phases
a goal can be bound to are on Maven Build Lifecycle; the
alternative that changes how Maven itself builds, a core extension, is on
Extensions.
What a plugin is made of
A plugin is a JAR whose META-INF/maven/plugin.xml describes one or more goals. Each goal is implemented by a Mojo, the
name being a play on POJO (plain old Java object) with "Maven" in place of "plain". The descriptor says, for every goal, its name,
the class that implements it, the phase it fits into by default, the Maven features it needs and every parameter with its
type, default value and the user property that sets it. Maven reads the descriptor to decide what to run and how to configure it;
the Java code is loaded only when the goal runs.
The descriptor is not written by hand. The Maven Plugin Plugin (maven-plugin-plugin, from the Maven Plugin Tools project)
generates it from the annotations and the Javadoc of the Mojo classes while the plugin is built, and the maven-plugin
packaging wires that into the lifecycle. The two diagrams show the two halves: how the JAR comes to be, and what Maven does
with it when someone types a goal:
The tidepool goal
tidepool:check-stations validates the station files that the tidepool engine reads (data/stations.csv, one station per row
with id, name, latitude, longitude and a semicolon-separated list of constituents). The checks are the ones of the Ant
task: five columns per row, latitude within [-90, 90], longitude within [-180, 180], no duplicate ids, enough constituents, every
required id present. Only the way the build file talks to the code differs, and that is what this page is about:
Ant <checkstations> |
Maven tidepool:check-stations |
Java type of the field | How a user sets it |
|---|---|---|---|
|
|
|
|
nested |
|
|
|
|
|
|
|
|
|
|
|
|
|
a Java |
|
|
|
|
|
nested |
|
|
|
the task is skipped by not calling it |
|
|
|
The Gradle equivalent is a task class with annotated input properties, registered by a plugin with the id
tidepool.station-check: see Binary Plugins and
Writing Custom Tasks. The Ant version, with its
naming-convention setters and reflection, is on the page linked in the introduction.
The plugin project
A plugin project is a normal Maven project with the packaging maven-plugin. The directory is the usual layout plus the
integration tests under src/it, which are explained in the testing section:
tidepool-maven-plugin/
pom.xml
src/main/java/io/tidepool/maven/
CheckStationsMojo.java the goal check-stations
ListStationsMojo.java the goal list-stations (forks check-stations first)
StationReportMojo.java the report goal station-report
StationChecker.java the checks, free of Maven types
ReportMode.java the enum of the report parameter
src/test/java/io/tidepool/maven/
StationCheckerTest.java plain JUnit
CheckStationsMojoTest.java plugin testing harness
src/test/resources/ good.csv, bad.csv
src/it/ integration tests run by the Invoker Plugin
The POM, complete:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-maven-plugin</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>maven-plugin</packaging>
<name>Tidepool Maven Plugin</name>
<description>Validates tidepool station files.</description>
<properties>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.build.outputTimestamp>2026-10-16T00:00:00Z</project.build.outputTimestamp>
<maven.version>3.9.16</maven.version>
<plugin-tools.version>3.16.0</plugin-tools.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-plugin-api</artifactId>
<version>${maven.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-core</artifactId>
<version>${maven.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.maven.plugin-tools</groupId>
<artifactId>maven-plugin-annotations</artifactId>
<version>${plugin-tools.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.maven.reporting</groupId>
<artifactId>maven-reporting-impl</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.maven.plugin-testing</groupId>
<artifactId>maven-plugin-testing-harness</artifactId>
<version>3.5.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>6.1.3</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-plugin-plugin</artifactId>
<executions>
<execution>
<id>help-mojo</id>
<goals>
<goal>helpmojo</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-invoker-plugin</artifactId>
<version>3.10.1</version>
<configuration>
<cloneProjectsTo>${project.build.directory}/it</cloneProjectsTo>
<localRepositoryPath>${project.build.directory}/local-repo</localRepositoryPath>
<pomIncludes>
<pomInclude>*/pom.xml</pomInclude>
</pomIncludes>
<postBuildHookScript>verify</postBuildHookScript>
<streamLogs>true</streamLogs>
</configuration>
<executions>
<execution>
<id>integration-test</id>
<goals>
<goal>install</goal>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
<pluginManagement>
<plugins>
<plugin><artifactId>maven-clean-plugin</artifactId><version>3.5.0</version></plugin>
<plugin><artifactId>maven-resources-plugin</artifactId><version>3.5.0</version></plugin>
<plugin><artifactId>maven-compiler-plugin</artifactId><version>3.16.0</version></plugin>
<plugin><artifactId>maven-surefire-plugin</artifactId><version>3.6.0</version></plugin>
<plugin><artifactId>maven-jar-plugin</artifactId><version>3.5.1</version></plugin>
<plugin><artifactId>maven-install-plugin</artifactId><version>3.2.0</version></plugin>
<plugin><artifactId>maven-deploy-plugin</artifactId><version>3.2.0</version></plugin>
<plugin><artifactId>maven-plugin-plugin</artifactId><version>${plugin-tools.version}</version></plugin>
</plugins>
</pluginManagement>
</build>
</project>
What each part does:
-
The coordinates. The artifactId follows the convention
<prefix>-maven-plugin, which is what lets Maven derive the goal prefixtidepoolby itself. The patternmaven-<prefix>-pluginis reserved for the plugins of the Apache Maven project under the groupIdorg.apache.maven.plugins, and the official guide calls using it an infringement of the Apache Maven trademark. -
The packaging.
maven-plugingives the project its own set of lifecycle bindings (next section) and tells Maven that the artifact has to carry a descriptor. -
Three
provideddependencies.maven-plugin-apiforAbstractMojoand the exceptions,maven-coreforMavenProject,RuntimeInformationand the other build objects, andmaven-plugin-annotationsfor@Mojoand@Parameter. They areprovidedbecause Maven supplies them at run time. Their version is the oldest Maven that the plugin supports: the descriptor generated below recordsrequiredMavenVersion3.9.16 from it andrequiredJavaVersion21 frommaven.compiler.release. Maven enforces the Java requirement (see the mistakes section for the message). In a test with a plugin that declaredrequiredMavenVersion3.99.0, Maven 3.9.16, 3.10.0 and 4.0.0-rc-7 ran the goal without any complaint, so that entry is documentation rather than a guard. -
maven-reporting-implis the one compile-scope dependency, needed only by the report goal. It brings the Doxia sink API and a few helper libraries into the plugin class realm, which is why a plugin without report goals should not have it. -
maven-plugin-pluginwith ahelpmojoexecution. Thedescriptorgoal is bound by the packaging; thehelpmojogoal is not, and generates thehelpgoal described below. Its version is pinned to the Plugin Tools version, as the official guide does. -
project.build.outputTimestampmakes the JAR reproducible, see Security & Reproducible Builds. -
Every plugin version is pinned in
pluginManagement, as everywhere in this section.
Source: Guide to Developing Java Plugins: project definition and important notice on the naming convention
The lifecycle of the maven-plugin packaging
The packaging binds these goals, as listed in the Maven core reference (the Surefire, resources and compiler goals appear twice, for the main and the test code):
| Phase | Goal |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The descriptor is generated in process-classes, after compilation, from the compiled classes and the sources. Running mvn clean verify on Maven 3.10.0 shows the goals in order, with the helpmojo goal of the POM
first (it generates a source file, so it has to run before compile) and the Invoker Plugin at the end:
[INFO] --- clean:3.5.0:clean (default-clean) @ tidepool-maven-plugin ---
[INFO] Deleting /work/writing-plugins/tidepool-maven-plugin/target
[INFO] --- plugin:3.16.0:helpmojo (help-mojo) @ tidepool-maven-plugin ---
[INFO] --- resources:3.5.0:resources (default-resources) @ tidepool-maven-plugin ---
[INFO] skip non existing resourceDirectory /work/writing-plugins/tidepool-maven-plugin/src/main/resources
[INFO] --- compiler:3.16.0:compile (default-compile) @ tidepool-maven-plugin ---
[INFO] Recompiling the module because of changed source code.
[INFO] Compiling 6 source files with javac [debug release 21] to target/classes
[INFO] --- plugin:3.16.0:descriptor (default-descriptor) @ tidepool-maven-plugin ---
[INFO] Using 'UTF-8' encoding to read mojo source files.
[WARNING] Unresolvable link in javadoc tag with value #getOutputName() found in target/maven-plugin-plugin-sources/org.apache.maven.reporting/maven-reporting-impl/4.0.0/sources/org/apache/maven/re [...]
[WARNING] Unresolvable link in javadoc tag with value #getOutputName() found in target/maven-plugin-plugin-sources/org.apache.maven.reporting/maven-reporting-impl/4.0.0/sources/org/apache/maven/re [...]
[INFO] java-annotations mojo extractor found 4 mojo descriptors.
[INFO] java-javadoc mojo extractor found 0 mojo descriptor.
[INFO] ant mojo extractor found 0 mojo descriptor.
[INFO] bsh mojo extractor found 0 mojo descriptor.
[WARNING] Mojo station-report uses Plexus Component requirements (@Component annotation) for fields: [container, siteRenderer, siteTool]
[WARNING] Use JSR 330 annotations to inject dependencies instead.
[INFO] --- resources:3.5.0:testResources (default-testResources) @ tidepool-maven-plugin ---
[INFO] Copying 2 resources from src/test/resources to target/test-classes
[INFO] --- compiler:3.16.0:testCompile (default-testCompile) @ tidepool-maven-plugin ---
[INFO] Recompiling the module because of changed dependency.
[INFO] Compiling 2 source files with javac [debug release 21] to target/test-classes
[INFO] --- surefire:3.6.0:test (default-test) @ tidepool-maven-plugin ---
[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
[INFO] T E S T S
[INFO] Running io.tidepool.maven.CheckStationsMojoTest
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
WARNING: Final field logger in class org.apache.maven.plugin.testing.MojoLogWrapper has been mutated reflectively by class org.mockito.internal.util.reflection.ReflectionMemberAccessor in unnamed [...]
WARNING: Use --enable-final-field-mutation=ALL-UNNAMED to avoid a warning
WARNING: Mutating final fields will be blocked in a future release unless final field mutation is enabled
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.883 s -- in io.tidepool.maven.CheckStationsMojoTest
[INFO] Running io.tidepool.maven.StationCheckerTest
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.018 s -- in io.tidepool.maven.StationCheckerTest
[INFO] Results:
[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0
[INFO] --- jar:3.5.1:jar (default-jar) @ tidepool-maven-plugin ---
[INFO] Building jar: /work/writing-plugins/tidepool-maven-plugin/target/tidepool-maven-plugin-1.0.0-SNAPSHOT.jar
The plugin:descriptor lines show the extractors: java-annotations found the four Mojos (the three goals of the plugin
and the generated help goal), while java-javadoc (the tags of Maven 2, see the section on outdated material), ant and bsh
(Mojos written as Ant or BeanShell scripts) found none. The two warnings about #getOutputName() and about Plexus
@Component requirements come from the report goal and are explained in its section. Output of the tests and the integration
tests is covered in the testing section.
The Mojo class
The checks live in a plain class that knows nothing about Maven. It reports findings through two callbacks, so a Mojo can send them to the Maven log and a unit test can collect them in a list:
package io.tidepool.maven;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.List;
import java.util.Set;
import java.util.function.BiConsumer;
/** The checks behind the goals, kept free of Maven types so that they are easy to test. */
final class StationChecker {
/** Receives one finding: the severity and the text. */
enum Severity { ERROR, WARNING }
private final int minConstituents;
private final BiConsumer<Severity, String> findings;
private final BiConsumer<String, String> rows;
StationChecker(int minConstituents, BiConsumer<Severity, String> findings, BiConsumer<String, String> rows) {
this.minConstituents = minConstituents;
this.findings = findings;
this.rows = rows;
}
/** Checks one file, adds the station ids to {@code seen} and returns the number of problems. */
int check(File input, Set<String> seen) throws IOException {
List<String> lines = Files.readAllLines(input.toPath(), StandardCharsets.UTF_8);
int problems = 0;
for (int i = 1; i < lines.size(); i++) { // line 0 is the header
String[] f = lines.get(i).split(",", -1);
String where = input.getName() + ":" + (i + 1);
rows.accept(where, lines.get(i));
if (f.length != 5) {
findings.accept(Severity.ERROR, where + " expected 5 columns, found " + f.length);
problems++;
continue;
}
if (!seen.add(f[0])) {
findings.accept(Severity.ERROR, where + " duplicate station id " + f[0]);
problems++;
}
problems += range(where, "latitude", f[2], -90, 90);
problems += range(where, "longitude", f[3], -180, 180);
int n = f[4].isEmpty() ? 0 : f[4].split(";").length;
if (n < minConstituents) {
findings.accept(Severity.WARNING,
where + " has " + n + " constituent(s), at least " + minConstituents + " required");
problems++;
}
}
return problems;
}
private int range(String where, String what, String text, double min, double max) {
try {
double v = Double.parseDouble(text);
if (v < min || v > max) {
findings.accept(Severity.ERROR, where + " " + what + " " + v + " is outside [" + min + ", " + max + "]");
return 1;
}
return 0;
} catch (NumberFormatException e) {
findings.accept(Severity.ERROR, where + " " + what + " is not a number: '" + text + "'");
return 1;
}
}
}
The goal itself extends org.apache.maven.plugin.AbstractMojo, which supplies getLog() and the Mojo interface, and
implements execute():
package io.tidepool.maven;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import javax.inject.Inject;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.rtinfo.RuntimeInformation;
/**
* Validates tidepool station files: five columns per row, latitude within [-90, 90], longitude within
* [-180, 180], no duplicate ids, enough constituents and every required id present.
*
* @since 1.0.0
*/
@Mojo(name = "check-stations", defaultPhase = LifecyclePhase.VALIDATE, threadSafe = true)
public class CheckStationsMojo extends AbstractMojo {
/** The station file to check. */
@Parameter(property = "tidepool.file", defaultValue = "${project.basedir}/data/stations.csv")
private File file;
/** More station files, checked together with {@code file}. */
@Parameter
private List<File> files;
/** Station ids that must be present in the checked files. */
@Parameter(property = "tidepool.required")
private List<String> required;
/** Every station needs at least this many constituents. */
@Parameter(property = "tidepool.minConstituents", defaultValue = "1")
private int minConstituents;
/** Fail the build when a problem is found, or only warn. */
@Parameter(property = "tidepool.failOnError", defaultValue = "true")
private boolean failOnError;
/** {@code SUMMARY} or {@code FULL}; {@code FULL} also lists the station ids that were seen. */
@Parameter(property = "tidepool.report", defaultValue = "SUMMARY")
private ReportMode report;
/** Name of a project property that receives the number of problems. */
@Parameter(property = "tidepool.resultProperty")
private String resultProperty;
/** Skip the check. */
@Parameter(property = "tidepool.skip", defaultValue = "false")
private boolean skip;
private final MavenProject project;
private final RuntimeInformation runtime;
@Inject
public CheckStationsMojo(MavenProject project, RuntimeInformation runtime) {
this.project = project;
this.runtime = runtime;
}
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
if (skip) {
getLog().info("Skipping the station check");
return;
}
getLog().debug("Running on Maven " + runtime.getMavenVersion());
List<File> inputs = new ArrayList<>();
if (file != null) {
inputs.add(file);
}
if (files != null) {
inputs.addAll(files);
}
if (inputs.isEmpty()) {
throw new MojoExecutionException("Set file or files");
}
StationChecker checker = new StationChecker(minConstituents,
(severity, text) -> {
if (severity == StationChecker.Severity.ERROR) {
getLog().error(text);
} else {
getLog().warn(text);
}
},
(where, row) -> getLog().debug("Row " + where + " -> " + row));
int problems = 0;
Set<String> seen = new TreeSet<>();
for (File input : inputs) {
getLog().debug("Checking " + input.getName());
try {
problems += checker.check(input, seen);
} catch (IOException e) {
throw new MojoExecutionException(
"Cannot read " + input + " (" + e.getClass().getSimpleName() + ")", e);
}
}
if (required != null) {
for (String id : required) {
if (!seen.contains(id)) {
getLog().error("Required station " + id + " is missing");
problems++;
}
}
}
if (report == ReportMode.FULL) {
getLog().info("Stations seen: " + String.join(",", seen));
}
if (resultProperty != null) {
project.getProperties().setProperty(resultProperty, Integer.toString(problems));
}
if (problems == 0) {
getLog().info(inputs.size() + " file(s), " + seen.size() + " station(s): OK");
} else if (failOnError) {
throw new MojoFailureException(problems + " problem(s) found in station data");
} else {
getLog().warn(problems + " problem(s) found in station data");
}
}
}
package io.tidepool.maven;
/** Values of the {@code report} parameter. */
public enum ReportMode {
/** Only the verdict. */
SUMMARY,
/** The verdict and the ids of the stations that were seen. */
FULL
}
The requirements on a Mojo are few: a public void execute() that throws only MojoExecutionException,
MojoFailureException or a runtime exception, and one field per parameter. Maven fills the fields before it calls execute().
Everything else is metadata, and the metadata comes from the annotations.
The @Mojo annotation
@Mojo(name = "check-stations") is the minimal form, and the only attribute without a default. The class Javadoc becomes the
description of the goal that tidepool:help and the generated site show, and a @since tag becomes the since entry in the
descriptor; the official sample also shows @deprecated on classes and fields. These are all the attributes of @Mojo in
maven-plugin-annotations 3.16.0, as listed by javap and the official page:
| Attribute | Default | Meaning |
|---|---|---|
|
required |
The goal name, |
|
|
The phase the goal is bound to when a POM lists it in an execution without a |
|
|
Which dependencies Maven must resolve, and download, before the goal runs: |
|
|
Like the previous one but only builds the dependency tree, without downloading the files (Maven 3.0 and later). |
|
|
|
|
|
|
|
|
|
|
|
Declares that the goal can run in parallel builds ( |
|
|
|
|
the basic configurator |
The role hint of a custom component that sets the parameters. Almost never used. |
|
various |
Unsupported since Maven 3.0 according to the official annotations page; they remain in the annotation for old plugins. |
LifecyclePhase has one constant per standard phase of the default, clean and site lifecycles (VALIDATE, INITIALIZE,
GENERATE_SOURCES and so on up to DEPLOY, PRE_CLEAN, CLEAN, POST_CLEAN, PRE_SITE, SITE, POST_SITE, SITE_DEPLOY)
plus NONE.
The @Parameter annotation
Every field with @Parameter is a parameter of the goal, named after the field (or after the name attribute, which is needed
when a setter is annotated and its name does not match). The attributes, again from the official page and javap:
| Attribute | Meaning |
|---|---|
|
The user property that sets the parameter from the command line, |
|
The value when nothing else sets the parameter. It may contain expressions such as |
|
|
|
|
|
A second name accepted in |
|
The parameter name when it differs from the field name; used mainly on setter methods. |
Annotations on a public setter work as well as on a field, since Plugin Tools 3.7.0, which makes a Mojo usable outside of Maven.
The Javadoc of the field is the description in the help output and the generated site, so it is worth writing in full
sentences. This is how tidepool:help shows the parameters of the finished goal:
[INFO] --- tidepool:1.0.0-SNAPSHOT:help (default-cli) @ tidepool-data ---
[INFO] Tidepool Maven Plugin 1.0.0-SNAPSHOT
Validates tidepool station files.
tidepool:check-stations
Validates tidepool station files: five columns per row, latitude within [-90,
90], longitude within [-180, 180], no duplicate ids, enough constituents and
every required id present.
Available parameters:
failOnError (Default: true)
Fail the build when a problem is found, or only warn.
User property: tidepool.failOnError
file (Default: ${project.basedir}/data/stations.csv)
The station file to check.
User property: tidepool.file
files
More station files, checked together with file.
minConstituents (Default: 1)
Every station needs at least this many constituents.
User property: tidepool.minConstituents
report (Default: SUMMARY)
SUMMARY or FULL; FULL also lists the station ids that were seen.
User property: tidepool.report
required
Station ids that must be present in the checked files.
User property: tidepool.required
resultProperty
Name of a project property that receives the number of problems.
User property: tidepool.resultProperty
skip (Default: false)
Skip the check.
User property: tidepool.skip
[INFO] BUILD SUCCESS
The descriptor entry behind that output, in META-INF/maven/plugin.xml of the built JAR (one parameter and the default
configuration shown; the file lists every parameter):
<mojo>
<goal>check-stations</goal>
<description>Validates tidepool station files: five columns per row, latitude within [-90, 90], longitude within [-180, 180], no duplicate ids, enough constituents and every required id present.</description>
<requiresDirectInvocation>false</requiresDirectInvocation>
<requiresProject>true</requiresProject>
<requiresReports>false</requiresReports>
<aggregator>false</aggregator>
<requiresOnline>false</requiresOnline>
<inheritedByDefault>true</inheritedByDefault>
<phase>validate</phase>
<implementation>io.tidepool.maven.CheckStationsMojo</implementation>
<language>java</language>
<instantiationStrategy>per-lookup</instantiationStrategy>
<executionStrategy>once-per-session</executionStrategy>
<since>1.0.0</since>
<threadSafe>true</threadSafe>
<parameters>
<!-- the other parameters are left out of this listing -->
<parameter>
<name>minConstituents</name>
<type>int</type>
<required>false</required>
<editable>true</editable>
<description>Every station needs at least this many constituents.</description>
</parameter>
</parameters>
<configuration>
<!-- one entry per parameter, for example: -->
<minConstituents implementation="int" default-value="1">${tidepool.minConstituents}</minConstituents>
</configuration>
</mojo>
Types and conversion
Maven converts the text of a <configuration> element, or of a -D property, to the type of the field. The rules are in the
configuration guide; these are the types that check-stations uses, and what the runs below show:
| Field type | Conversion |
|---|---|
|
The wrapper’s own parser. A bad value fails before |
|
|
an |
|
|
The text as it is, after expression evaluation. |
|
One child element per item, whatever its name; the element name does not matter. Since Maven 3.3.9 a comma-separated text also
works, and that is what |
The guide documents more: Date (four fixed patterns), URL, URI, Class, StringBuilder, Set (a HashSet, or a TreeSet for
a SortedSet), Map<String,String> (child element name as key), Properties, and complex objects (a nested element maps to a
class in the Mojo’s package, or to the class named by an implementation attribute). A complex object is the closest analogue of an
Ant nested element, and the right choice when a goal needs a structured option such as
<station><id>SF</id><minConstituents>3</minConstituents></station>; this example uses a list of plain strings instead.
Running the goal
The plugin is built and installed into a local repository of its own with mvn install -Dmaven.repo.local=..., then used by a
consumer project. The consumer is a minimal project with the packaging pom and the three data files:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-data</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<build>
<plugins>
<plugin>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-maven-plugin</artifactId>
<version>1.0.0-SNAPSHOT</version>
<executions>
<execution>
<id>check</id>
<goals>
<goal>check-stations</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
The execution has no <phase>, so Maven uses defaultPhase, which is validate. A plain mvn validate runs the goal with the
defaults. (pom-configured.xml, pom-result.xml and the other POMs of this section are copies of this one with a different
<plugin> part.)
$ mvn validate
[INFO] --- tidepool:1.0.0-SNAPSHOT:check-stations (check) @ tidepool-data ---
[INFO] 1 file(s), 3 station(s): OK
[INFO] BUILD SUCCESS
The defaults are data/stations.csv with at least one constituent per station. The same goal on the broken file, with the
options of the Ant example given as user properties (bad.csv has a latitude of 137.8, a non-numeric latitude, a duplicate id and a
station without constituents):
$ mvn -Dtidepool.file=data/bad.csv -Dtidepool.minConstituents=2 -Dtidepool.required=SF,HNL validate
[INFO] --- tidepool:1.0.0-SNAPSHOT:check-stations (check) @ tidepool-data ---
[ERROR] bad.csv:3 latitude 137.8 is outside [-90.0, 90.0]
[ERROR] bad.csv:4 latitude is not a number: 'north'
[WARNING] bad.csv:4 has 1 constituent(s), at least 2 required
[ERROR] bad.csv:5 duplicate station id SF
[WARNING] bad.csv:5 has 0 constituent(s), at least 2 required
[ERROR] Required station HNL is missing
[ERROR] BUILD FAILURE
[ERROR] Failed to execute goal io.tidepool:tidepool-maven-plugin:1.0.0-SNAPSHOT:check-stations (check) on project tidepool-data: 6 problem(s) found in station data -> [Help 1]
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException
Lines at error level are the problems, lines at warning level are the missing constituents, and the build fails with one summary
that carries the name of the exception in its help link. The link is the useful part: it says whether the Mojo threw a
MojoFailureException, a MojoExecutionException or whether the configuration failed before the Mojo ran.
Source: Guide to Developing Java Plugins: executing your first Mojo and attaching the Mojo to the build lifecycle
The list, the result property and the skip
files takes more files, and resultProperty with failOnError set to false turns the check into a counter that other plugins
can read. This POM (pom-result.xml, same project) checks stations.csv plus more.csv, which repeats the station SF, and prints
the property with the Antrun plugin, which evaluates ${problems} at the time it runs:
<build>
<plugins>
<plugin>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-maven-plugin</artifactId>
<version>1.0.0-SNAPSHOT</version>
<executions>
<execution>
<id>check</id>
<goals>
<goal>check-stations</goal>
</goals>
<configuration>
<failOnError>false</failOnError>
<resultProperty>problems</resultProperty>
<files>
<file>${project.basedir}/data/more.csv</file>
</files>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<id>show</id>
<phase>validate</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target>
<echo>problems=${problems}</echo>
</target>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
$ mvn -f pom-result.xml validate
[INFO] --- tidepool:1.0.0-SNAPSHOT:check-stations (check) @ tidepool-data ---
[ERROR] more.csv:3 duplicate station id SF
[WARNING] 1 problem(s) found in station data
[INFO] --- antrun:3.2.0:run (show) @ tidepool-data ---
[INFO] Executing tasks
[WARNING] [echo] problems=1
[INFO] Executed tasks
[INFO] BUILD SUCCESS
The check fails nothing here: it logs the problem at warning level, and the property reaches the next plugin because
project.getProperties().setProperty(...) changes the model that expression evaluation reads. report=FULL adds the station ids,
and skip ends the goal at its first line:
$ mvn -Dtidepool.report=FULL validate
[INFO] --- tidepool:1.0.0-SNAPSHOT:check-stations (check) @ tidepool-data ---
[INFO] Stations seen: BOS,HNL,SF
[INFO] 1 file(s), 3 station(s): OK
[INFO] BUILD SUCCESS
$ mvn -Dtidepool.skip validate
[INFO] --- tidepool:1.0.0-SNAPSHOT:check-stations (check) @ tidepool-data ---
[INFO] Skipping the station check
[INFO] BUILD SUCCESS
Parameters that do not convert
Conversion errors are reported by Maven, before execute() is reached, with the parameter and the reason. The enum is
case-sensitive, unlike the attribute of the Ant task:
$ mvn -Dtidepool.report=full validate
[INFO] --- tidepool:1.0.0-SNAPSHOT:check-stations (check) @ tidepool-data ---
[ERROR] BUILD FAILURE
[ERROR] Failed to execute goal io.tidepool:tidepool-maven-plugin:1.0.0-SNAPSHOT:check-stations (check) on project tidepool-data: Unable to parse configuration of mojo io.tidepool:tidepool-maven-pl [...]
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/PluginConfigurationException
$ mvn -Dtidepool.minConstituents=two validate
[INFO] --- tidepool:1.0.0-SNAPSHOT:check-stations (check) @ tidepool-data ---
[ERROR] BUILD FAILURE
[ERROR] Failed to execute goal io.tidepool:tidepool-maven-plugin:1.0.0-SNAPSHOT:check-stations (check) on project tidepool-data: Unable to parse configuration of mojo io.tidepool:tidepool-maven-pl [...]
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/PluginConfigurationException
A file that cannot be read is the Mojo’s own error, raised as a MojoExecutionException with the cause kept:
$ mvn -Dtidepool.file=data/nowhere.csv validate
[INFO] --- tidepool:1.0.0-SNAPSHOT:check-stations (check) @ tidepool-data ---
[ERROR] BUILD FAILURE
[ERROR] Failed to execute goal io.tidepool:tidepool-maven-plugin:1.0.0-SNAPSHOT:check-stations (check) on project tidepool-data: Cannot read /work/writing-plugins/consumer/data/nowhere.csv (NoSuch [...]
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoExecutionException
Where a value comes from
A parameter gets its value from the first of these that applies, in this order: the <configuration> of the execution or the
plugin in the POM (including inherited and pluginManagement configuration), then the user property named by property, then
defaultValue. The order surprises people who expect -D to win. pom-configured.xml sets minConstituents to 2 and the
required ids in the POM:
<plugin>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-maven-plugin</artifactId>
<version>1.0.0-SNAPSHOT</version>
<configuration>
<minConstituents>2</minConstituents>
<required>
<id>SF</id>
<id>HNL</id>
</required>
</configuration>
<executions>
<execution>
<id>check</id>
<goals>
<goal>check-stations</goal>
</goals>
</execution>
</executions>
</plugin>
The command line asks for 3, and the debug output of the configurator shows which one the Mojo received (the (f) lines are
the fields it set, one per parameter that has a value):
$ mvn -X -Dtidepool.minConstituents=3 -f pom-configured.xml validate
[DEBUG] Configuring mojo execution 'io.tidepool:tidepool-maven-plugin:1.0.0-SNAPSHOT:check-stations:check' with basic configurator -->
[DEBUG] (f) failOnError = true
[DEBUG] (f) file = /work/writing-plugins/consumer/data/stations.csv
[DEBUG] (f) minConstituents = 2
[DEBUG] (f) report = SUMMARY
[DEBUG] (f) required = [SF, HNL]
[DEBUG] (f) skip = false
[DEBUG] -- end configuration --
[DEBUG] Running on Maven 3.10.0
The POM’s 2 won. To let users override a value from the command line, leave it out of the POM, or put it in a POM property and
refer to it from <configuration> with ${...}. The same output shows that the file default
${project.basedir}/data/stations.csv was evaluated to an absolute path.
Build objects and injection
A Mojo usually needs more than its own parameters: the project, the session, the plugin’s own descriptor, the Maven version,
the settings. Maven offers them in three ways; the first is used by check-stations.
Constructor injection with JSR-330 is the form to use in new code. The Mojo has one public constructor annotated with
@Inject (javax.inject.Inject on Maven 3), and Maven (through its Eclipse Sisu container) passes the objects in. The
official annotations page lists the injectable build objects: MavenSession, MavenProject, MojoExecution, and any component of
Maven or of another extension, such as RuntimeInformation used here to log the Maven version. A Mojo with a constructor has
final fields and can be created in a unit test with plain new, or with Mockito mocks, without a container. No annotation
processing is needed: the descriptor does not list injected constructor arguments, as the plugin.xml excerpt above shows.
Pseudo-parameters are the older way: a read-only parameter whose default is an expression. @Parameter(defaultValue = "${project}", readonly = true)
injects the MavenProject; ${session}, ${mojoExecution}, ${plugin} (the plugin descriptor), ${settings},
${reactorProjects} and the ${project.*} paths work the same. They appear in the descriptor as parameters, and the official
page now recommends constructor injection for plugins that target Maven 3.2.5 or later.
Plexus @Component fields are the oldest form, and the one to leave behind. @Component from maven-plugin-annotations injects a
Plexus component by role; the build above warns about it, for a field of the report base class: "Mojo station-report uses Plexus
Component requirements (@Component annotation) for fields … Use JSR 330 annotations to inject dependencies instead." Maven 4
removed Plexus dependency injection as an API (Sisu still ships for compatibility), and the Maven 4 notes say that a plugin that still
relies on it must be updated to JSR-330.
The debug line written by the constructor-injected RuntimeInformation proves that the same JAR runs on all three releases:
$ mvn -X validate (Maven 3.9.16)
[DEBUG] Running on Maven 3.9.16
$ mvn -X validate (Maven 3.10.0)
[DEBUG] Running on Maven 3.10.0
$ mvn -X validate (Maven 4.0.0-rc-7)
[DEBUG] Running on Maven 4.0.0-rc-7
Logging and failing
getLog() returns the Maven logger with four levels: debug (shown with -X), info, warn and error. Since Maven 3.1.0
the logging is backed by SLF4J, and the logger is the way to let -q, -X and the logging configuration of the Maven
installation decide what the user sees. Ant’s five priorities map onto these four:
errors to error, warnings to warn, the verdict to info, and both the per-file and the per-row detail to debug, because
Maven has no verbose level between info and debug. The run with -X above shows the rows that check-stations writes at
debug level.
A Mojo ends a build by throwing:
-
MojoFailureException: the build failed for a reason that the user can fix, an expected failure such as a failed assertion or a compilation error.check-stationsthrows it when it found problems. -
MojoExecutionException: an unexpected problem while the goal ran, such as an unreadable file; the constructor takes the cause, which-eprints.check-stationsthrows it for an I/O error and for missing input. -
The Mojo API specification also allows a
RuntimeException. It ends the build in the same way, but it signals a bug in the plugin rather than a problem that the user can fix.
The Javadoc of the two classes says that throwing a failure shows "BUILD FAILURE" and an execution exception shows "BUILD ERROR".
That is not what Maven 3.10.0 prints: the output of the missing file above shows "BUILD FAILURE" for the
MojoExecutionException as well. The difference is only the name in the help link at the end of the message
(MojoFailureException against MojoExecutionException, or PluginConfigurationException for the conversion errors), which
tools such as IDEs and CI scripts can use. The message of the exception is what users read, so name the file, the value and the
reason; the build adds the goal, the execution id and the project. On Maven 4 the exceptions of the new API are different; see
the last section.
Source: MojoFailureException API and Common bugs and pitfalls
Binding a goal to the lifecycle
A goal runs when a POM execution binds it to a phase and Maven reaches that phase, or when it is named on the command line. The
phase of an execution is the <phase> of the POM, or, when that is missing, the defaultPhase of the @Mojo. The consumer POM
above relies on defaultPhase = VALIDATE. A goal run from the command line gets the execution id default-cli:
$ mvn tidepool:check-stations
[INFO] --- tidepool:1.0.0-SNAPSHOT:check-stations (default-cli) @ tidepool-data ---
[INFO] 1 file(s), 3 station(s): OK
[INFO] BUILD SUCCESS
A goal with neither a <phase> nor a defaultPhase is the classic mistake. list-stations has no defaultPhase, so a POM that
lists it without a <phase> binds it nowhere, and Maven says nothing at all. pom-phases.xml lists the goal twice, once without a phase
(execution list) and once in verify (execution late); the output of mvn verify has no trace of the execution list:
$ mvn -f pom-phases.xml verify
[INFO] --- tidepool:1.0.0-SNAPSHOT:check-stations (check) @ tidepool-data ---
[INFO] 1 file(s), 3 station(s): OK
[INFO] >>> tidepool:1.0.0-SNAPSHOT:list-stations (late) > :check-stations @ tidepool-data >>>
[INFO] --- tidepool:1.0.0-SNAPSHOT:check-stations (check-stations) @ tidepool-data ---
[INFO] 1 file(s), 3 station(s): OK
[INFO] <<< tidepool:1.0.0-SNAPSHOT:list-stations (late) < :check-stations @ tidepool-data <<<
[INFO] --- tidepool:1.0.0-SNAPSHOT:list-stations (late) @ tidepool-data ---
[INFO] SF San Francisco
[INFO] BOS Boston
[INFO] HNL Honolulu
[INFO] BUILD SUCCESS
Because the phase decides the order of the executions, a goal that changes files for others should be bound early, one that
checks results late, and an aggregating goal (aggregator = true) in a phase of the parent project. Several executions of the same goal
with different configuration, ids, <inherited> and the order of executions within one phase are explained on
Plugins & Their Configuration.
Forking a lifecycle with @Execute
A goal can ask Maven to run something else first with @Execute, which takes a goal, or a phase (or a customPhase), and
a lifecycle id. list-stations lists the stations of a file, but only after check-stations has accepted it:
package io.tidepool.maven;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.List;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Execute;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
/**
* Lists the station ids of a file, after the {@code check-stations} goal accepted it.
*
* @since 1.0.0
*/
@Mojo(name = "list-stations", threadSafe = true)
@Execute(goal = "check-stations")
public class ListStationsMojo extends AbstractMojo {
/** The station file to list. */
@Parameter(property = "tidepool.file", defaultValue = "${project.basedir}/data/stations.csv")
private File file;
@Override
public void execute() throws MojoExecutionException {
try {
List<String> lines = Files.readAllLines(file.toPath(), StandardCharsets.UTF_8);
for (String line : lines.subList(1, lines.size())) {
String[] f = line.split(",", -1);
getLog().info(f[0] + " " + f[1]);
}
} catch (IOException e) {
throw new MojoExecutionException("Cannot read " + file, e);
}
}
}
Maven brackets the forked execution in the log with >>> and <<< lines and then runs the goal. The forked execution gets the id
of the forked goal, check-stations, and not the id of the caller:
$ mvn tidepool:list-stations
[INFO] >>> tidepool:1.0.0-SNAPSHOT:list-stations (default-cli) > :check-stations @ tidepool-data >>>
[INFO] --- tidepool:1.0.0-SNAPSHOT:check-stations (check-stations) @ tidepool-data ---
[INFO] 1 file(s), 3 station(s): OK
[INFO] <<< tidepool:1.0.0-SNAPSHOT:list-stations (default-cli) < :check-stations @ tidepool-data <<<
[INFO] --- tidepool:1.0.0-SNAPSHOT:list-stations (default-cli) @ tidepool-data ---
[INFO] SF San Francisco
[INFO] BOS Boston
[INFO] HNL Honolulu
[INFO] BUILD SUCCESS
$ mvn tidepool:list-stations -Dtidepool.file=data/bad.csv
[INFO] >>> tidepool:1.0.0-SNAPSHOT:list-stations (default-cli) > :check-stations @ tidepool-data >>>
[INFO] --- tidepool:1.0.0-SNAPSHOT:check-stations (check-stations) @ tidepool-data ---
[ERROR] bad.csv:3 latitude 137.8 is outside [-90.0, 90.0]
[ERROR] bad.csv:4 latitude is not a number: 'north'
[ERROR] bad.csv:5 duplicate station id SF
[WARNING] bad.csv:5 has 0 constituent(s), at least 1 required
[ERROR] BUILD FAILURE
[ERROR] Failed to execute goal io.tidepool:tidepool-maven-plugin:1.0.0-SNAPSHOT:check-stations (check-stations) on project tidepool-data: 4 problem(s) found in station data -> [Help 1]
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException
On the bad file the build stops in the forked execution and the list-stations goal never runs. The forked execution of the same
goal also ran in the late execution of pom-phases.xml above, which shows that the check ran twice there: a forked lifecycle
repeats work that the main lifecycle did already, so @Execute is meant for goals that are run alone (reports and
mvn tidepool:... calls) and not for goals that sit in a normal build. With phase instead of goal, the lifecycle up to that
phase is forked instead of a single goal.
The descriptor, the help goal and the prefix
plugin.xml
The descriptor goal wrote target/classes/META-INF/maven/plugin.xml; the JAR contains it, the classes, the generated help
Mojo and its data:
$ unzip -l target/tidepool-maven-plugin-1.0.0-SNAPSHOT.jar (only the names)
META-INF/MANIFEST.MF
META-INF/maven/io.tidepool/tidepool-maven-plugin/plugin-help.xml
META-INF/maven/plugin.xml
io/tidepool/maven/CheckStationsMojo.class
io/tidepool/maven/ListStationsMojo.class
io/tidepool/maven/ReportMode.class
io/tidepool/maven/StationChecker$Severity.class
io/tidepool/maven/StationChecker.class
io/tidepool/maven/StationReportMojo.class
io/tidepool/tidepool_maven_plugin/HelpMojo.class
META-INF/maven/io.tidepool/tidepool-maven-plugin/pom.xml
META-INF/maven/io.tidepool/tidepool-maven-plugin/pom.properties
The header of the descriptor, which is where the goal prefix and the minimum versions are:
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated by maven-plugin-tools 3.16-->
<plugin>
<name>Tidepool Maven Plugin</name>
<description>Validates tidepool station files.</description>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-maven-plugin</artifactId>
<version>1.0.0-SNAPSHOT</version>
<goalPrefix>tidepool</goalPrefix>
<isolatedRealm>false</isolatedRealm>
<inheritedByDefault>true</inheritedByDefault>
<requiredJavaVersion>21</requiredJavaVersion>
<requiredMavenVersion>3.9.16</requiredMavenVersion>
<mojos>
...
The java-annotations extractor is the default; two more are included in Plugin Tools and can be selected with the
<extractors> parameter of plugin:descriptor: the java-javadoc extractor for the old Javadoc tags and extractors for Ant and
BeanShell Mojos (the plugin build above printed a line for each). Only the annotation extractor is used here.
The generated help goal
Every official plugin has a help goal that prints its goals and parameters. The helpmojo goal of the Maven Plugin Plugin
generates it: it reads the same descriptor data from plugin-help.xml in the JAR (see the listing above) and generates a source file
io/tidepool/tidepool_maven_plugin/HelpMojo.java in target/generated-sources/plugin, compiled into the JAR with the
other classes. Without arguments it lists the goals, with -Ddetail it shows the parameters of one goal, and
that is what the output of the parameters section came from. The overview:
$ mvn tidepool:help
[INFO] --- tidepool:1.0.0-SNAPSHOT:help (default-cli) @ tidepool-data ---
[INFO] Tidepool Maven Plugin 1.0.0-SNAPSHOT
Validates tidepool station files.
This plugin has 4 goals:
tidepool:check-stations
Validates tidepool station files: five columns per row, latitude within [-90,
90], longitude within [-180, 180], no duplicate ids, enough constituents and
every required id present.
tidepool:help
Display help information on tidepool-maven-plugin.
Call mvn tidepool:help -Ddetail=true -Dgoal=<goal-name> to display parameter
details.
tidepool:list-stations
Lists the station ids of a file, after the check-stations goal accepted it.
tidepool:station-report
Writes a one-page report listing the stations of a file; the page becomes part
of the project site.
[INFO] BUILD SUCCESS
The Maven Help Plugin can describe any plugin from its coordinates in a similar way (mvn help:describe -Dplugin=...); the
help goal inside the plugin is the one that ships with it, and is part of the user experience of a good plugin.
The goal prefix and pluginGroups
mvn tidepool:check-stations works only if Maven can map the prefix tidepool to a plugin. The resolution order is: the plugins
declared in the POM (build and reporting, with their descriptors), then, for each group in pluginGroups, the file
maven-metadata.xml of that group in the repositories and maven-metadata-local.xml in the local repository. The default groups are
org.apache.maven.plugins and org.codehaus.mojo. When the prefix is not derived from the artifactId, or the group is not on the
list, the goal must be written with its full coordinates. A project that declares no plugin is the test: without any setting Maven does
not know the prefix, and with the group in a settings file it does (a throwaway settings-pg.xml passed with -s, containing
only the pluginGroup element):
$ mvn tidepool:check-stations
[ERROR] BUILD FAILURE
[ERROR] No plugin found for prefix 'tidepool' in the current project and in the plugin groups [org.apache.maven.plugins, org.codehaus.mojo] available from the repositories [local (/work/writing-pl [...]
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/NoPluginFoundForPrefixException
$ mvn -s settings-pg.xml tidepool:check-stations
[INFO] Looking for compatible SNAPSHOT version of plugin io.tidepool:tidepool-maven-plugin
[INFO] --- tidepool:1.0.0-SNAPSHOT:check-stations (default-cli) @ bare ---
[INFO] 1 file(s), 3 station(s): OK
[INFO] BUILD SUCCESS
The prefix mapping is stored by install and deploy in the metadata file of the group. This is the one that the local
repository received from the two plugins built for this page:
<?xml version="1.0" encoding="UTF-8"?>
<metadata xmlns="http://maven.apache.org/METADATA/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/METADATA/1.1.0 https://maven.apache.org/xsd/repository-metadata-1.1.0.xsd">
<plugins>
<plugin>
<name>Tidepool Maven Plugin</name>
<prefix>tidepool</prefix>
<artifactId>tidepool-maven-plugin</artifactId>
</plugin>
<plugin>
<name>tidepool-maven4-plugin</name>
<prefix>tidepool4</prefix>
<artifactId>tidepool-maven4-plugin</artifactId>
</plugin>
</plugins>
</metadata>
The prefix is generated from the artifactId by removing the "maven" and "plugin" parts that are surrounded by hyphens. An
artifactId that does not follow the convention stops the build, and the fix is the goalPrefix parameter of
maven-plugin-plugin (the Maven 4 example below uses it). With tidepool-plugin-x as the artifactId:
$ mvn clean install
[INFO] --- clean:3.5.0:clean (default-clean) @ tidepool-plugin-x ---
[INFO] --- plugin:3.16.0:helpmojo (help-mojo) @ tidepool-plugin-x ---
[ERROR] BUILD FAILURE
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-plugin-plugin:3.16.0:helpmojo (help-mojo) on project tidepool-plugin-x: You need to specify a goalPrefix as it can not be correctly co [...]
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoExecutionException
Prefix mapping is described in depth in the guide; pluginGroups itself is a setting, covered on
settings.xml & Password Encryption.
Plugin documentation
maven-plugin-report-plugin 3.16.0 produces a page per goal with its parameters. The plugin goes into <reporting>, and the
site build generates the pages. (Calling its report goal directly from the command line failed on this setup with a message
about a null sink factory, which is why the pages are generated through mvn site.)
$ mvn -f pom-site.xml site (in the plugin project)
[INFO] Generating "Plugin Details" report --- maven-plugin-report-plugin:3.16.0:report
[INFO] BUILD SUCCESS
$ ls target/site | grep mojo
check-stations-mojo.html
help-mojo.html
list-stations-mojo.html
station-report-mojo.html
These pages are the "plugin documentation" that the official guide asks for; they also put the Javadoc of the parameters to use, one more reason to write it as prose.
Thread safety
The -T option runs the modules of a reactor in parallel, and several modules can then be inside the same Mojo class at the
same time, each with its own Mojo object (PER_LOOKUP) but possibly sharing static state, files or the plugin class realm.
threadSafe = true is the plugin author’s promise that the goal can run like that. check-stations is thread-safe because it
keeps no static state and writes only to the project it is running for. A Mojo without the flag makes Maven print a box per
module in a parallel build. The same three-module reactor (a parent and two children, each with its own copy of the data) with a
build of the plugin that lacks the flag, run with -T2:
$ mvn -T2 validate (plugin built without threadSafe)
[INFO] Reactor Build Order:
[INFO] tidepool-multi [pom]
[INFO] tidepool-a [pom]
[INFO] tidepool-b [pom]
[INFO] Using the MultiThreadedBuilder implementation with a thread count of 2
[INFO] ---------------------< io.tidepool:tidepool-multi >---------------------
[INFO] Building tidepool-multi 1.0.0-SNAPSHOT [1/3]
[INFO] --------------------------------[ pom ]---------------------------------
[WARNING] *****************************************************************
[WARNING] * Your build is requesting parallel execution, but this *
[WARNING] * project contains the following plugin(s) that have goals not *
[WARNING] * marked as thread-safe to support parallel execution. *
[WARNING] * While this /may/ work fine, please look for plugin updates *
[WARNING] * and/or request plugins be made thread-safe. *
[WARNING] * If reporting an issue, report it against the plugin in *
[WARNING] * question, not against Apache Maven. *
[WARNING] *****************************************************************
[WARNING] The following plugins are not marked as thread-safe in tidepool-multi:
The box is printed again for the other two modules (left out here), and the build passes. With the real plugin the warning is gone, and the log shows the two children being built in parallel:
$ mvn -T2 validate
[INFO] Reactor Build Order:
[INFO] tidepool-multi [pom]
[INFO] tidepool-a [pom]
[INFO] tidepool-b [pom]
[INFO] Using the MultiThreadedBuilder implementation with a thread count of 2
[INFO] --- tidepool:1.0.0-SNAPSHOT:check-stations (check) @ tidepool-multi ---
[INFO] 1 file(s), 3 station(s): OK
[INFO] --- tidepool:1.0.0-SNAPSHOT:check-stations (check) @ tidepool-b ---
[INFO] --- tidepool:1.0.0-SNAPSHOT:check-stations (check) @ tidepool-a ---
[INFO] 1 file(s), 3 station(s): OK
[INFO] 1 file(s), 3 station(s): OK
[INFO] Reactor Summary for tidepool-multi 1.0.0-SNAPSHOT:
[INFO] tidepool-multi ..................................... SUCCESS [ 0.937 s]
[INFO] tidepool-a ......................................... SUCCESS [ 0.002 s]
[INFO] tidepool-b ......................................... SUCCESS [ 0.002 s]
[INFO] BUILD SUCCESS
The order of tidepool-a and tidepool-b differs between the two outputs because the two modules run at the same time. The
parallel options are covered on
Build Performance; the wiki page on
parallel builds says that Maven warns about plugins that are not marked thread-safe.
A report goal
A plugin can contribute pages to the project site. A report Mojo is a goal that also implements
org.apache.maven.reporting.MavenReport; the easy way is to extend AbstractMavenReport from maven-reporting-impl, the
dependency of the POM above. The required methods are getOutputPath() (the page name; getOutputName() is deprecated in
favour of it but still abstract, so it delegates), getName(Locale), getDescription(Locale) and executeReport(Locale), which
writes the page through the Doxia sink:
package io.tidepool.maven;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.List;
import java.util.Locale;
import org.apache.maven.doxia.sink.Sink;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.reporting.AbstractMavenReport;
import org.apache.maven.reporting.MavenReportException;
/**
* Writes a one-page report listing the stations of a file; the page becomes part of the project site.
*
* @since 1.0.0
*/
@Mojo(name = "station-report", defaultPhase = LifecyclePhase.SITE, threadSafe = true)
public class StationReportMojo extends AbstractMavenReport {
/** The station file to report on. */
@Parameter(property = "tidepool.file", defaultValue = "${project.basedir}/data/stations.csv")
private File file;
@Override
public String getOutputPath() {
return "station-report";
}
/**
* @deprecated use {@link #getOutputPath()} instead
*/
@Override
@Deprecated
public String getOutputName() {
return getOutputPath();
}
@Override
public String getName(Locale locale) {
return "Tidepool Stations";
}
@Override
public String getDescription(Locale locale) {
return "The tide stations known to this project.";
}
@Override
protected void executeReport(Locale locale) throws MavenReportException {
List<String> lines;
try {
lines = Files.readAllLines(file.toPath(), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new MavenReportException("Cannot read " + file, e);
}
Sink sink = getSink();
sink.head();
sink.title();
sink.text(getName(locale));
sink.title_();
sink.head_();
sink.body();
sink.section1();
sink.sectionTitle1();
sink.text(getName(locale));
sink.sectionTitle1_();
sink.table();
sink.tableRows(new int[] {Sink.JUSTIFY_LEFT, Sink.JUSTIFY_LEFT, Sink.JUSTIFY_RIGHT, Sink.JUSTIFY_RIGHT}, false);
sink.tableRow();
for (String heading : new String[] {"Id", "Name", "Latitude", "Longitude"}) {
sink.tableHeaderCell();
sink.text(heading);
sink.tableHeaderCell_();
}
sink.tableRow_();
for (String line : lines.subList(1, lines.size())) {
String[] f = line.split(",", -1);
sink.tableRow();
for (int i = 0; i < 4; i++) {
sink.tableCell();
sink.text(f[i]);
sink.tableCell_();
}
sink.tableRow_();
}
sink.tableRows_();
sink.table_();
sink.section1_();
sink.body_();
}
}
The goal is a normal goal: it can run from the command line, and Maven renders the page with the skin it uses for the site. It
writes target/reports/station-report.html when called directly:
$ mvn tidepool:station-report
[INFO] --- tidepool:1.0.0-SNAPSHOT:station-report (default-cli) @ tidepool-data ---
[INFO] Rendering content with org.apache.maven.skins:maven-fluido-skin:jar:2.0.0-M9 skin
[INFO] BUILD SUCCESS
A project lists it under <reporting> to get it into mvn site (the site plugin version is pinned in pluginManagement, as
everywhere). The listing of the reporting part, and the lines of the site build that mention the report:
<reporting>
<plugins>
<plugin>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-maven-plugin</artifactId>
<version>1.0.0-SNAPSHOT</version>
<reportSets>
<reportSet>
<reports>
<report>station-report</report>
</reports>
</reportSet>
</reportSets>
</plugin>
</plugins>
</reporting>
$ mvn -f pom-site.xml site (lines about the plugin; the site also generates the default project reports)
[INFO] Configured 1 report for tidepool-maven-plugin:1.0.0-SNAPSHOT: station-report
[INFO] Generating "Tidepool Stations" report --- tidepool-maven-plugin:1.0.0-SNAPSHOT:station-report
The table of the generated station-report.html:
<section>
<h1>Tidepool Stations</h1>
<table class="table table-striped">
<tr class="a">
<th>Id</th>
<th>Name</th>
<th>Latitude</th>
<th>Longitude</th></tr>
<tr class="b">
<td style="text-align: left;">SF</td>
<td style="text-align: left;">San Francisco</td>
<td style="text-align: right;">37.8063</td>
<td style="text-align: right;">-122.4659</td></tr>
<tr class="a">
<td style="text-align: left;">BOS</td>
<td style="text-align: left;">Boston</td>
<td style="text-align: right;">42.3539</td>
<td style="text-align: right;">-71.0503</td></tr>
<tr class="b">
<td style="text-align: left;">HNL</td>
<td style="text-align: left;">Honolulu</td>
<td style="text-align: right;">21.3067</td>
<td style="text-align: right;">-157.8645</td></tr></table></section>
Three things came out of running it. First, sink.table() alone produced no <table> element: in the Doxia 2.0.0 of this build the element is opened by
tableRows(...), which also takes the alignment of the columns, and closed by tableRows_(); the first version of this class
rendered the rows as bare <tr> elements. Second, the descriptor build warns about two things that come from the base class and
not from the code above: the Javadoc link #getOutputName() in AbstractMavenReport cannot be resolved while the plugin tools read
its source, and its fields (container, siteRenderer, siteTool) are Plexus @Component fields, which is the warning
quoted earlier. Third, the base class has not moved to JSR-330 in maven-reporting-impl 4.0.0, which is the reason for the second warning. It
still ran on Maven 4.0.0-rc-7, where the same tidepool:station-report call ended with a successful build (the Sisu compatibility
layer is still shipped):
$ mvn tidepool:station-report (Maven 4.0.0-rc-7)
[INFO] --- tidepool:1.0.0-SNAPSHOT:station-report (default-cli) @ tidepool-data ---
[INFO] Rendering content with org.apache.maven.skins:maven-fluido-skin:jar:2.0.0-M9 skin
[INFO] BUILD SUCCESS
Testing a plugin
The official guide to testing plugins separates the unit test, which checks a Mojo as an isolated unit without a real Maven build, from the functional or integration test, which launches Maven on a project and looks at the result. The advice is to write mostly unit tests and some functional tests. The tidepool plugin has three layers.
Unit tests of the logic
The checks are in StationChecker, free of Maven types, so JUnit alone is enough:
package io.tidepool.maven;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
class StationCheckerTest {
@TempDir
Path dir;
private int check(String csv, int minConstituents, List<String> messages) throws IOException {
Path file = Files.writeString(dir.resolve("stations.csv"), csv);
StationChecker checker = new StationChecker(minConstituents,
(severity, text) -> messages.add(severity + " " + text), (where, row) -> { });
Set<String> seen = new TreeSet<>();
return checker.check(file.toFile(), seen);
}
@Test
void acceptsAValidFile() throws IOException {
List<String> messages = new ArrayList<>();
int problems = check("id,name,latitude,longitude,constituents\n"
+ "SF,San Francisco,37.8,-122.4,M2:0.5:1;S2:0.1:2\n", 2, messages);
assertEquals(0, problems);
assertTrue(messages.isEmpty());
}
@Test
void reportsLatitudeOutOfRangeAndDuplicates() throws IOException {
List<String> messages = new ArrayList<>();
int problems = check("id,name,latitude,longitude,constituents\n"
+ "SF,San Francisco,137.8,-122.4,M2:0.5:1\n"
+ "SF,Again,37.8,-122.4,M2:0.5:1\n", 1, messages);
assertEquals(2, problems);
assertEquals("ERROR stations.csv:2 latitude 137.8 is outside [-90.0, 90.0]", messages.get(0));
assertEquals("ERROR stations.csv:3 duplicate station id SF", messages.get(1));
}
@Test
void warnsAboutTooFewConstituents() throws IOException {
List<String> messages = new ArrayList<>();
int problems = check("id,name,latitude,longitude,constituents\n"
+ "BOS,Boston,42.3,-71.0,M2:1.3:15\n", 2, messages);
assertEquals(1, problems);
assertEquals("WARNING stations.csv:2 has 1 constituent(s), at least 2 required", messages.get(0));
}
}
This layer needs no Maven API at all, runs in milliseconds and is where the many cases go (one per rule). The official page
adds the other plain-JUnit technique: Mojos with constructor injection can be created with new and Mockito mocks for the
injected objects.
The plugin testing harness
The Maven Plugin Testing Harness runs a Mojo inside a minimal Maven container: it reads the plugin descriptor from the
classpath, builds the Mojo with its injected components and sets the parameters. Version 3.4.0 added a JUnit 5 extension in the
package org.apache.maven.api.plugin.testing (@MojoTest, @InjectMojo, @MojoParameter, @Basedir), which the page of the project
describes as having similar functionality to the Maven 4 test support, to ease migration. The older JUnit 3 and 4 classes
(AbstractMojoTestCase and MojoRule) remain in the 3.5.1 JAR for backward compatibility, and the project says that new tests
should use JUnit 5.
The dependency, in the POM above, is maven-plugin-testing-harness at the test scope. A test:
package io.tidepool.maven;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.apache.maven.api.plugin.testing.InjectMojo;
import org.apache.maven.api.plugin.testing.MojoParameter;
import org.apache.maven.api.plugin.testing.MojoTest;
import org.apache.maven.plugin.MojoFailureException;
import org.junit.jupiter.api.Test;
@MojoTest
class CheckStationsMojoTest {
@Test
@InjectMojo(goal = "check-stations")
@MojoParameter(name = "file", value = "src/test/resources/good.csv")
void acceptsTheGoodFile(CheckStationsMojo mojo) {
assertDoesNotThrow(mojo::execute);
}
@Test
@InjectMojo(goal = "check-stations")
@MojoParameter(name = "file", value = "src/test/resources/bad.csv")
@MojoParameter(name = "minConstituents", value = "2")
void rejectsTheBadFile(CheckStationsMojo mojo) {
MojoFailureException e = assertThrows(MojoFailureException.class, mojo::execute);
assertEquals("5 problem(s) found in station data", e.getMessage());
}
}
@InjectMojo(goal = "check-stations") looks the goal up in the descriptor that plugin:descriptor wrote to target/classes, which
is why the descriptor exists before mvn test (the process-classes phase runs first) and why the test would fail on a plugin whose
annotations were wrong. Each @MojoParameter sets one parameter of the Mojo, as if it were in <configuration>; a relative
file such as src/test/resources/good.csv is resolved against the working directory of the test. The constructor-injected
MavenProject and RuntimeInformation were supplied by the extension without any mock in the test. The surefire part of
the build log, on Maven 3.10.0:
[INFO] --- surefire:3.6.0:test (default-test) @ tidepool-maven-plugin ---
[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
[INFO] T E S T S
[INFO] Running io.tidepool.maven.CheckStationsMojoTest
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
WARNING: Final field logger in class org.apache.maven.plugin.testing.MojoLogWrapper has been mutated reflectively by class org.mockito.internal.util.reflection.ReflectionMemberAccessor in unnamed [...]
WARNING: Use --enable-final-field-mutation=ALL-UNNAMED to avoid a warning
WARNING: Mutating final fields will be blocked in a future release unless final field mutation is enabled
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.883 s -- in io.tidepool.maven.CheckStationsMojoTest
[INFO] Running io.tidepool.maven.StationCheckerTest
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.018 s -- in io.tidepool.maven.StationCheckerTest
[INFO] Results:
[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0
The warning lines come from the dependencies of the harness (Mockito, SLF4J) on JDK 26: Mockito mutates a final field
of the harness reflectively, which JDK 26 reports and announces that it will block in a future release. They are harmless for
this page and are the kind of noise that a harness test brings. On Maven 4.0.0-rc-7 the same lines are printed with a
[WARNING] [stderr] prefix.
The test fails the way any JUnit test does when the Mojo misbehaves; the harness tests the Mojo class and its parameters only. Whether the descriptor, the packaging and the real lifecycle work is a job for the next layer.
Integration tests with the Invoker Plugin
The Maven Invoker Plugin runs real Maven builds on small projects kept under src/it and checks the result with a script. It is
what Maven’s own plugins use for their integration tests, and the official testing page points to it for functional tests. The
configuration is the one of the POM above: the IT projects are cloned to target/it, the plugin under test is installed into
a private repository (target/local-repo, so the user’s repository is not polluted), and the verify script of each project is
run after its build.
The three projects of the plugin:
src/it/
good-data/ pom.xml invoker.properties verify.groovy data/stations.csv
bad-data/ pom.xml invoker.properties verify.groovy data/stations.csv
lenient/ pom.xml invoker.properties verify.groovy data/stations.csv
An IT project is a normal POM that uses the plugin under test. The coordinates are written as @project.groupId@,
@project.artifactId@ and @project.version@: the Invoker Plugin replaces tokens enclosed in @...@ in the POMs it clones, so
the project under test always uses the version that was just built (it needs cloneProjectsTo, which the configuration sets):
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>io.tidepool.it</groupId>
<artifactId>bad-data</artifactId>
<version>1.0</version>
<packaging>pom</packaging>
<build>
<plugins>
<plugin>
<groupId>@project.groupId@</groupId>
<artifactId>@project.artifactId@</artifactId>
<version>@project.version@</version>
<executions>
<execution>
<id>check</id>
<goals>
<goal>check-stations</goal>
</goals>
<configuration>
<minConstituents>2</minConstituents>
<required>
<id>HNL</id>
</required>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
invoker.properties says what to run and what to expect. bad-data expects the build to fail, which is the point of the test:
invoker.goals = validate
invoker.buildResult = failure
The post-build script is Groovy (BeanShell works as well). The variable basedir is the cloned project, and the plugin
saves the output of the build in build.log there. A failed assert fails the IT:
def log = new File(basedir, 'build.log').text
assert log.contains('[ERROR] stations.csv:3 latitude 137.8 is outside [-90.0, 90.0]')
assert log.contains('[ERROR] stations.csv:5 duplicate station id SF')
assert log.contains('[ERROR] Required station HNL is missing')
assert log.contains('6 problem(s) found in station data')
The integration test phase of the whole build on Maven 3.10.0 was shown earlier in the build log. These are the lines of the run
goal, which pass each project to a fresh Maven, run the script and print the result. The install goal copies the plugin and every
artifact of its dependency closure (the long Installing ... list of the build log was left out of the listing):
[INFO] --- invoker:3.10.1:install (integration-test) @ tidepool-maven-plugin ---
[INFO] Installing /work/writing-plugins/tidepool-maven-plugin/target/tidepool-maven-plugin-1.0.0-SNAPSHOT.jar to /work/writing-plugins/tidepool-maven-plugin/target/local-repo/io/tidepool/tidepool- [...]
[INFO] Installing /work/writing-plugins/tidepool-maven-plugin/pom.xml to /work/writing-plugins/tidepool-maven-plugin/target/local-repo/io/tidepool/tidepool-maven-plugin/1.0.0-SNAPSHOT/tidepool-mav [...]
[INFO] --- invoker:3.10.1:run (integration-test) @ tidepool-maven-plugin ---
[INFO] Building: good-data/pom.xml
[INFO] Running post-build script: /work/writing-plugins/tidepool-maven-plugin/target/it/good-data/verify.groovy
[INFO] Finished post-build script: /work/writing-plugins/tidepool-maven-plugin/target/it/good-data/verify.groovy
[INFO] good-data/pom.xml ................................ SUCCESS (2.466 s)
[INFO] Building: bad-data/pom.xml
[INFO] Running post-build script: /work/writing-plugins/tidepool-maven-plugin/target/it/bad-data/verify.groovy
[INFO] Finished post-build script: /work/writing-plugins/tidepool-maven-plugin/target/it/bad-data/verify.groovy
[INFO] bad-data/pom.xml ................................. SUCCESS (1.555 s)
[INFO] Building: lenient/pom.xml
[INFO] Running post-build script: /work/writing-plugins/tidepool-maven-plugin/target/it/lenient/verify.groovy
[INFO] Finished post-build script: /work/writing-plugins/tidepool-maven-plugin/target/it/lenient/verify.groovy
[INFO] lenient/pom.xml .................................. SUCCESS (1.482 s)
[INFO] Build Summary:
[INFO] Passed: 3, Failed: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
A script that asserts something false produces a failed IT and a failed build. Changing the expected count in the
lenient script from 4 to 5 and running a single IT with -Dinvoker.test=lenient (the tests of the other two projects are
skipped, and the unit tests with -DskipTests) gives:
$ mvn verify -DskipTests -Dinvoker.test=lenient
[INFO] java-javadoc mojo extractor found 0 mojo descriptor.
[INFO] ant mojo extractor found 0 mojo descriptor.
[INFO] bsh mojo extractor found 0 mojo descriptor.
[WARNING] Mojo station-report uses Plexus Component requirements (@Component annotation) for fields: [container, siteRenderer, siteTool]
[WARNING] Use JSR 330 annotations to inject dependencies instead.
[INFO] --- resources:3.5.0:testResources (default-testResources) @ tidepool-maven-plugin ---
[INFO] Copying 2 resources from src/test/resources to target/test-classes
[INFO] --- compiler:3.16.0:testCompile (default-testCompile) @ tidepool-maven-plugin ---
[INFO] Nothing to compile - all classes are up to date.
[INFO] --- surefire:3.6.0:test (default-test) @ tidepool-maven-plugin ---
[INFO] Tests are skipped.
[INFO] --- jar:3.5.1:jar (default-jar) @ tidepool-maven-plugin ---
[INFO] --- invoker:3.10.1:install (integration-test) @ tidepool-maven-plugin ---
[INFO] Installing /work/writing-plugins/tidepool-maven-plugin/target/tidepool-maven-plugin-1.0.0-SNAPSHOT.jar to /work/writing-plugins/tidepool-maven-plugin/target/local-repo/io/tidepool/tidepool- [...]
[INFO] Installing /work/writing-plugins/tidepool-maven-plugin/pom.xml to /work/writing-plugins/tidepool-maven-plugin/target/local-repo/io/tidepool/tidepool-maven-plugin/1.0.0-SNAPSHOT/tidepool-mav [...]
[INFO] Installing /work/writing-plugins/m2/org/apache/maven/reporting/maven-reporting-impl/4.0.0/maven-reporting-impl-4.0.0.jar to /work/writing-plugins/tidepool-maven-plugin/target/local-repo/org [...]
[INFO] Installing /work/writing-plugins/m2/org/apache/maven/reporting/maven-reporting-impl/4.0.0/maven-reporting-impl-4.0.0.pom to /work/writing-plugins/tidepool-maven-plugin/target/local-repo/org [...]
[INFO] Installing /work/writing-plugins/m2/org/apache/maven/reporting/maven-reporting-api/4.0.0/maven-reporting-api-4.0.0.jar to /work/writing-plugins/tidepool-maven-plugin/target/local-repo/org/a [...]
[INFO] Installing /work/writing-plugins/m2/org/apache/maven/reporting/maven-reporting-api/4.0.0/maven-reporting-api-4.0.0.pom to /work/writing-plugins/tidepool-maven-plugin/target/local-repo/org/a [...]
[INFO] Installing /work/writing-plugins/m2/org/apache/maven/shared/maven-shared-utils/3.4.2/maven-shared-utils-3.4.2.jar to /work/writing-plugins/tidepool-maven-plugin/target/local-repo/org/apache [...]
[INFO] Installing /work/writing-plugins/m2/org/apache/maven/shared/maven-shared-utils/3.4.2/maven-shared-utils-3.4.2.pom to /work/writing-plugins/tidepool-maven-plugin/target/local-repo/org/apache [...]
[INFO] Installing /work/writing-plugins/m2/org/apache/maven/doxia/doxia-sink-api/2.0.0/doxia-sink-api-2.0.0.jar to /work/writing-plugins/tidepool-maven-plugin/target/local-repo/org/apache/maven/do [...]
[INFO] Installing /work/writing-plugins/m2/org/apache/maven/doxia/doxia-sink-api/2.0.0/doxia-sink-api-2.0.0.pom to /work/writing-plugins/tidepool-maven-plugin/target/local-repo/org/apache/maven/do [...]
[INFO] Installing /work/writing-plugins/m2/org/apache/maven/doxia/doxia-site-model/2.0.0/doxia-site-model-2.0.0.jar to /work/writing-plugins/tidepool-maven-plugin/target/local-repo/org/apache/mave [...]
[INFO] Installing /work/writing-plugins/m2/org/apache/maven/doxia/doxia-site-model/2.0.0/doxia-site-model-2.0.0.pom to /work/writing-plugins/tidepool-maven-plugin/target/local-repo/org/apache/mave [...]
[INFO] Installing /work/writing-plugins/m2/org/apache/maven/doxia/doxia-core/2.0.0/doxia-core-2.0.0.jar to /work/writing-plugins/tidepool-maven-plugin/target/local-repo/org/apache/maven/doxia/doxi [...]
[INFO] Installing /work/writing-plugins/m2/org/apache/maven/doxia/doxia-core/2.0.0/doxia-core-2.0.0.pom to /work/writing-plugins/tidepool-maven-plugin/target/local-repo/org/apache/maven/doxia/doxi [...]
(The Groovy stack trace in the log, between the assertion and the summary, was left out.) The project directory under
target/it/lenient keeps build.log, which is the first place to look. -Dinvoker.skip skips the whole run goal, and
-Dinvoker.streamLogs or -Dinvoker.streamLogsOnFailures copy the build log of each IT to the console.
The three layers on three Maven releases
The whole build of the plugin (compile, descriptor, the five unit and harness tests and the three integration tests) was run on Maven 3.9.16, 3.10.0 and 4.0.0-rc-7, with the same result on all of them:
| Maven | Unit and harness tests | Invoker ITs | Differences in the log |
|---|---|---|---|
3.9.16 |
5 run, 0 failures |
3 passed |
none compared with 3.10.0 (apart from times) |
3.10.0 |
5 run, 0 failures |
3 passed |
the reference run above |
4.0.0-rc-7 |
5 run, 0 failures |
3 passed |
error output of the test JVM is prefixed with |
The ITs launch the Maven that runs the build, so the Invoker Plugin also checked the installed plugin on each of those releases. A project that must support a range of Maven versions should run the build of its own plugin on the oldest and the newest of the range, which is how the table above was produced. The developers' advice for Maven 4 is to test plugins on Maven 3.9.x first and to pay attention to the warnings.
Source: Testing Plugins: maven-invoker-plugin, Invoker Plugin: usage and Invoker Plugin: filtering files
The page on testing recommends a third tool for functional tests, maven-verifier, a JUnit-based library that launches Maven on
a project extracted from the test resources; it is what Maven’s own core integration tests use. It was not used here.
The Maven 4 plugin API (experimental)
Maven 4 (release candidate) comes with a new plugin API: an immutable model, interfaces in org.apache.maven.api.* and a
dependency injection framework, Maven DI, with its own annotations. The Maven 4 notes call it experimental in 4.0.0, advise
against using it unless one knows precisely what one is doing, and cite the Maven Compiler Plugin 4.x as a user of it. The
Mojo specification page says that it does not cover this API. A plugin written against it runs only on Maven 4, so a plugin
that must also run on Maven 3 stays on the Maven 3 API, which Maven 4 is expected to run for every Maven 3 plugin that follows the
current practices, as tidepool-maven-plugin did in all the runs above. This section writes the same check against the new API, to show how it differs,
and runs it. Plugin Tools 4.x is still a pre-release (4.0.0-beta-2 for the plugin and 4.0.0-beta-3 for the tools on Maven Central
on 2026-10-16), so versions and details can change.
The POM declares the Maven 4 API instead of the Maven 3 one: maven-api-core for the interfaces and the Mojo annotations,
maven-api-di for @Inject, both at provided; the Maven Plugin Plugin 4.0.0-beta-2 generates a descriptor in the new XML
format. The artifactId does not follow the -maven-plugin convention here, so the prefix has to be set:
<dependencies>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-api-core</artifactId>
<version>${maven.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-api-di</artifactId>
<version>${maven.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-plugin-plugin</artifactId>
<version>4.0.0-beta-2</version>
<configuration>
<goalPrefix>tidepool4</goalPrefix>
</configuration>
</plugin>
</plugins>
...
</build>
package io.tidepool.maven4;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import org.apache.maven.api.Project;
import org.apache.maven.api.di.Inject;
import org.apache.maven.api.plugin.Log;
import org.apache.maven.api.plugin.Mojo;
import org.apache.maven.api.plugin.MojoException;
import org.apache.maven.api.plugin.annotations.Parameter;
/**
* Validates tidepool station files, written against the Maven 4 plugin API.
*
* @since 1.0.0
*/
@org.apache.maven.api.plugin.annotations.Mojo(name = "check-stations", defaultPhase = "validate")
public class CheckStationsMojo implements Mojo {
/** The station file to check. */
@Parameter(property = "tidepool.file", defaultValue = "${project.basedir}/data/stations.csv")
Path file;
/** Station ids that must be present. */
@Parameter(property = "tidepool.required")
List<String> required;
/** Every station needs at least this many constituents. */
@Parameter(property = "tidepool.minConstituents", defaultValue = "1")
int minConstituents;
@Inject
Log log;
@Inject
Project project;
@Override
public void execute() throws MojoException {
log.debug("Project " + project.getArtifactId());
StationChecker checker = new StationChecker(minConstituents,
(severity, text) -> {
if (severity == StationChecker.Severity.ERROR) {
log.error(text);
} else {
log.warn(text);
}
},
(where, row) -> log.debug("Row " + where + " -> " + row));
Set<String> seen = new TreeSet<>();
int problems;
try {
problems = checker.check(file.toFile(), seen);
} catch (IOException e) {
throw new MojoException("Cannot read " + file + " (" + e.getClass().getSimpleName() + ")", e);
}
if (required != null) {
for (String id : required) {
if (!seen.contains(id)) {
log.error("Required station " + id + " is missing");
problems++;
}
}
}
if (problems == 0) {
log.info("1 file(s), " + seen.size() + " station(s): OK");
} else {
throw new MojoException(problems + " problem(s) found in station data");
}
}
}
The differences from the Maven 3 Mojo are all visible in the listing:
| Concern | Maven 3 API | Maven 4 API |
|---|---|---|
the Mojo contract |
extend |
implement |
annotations |
|
|
injection |
|
|
the log |
|
an injected |
the project |
|
the injected, immutable |
paths and failures |
|
|
The build and the descriptor on Maven 4.0.0-rc-7 (the plugin tools write a new namespace, http://maven.apache.org/PLUGIN/2.0.0,
and a different set of element names such as directInvocationOnly and projectRequired):
$ mvn clean install (Maven 4.0.0-rc-7)
[INFO] --- clean:3.5.0:clean (default-clean) @ tidepool-maven4-plugin ---
[INFO] Deleting /work/writing-plugins/tidepool-maven4-plugin/target
[INFO] --- resources:3.5.0:resources (default-resources) @ tidepool-maven4-plugin ---
[INFO] skip non existing resourceDirectory /work/writing-plugins/tidepool-maven4-plugin/src/main/resources
[INFO] --- compiler:3.16.0:compile (default-compile) @ tidepool-maven4-plugin ---
[INFO] Recompiling the module because of changed source code.
[INFO] Compiling 2 source files with javac [debug release 21] to target/classes
[INFO] --- plugin:4.0.0-beta-2:descriptor (default-descriptor) @ tidepool-maven4-plugin ---
[INFO] Using 'UTF-8' encoding to read mojo source files.
[INFO] java-annotations mojo extractor found 1 mojo descriptor.
[INFO] --- resources:3.5.0:testResources (default-testResources) @ tidepool-maven4-plugin ---
[INFO] skip non existing resourceDirectory /work/writing-plugins/tidepool-maven4-plugin/src/test/resources
[INFO] --- compiler:3.16.0:testCompile (default-testCompile) @ tidepool-maven4-plugin ---
[INFO] No sources to compile
[INFO] --- surefire:3.6.0:test (default-test) @ tidepool-maven4-plugin ---
[INFO] No tests to run.
[INFO] --- jar:3.5.1:jar (default-jar) @ tidepool-maven4-plugin ---
[INFO] --- install:3.2.0:install (default-install) @ tidepool-maven4-plugin ---
[INFO] Installing /work/writing-plugins/tidepool-maven4-plugin/target/tidepool-maven4-plugin-1.0.0-SNAPSHOT.jar to /work/writing-plugins/m2/io/tidepool/tidepool-maven4-plugin/1.0.0-SNAPSHOT/tidepo [...]
[INFO] Installing /work/writing-plugins/tidepool-maven4-plugin/pom.xml to /work/writing-plugins/m2/io/tidepool/tidepool-maven4-plugin/1.0.0-SNAPSHOT/tidepool-maven4-plugin-1.0.0-SNAPSHOT-build.pom
[INFO] Installing /work/writing-plugins/tidepool-maven4-plugin/target/consumer-14772637952008824308.pom to /work/writing-plugins/m2/io/tidepool/tidepool-maven4-plugin/1.0.0-SNAPSHOT/tidepool-maven [...]
[INFO] BUILD SUCCESS
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated by maven-plugin-tools 4.0-->
<plugin xmlns="http://maven.apache.org/PLUGIN/2.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:location="http://maven.apache.org/PLUGIN/2.0.0 https://maven.apache.org/xsd/plugin-2.0.0.xsd">
<name>tidepool-maven4-plugin</name>
<description></description>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-maven4-plugin</artifactId>
<version>1.0.0-SNAPSHOT</version>
<goalPrefix>tidepool4</goalPrefix>
<isolatedRealm>false</isolatedRealm>
<inheritedByDefault>true</inheritedByDefault>
<requiredJavaVersion>21</requiredJavaVersion>
<requiredMavenVersion>4.0.0-rc-7</requiredMavenVersion>
<mojos>
<mojo>
<goal>check-stations</goal>
<description>Validates tidepool station files, written against the Maven 4 plugin API.</description>
<directInvocationOnly>false</directInvocationOnly>
<projectRequired>true</projectRequired>
<aggregator>false</aggregator>
<onlineRequired>false</onlineRequired>
<inheritedByDefault>true</inheritedByDefault>
<phase>validate</phase>
<implementation>io.tidepool.maven4.CheckStationsMojo</implementation>
<language>java</language>
<since>1.0.0</since>
<parameters>
<!-- the parameters minConstituents and required are left out of this listing -->
<parameter>
<name>file</name>
<type>java.nio.file.Path</type>
<required>false</required>
<editable>true</editable>
<description>The station file to check.</description>
<expression>${tidepool.file}</expression>
<defaultValue>${project.basedir}/data/stations.csv</defaultValue>
</parameter>
</parameters>
</mojo>
</mojos>
</plugin>
Maven 4 installs the POM of the plugin under the name -build.pom and writes a separate consumer POM as .pom (the model
differences are on
Maven 4 POM Model & Consumer POM). The
goal runs from the command line with its full coordinates, the prefix being tidepool4:
$ mvn io.tidepool:tidepool-maven4-plugin:1.0.0-SNAPSHOT:check-stations (Maven 4.0.0-rc-7)
[INFO] --- tidepool4:1.0.0-SNAPSHOT:check-stations (default-cli) @ tidepool-data ---
[INFO] 1 file(s), 3 station(s): OK
[INFO] BUILD SUCCESS
$ mvn io.tidepool:tidepool-maven4-plugin:1.0.0-SNAPSHOT:check-stations -Dtidepool.file=data/bad.csv -Dtidepool.required=HNL
[INFO] --- tidepool4:1.0.0-SNAPSHOT:check-stations (default-cli) @ tidepool-data ---
[ERROR] bad.csv:3 latitude 137.8 is outside [-90.0, 90.0]
[ERROR] bad.csv:4 latitude is not a number: 'north'
[ERROR] bad.csv:5 duplicate station id SF
[WARNING] bad.csv:5 has 0 constituent(s), at least 1 required
[ERROR] Required station HNL is missing
[ERROR] BUILD FAILURE
[ERROR] Failed to execute goal io.tidepool:tidepool-maven4-plugin:1.0.0-SNAPSHOT:check-stations (default-cli) on project tidepool-data: 5 problem(s) found in station data
Maven 3 cannot run it. The error is the one for a Mojo that the container cannot create; its first lines are shown because the
stack trace that precedes it is Sisu’s (it names @Component cannot contain null values), and the result is the same on
3.9.16 and 3.10.0:
$ mvn io.tidepool:tidepool-maven4-plugin:1.0.0-SNAPSHOT:check-stations (Maven 3.10.0)
[ERROR] BUILD FAILURE
[ERROR] Failed to execute goal io.tidepool:tidepool-maven4-plugin:1.0.0-SNAPSHOT:check-stations (default-cli) on project tidepool-data: Execution default-cli of goal io.tidepool:tidepool-maven4-pl [...]
[ERROR] role: org.apache.maven.plugin.Mojo
[ERROR] roleHint: io.tidepool:tidepool-maven4-plugin:1.0.0-SNAPSHOT:check-stations
[ERROR] -> [Help 1]
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/PluginContainerException
The Maven DI page says the same in words: plugins that use Maven DI are only compatible with Maven 4, and plugins that must also run on Maven 3 should use JSR-330. The Maven 3 plugin of this page runs on both, which is what the Maven 4 notes expect of a plugin written to Maven 3 best practices. The step-by-step migration is on Migrating to Maven 4, and what else changed in Maven 4 is on What’s New in Maven 4.
Classic mistakes and their real messages
Every message below was produced by the plugin of this page with one deliberate change. The ones already shown above (a POM
value that overrides -D, a wrong enum case, a text that is not a number, a Mojo without a default phase, a missing prefix,
a non-thread-safe Mojo in a parallel build and a Maven 4 plugin on Maven 3) are listed in the table at the end of the section.
A project without any Mojo. The build of a plugin whose classes lost their @Mojo annotations fails when the descriptor is
extracted:
$ mvn clean install
[INFO] java-annotations mojo extractor found 0 mojo descriptor.
[INFO] java-javadoc mojo extractor found 0 mojo descriptor.
[INFO] ant mojo extractor found 0 mojo descriptor.
[INFO] bsh mojo extractor found 0 mojo descriptor.
[ERROR] BUILD FAILURE
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-plugin-plugin:3.16.0:descriptor (default-descriptor) on project tidepool-maven-plugin: Error extracting plugin descriptor: 'No mojo de [...]
Packaging jar instead of maven-plugin. The JAR installs without complaint, because it is a valid JAR with no descriptor;
the consumer fails when it first needs the goal:
$ mvn validate (consumer, using the jar-packaged build)
[ERROR] BUILD FAILURE
[ERROR] Failed to parse plugin descriptor for io.tidepool:tidepool-maven-plugin:1.0.0-SNAPSHOT (/work/writing-plugins/errs/m2-jar/io/tidepool/tidepool-maven-plugin/1.0.0-SNAPSHOT/tidepool-maven-pl [...]
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/PluginDescriptorParsingException
A required parameter without a value. A new @Parameter(property = "tidepool.owner", required = true) field with no default fails every
build that does not set it:
$ mvn validate (consumer, after adding the field)
[INFO] --- tidepool:1.0.0-SNAPSHOT:check-stations (check) @ tidepool-data ---
[ERROR] BUILD FAILURE
[ERROR] Failed to execute goal io.tidepool:tidepool-maven-plugin:1.0.0-SNAPSHOT:check-stations (check) on project tidepool-data: The parameters 'owner' for goal io.tidepool:tidepool-maven-plugin:1 [...]
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/PluginParameterException
The message names the parameter and the goal, not the property that sets it. A required parameter breaks every existing user
of the plugin when it is added in a minor release; give it a default or leave it optional and check in execute().
A plugin that needs a newer Java than the one running Maven. The requiredJavaVersion of the descriptor (21 for the real plugin, from
maven.compiler.release) is checked before the goal runs. With the requiredJavaVersion parameter of maven-plugin-plugin set to an
absurd 99 in a copy of the plugin, on Maven 3.10.0 (the output on 3.9.16 and on 4.0.0-rc-7 has the same message):
$ mvn validate (consumer, using the copy with requiredJavaVersion 99)
[ERROR] BUILD FAILURE
[ERROR] Failed to execute goal io.tidepool:tidepool-maven-plugin:1.0.0-SNAPSHOT:check-stations (check) on project tidepool-data:
[ERROR] The plugin io.tidepool:tidepool-maven-plugin:1.0.0-SNAPSHOT has unmet prerequisites:
[ERROR] Required Java version 99 is not met by current version: 26
[ERROR] -> [Help 1]
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/PluginIncompatibleException
| Mistake | What happens |
|---|---|
|
The build of the plugin stops with "You need to specify a goalPrefix as it can not be correctly computed". |
|
"No plugin found for prefix 'tidepool' in the current project and in the plugin groups". |
Goal in a POM execution with neither |
Silently never runs. |
A value in |
The POM wins; the user property is ignored without a message. |
An enum parameter written in the wrong case |
"Cannot convert 'full' to Enum: No enum constant …". |
A Mojo without |
A box of warnings for every module of the reactor. |
A plugin built for a newer Java than the one that runs Maven ( |
"The plugin … has unmet prerequisites: Required Java version 99 is not met by current version: 26" (here with a deliberately absurd version). |
A Maven 4 API Mojo run on Maven 3 |
"Unable to load the mojo … java.util.NoSuchElementException". |
Changing the plugin and not reinstalling it |
Consumers resolve the plugin from the local repository, so they keep running the old JAR until |
The last row is the one that costs the most time in practice. It is not an error message: the consumer simply uses the JAR it finds
in the local repository. The cure is to run mvn install in the plugin project before running the consumer.
Where older material is outdated
-
Javadoc-tag Mojos. Maven 2 era plugins declared the goal with Javadoc tags (
@goal,@phase,@parameter). Plugin Tools still has an extractor for them (it ran on this page’s build and found nothing), but the supported form is the Java annotations of this page, and the official cookbook explains how to upgrade. Maven 4 breaks plugins that still depend on the Maven 2 API. -
Plexus
@Componentandplexus-component-annotations. The old way of injecting objects; Maven 4 removed Plexus dependency injection, and the Maven 4 notes ask for JSR-330. Constructor injection also made the Mojo testable without a container. -
maven-<name>-pluginfor your own plugin. The guide now states that the pattern is reserved for the Apache Maven project and that using it infringes the trademark; name plugins<name>-maven-plugin. -
JUnit 3
AbstractMojoTestCase. Still present in the harness, but the project says that new tests should use the JUnit 5 extension (since 3.4.0). -
"BUILD ERROR" for
MojoExecutionException. The Javadoc of the exception still says so; Maven 3.10.0 prints "BUILD FAILURE" for both exceptions. -
Books written for Maven 2 and 3.0. They declare Mojos with Javadoc tags. The workflow in this page is the current one: annotations, the generated descriptor, the harness and the Invoker Plugin.
Equivalents in Ant and Gradle
| Maven | Ant | Gradle |
|---|---|---|
A Mojo (a goal) in a plugin JAR with |
A class that extends |
A task class in a binary plugin, registered with |
|
Setter and nested-element methods; attributes of the XML element; Ant properties. |
Task properties with input annotations, set in the build script or by the plugin’s extension. |
|
|
Task dependencies and |
|
|
|
|
Not applicable (single-threaded). |
Tasks declare how they can run in parallel; see the Gradle section. |
Plugin testing: harness, Invoker Plugin |
AntUnit or JUnit with |
Unit tests with |
The Ant version of the same check, with its tests, is on Writing Custom Ant Tasks; the Gradle version is built on Binary Plugins and tested on Testing & Publishing Plugins. Releasing the finished plugin follows the same path as any library: Releasing and Publishing to Maven Central. Publishing was not run for this plugin. For the bigger picture of how the three tools extend themselves see Comparing Ant, Maven & Gradle.