Maven Enforcer

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 and the Apache Maven Enforcer Plugin 3.6.3 (documentation published 2026-05-15). Every example was run on JDK 26 on 2026-10-16, in scratch copies of the tidepool project (a parent POM and the tidepool-model, tidepool-engine and tidepool-cli modules, all plugin versions pinned in pluginManagement, maven.compiler.release 21). Where behaviour differs, the same build was also run on Maven 3.9.16 and on Maven 4.0.0-rc-7 (a release candidate, not a final release). Console output was filtered with grep or sed where that is said, and the lines Loaded N auto-discovered prefixes and elapsed times were left out.

A Maven build can do almost anything, and that is the problem: a project that only compiles with Java 21, a dependency tree where two libraries need two versions of a third, a commons-logging that crept in through a transitive dependency, a plugin whose version nobody pinned. None of these breaks the build on your machine today. The Enforcer plugin turns such assumptions into checks that fail the build early, with a message that says what to fix. It is the usual way to apply the practices in Troubleshooting & Best Practices automatically, and it is the cheapest way to keep the dependency practices of Dependency Analysis & Convergence from rotting.

How the plugin is used

The plugin has one goal, enforcer:enforce, which runs a list of rules against the current project and the current Maven runtime. A rule passes silently or throws an exception with a message, and the goal turns the exceptions into a build failure. The goal’s default phase is validate, the first phase of the build lifecycle, so the checks run before anything is compiled. How a plugin is declared, pinned and bound is the subject of Plugins & Their Configuration; here is the tidepool parent POM with the plugin pinned in pluginManagement and a first execution in build/plugins:

<build>
  <pluginManagement>
    <plugins>
      <plugin><artifactId>maven-enforcer-plugin</artifactId><version>3.6.3</version></plugin>
      <!-- the other plugins, pinned the same way -->
    </plugins>
  </pluginManagement>
  <plugins>
    <plugin>
      <artifactId>maven-enforcer-plugin</artifactId>
      <executions>
        <execution>
          <id>enforce-environment</id>
          <goals><goal>enforce</goal></goals>
          <configuration>
            <rules>
              <requireMavenVersion>
                <version>[3.9.0,)</version>
                <message>Tidepool needs Maven 3.9.0 or newer.</message>
              </requireMavenVersion>
              <requireJavaVersion>
                <version>[21,)</version>
              </requireJavaVersion>
            </rules>
          </configuration>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

Each child of <rules> is the name of a rule, and its own children are the rule’s parameters. The sample output of mvn validate on Maven 3.10.0 (filtered to the lines of the first module):

$ mvn -B validate | grep -E "enforcer:|Rule"
[INFO] --- enforcer:3.6.3:enforce (enforce-environment) @ tidepool-parent ---
[INFO] Rule 0: org.apache.maven.enforcer.rules.version.RequireMavenVersion passed
[INFO] Rule 1: org.apache.maven.enforcer.rules.version.RequireJavaVersion passed
[INFO] --- enforcer:3.6.3:enforce (enforce-environment) @ tidepool-model ---
[INFO] --- enforcer:3.6.3:enforce (enforce-environment) @ tidepool-engine ---
[INFO] --- enforcer:3.6.3:enforce (enforce-environment) @ tidepool-cli ---

Two details are visible here. The rules are numbered in the order of the POM and named by their implementing class, which is what to look for in an error. And the rules of the three child modules are not listed: a rule can declare that its result is the same in every module (the version rules do, because the Maven and Java runtime does not change inside one build), and the plugin then runs it once per build. The rules that look at the project, such as the dependency rules below, run again in each module. The parameter ignoreCache (user property enforcer.ignoreCache) switches that off; with -Denforcer.ignoreCache=true the RequireMavenVersion line appears under every module (run here: it did, for tidepool-parent, tidepool-model and tidepool-engine).

The parameters of enforcer:enforce

The plugin’s parameters, from its goal documentation:

Parameter User property Meaning

rules

none

The rules to run, as XML: each first-level element is a rule name, its children the rule’s parameters.

skip

enforcer.skip

Skip all checks. Default false.

fail

enforcer.fail

Fail the build when a rule fails. Default true; with false the failures are logged as warnings.

failFast

enforcer.failFast

Stop at the first rule that fails instead of reporting all of them. Default false.

failIfNoRules

enforcer.failIfNoRules

Fail when no rule is configured (since 3.2.0). Default true.

ignoreCache

enforcer.ignoreCache

Do not reuse the results of rules that say they can be cached. Default false.

rulesToExecute

enforcer.rules

Run only the rules with these names (since 3.2.0).

rulesToSkip

enforcer.skipRules

Do not run the rules with these names (since 3.2.0).

When a rule fails

A failing rule stops the build in the module where it fails. To see one, the Java requirement of the parent POM was temporarily changed to [27,), a JDK newer than the one running:

$ mvn -B validate -pl tidepool-model | grep -E "ERROR"
[ERROR] BUILD FAILURE
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-enforcer-plugin:3.6.3:enforce (enforce-environment) on project tidepool-model:
[ERROR] Rule 1: org.apache.maven.enforcer.rules.version.RequireJavaVersion failed with message:
[ERROR] Detected JDK /Library/Java/JavaVirtualMachines/jdk-26.jdk/Contents/Home is version 26 which is not in the allowed range [27,).
[ERROR] -> [Help 1]
...

(... marks the usual Maven help lines that were cut.) Without message, a rule prints its own explanation, as above; with message, the text you give is shown instead (see the Tidepool needs Maven ... text in the extension section below). Three switches change what a failure does:

  • level is a parameter every rule accepts (see below). A rule with <level>WARN</level> logs a warning and the build goes on.

  • -Denforcer.fail=false makes every failure a warning. Run here with the Maven requirement unmet, the output had [WARNING] Rule 0: ...RequireMavenVersion failed with message: and BUILD SUCCESS.

  • -Denforcer.skip skips the whole goal: the log still shows the --- enforcer:3.6.3:enforce (...) header and then no Rule lines (run here, BUILD SUCCESS).

By default all failing rules of an execution are reported together, which is useful when you are fixing a list of problems. With failFast the execution stops at the first failing rule (see the dependency example below).

The common parameters of every rule

Every rule accepts level, which is ERROR (the default) or WARN, and ruleName, an optional name for the rule configuration. Individual rules add their own, such as message. The tidepool parent can warn about a convergence problem without blocking while the team fixes it:

<dependencyConvergence>
  <level>WARN</level>
</dependencyConvergence>

and the output, in a module with the conflict of the next section (filtered with sed to the rule, the other rules of the execution passed or failed as usual):

[WARNING] Rule 0: org.apache.maven.enforcer.rules.dependency.DependencyConvergence warned with message:
Failed while enforcing releasability.

Dependency convergence error for commons-codec:commons-codec:jar:1.11. Paths to dependency are:
+-io.tidepool:tidepool-engine:jar:1.0.0-SNAPSHOT
  +-org.apache.httpcomponents:httpclient:jar:4.5.14:compile
    +-commons-codec:commons-codec:jar:1.11:compile
and
+-io.tidepool:tidepool-engine:jar:1.0.0-SNAPSHOT
  +-org.apache.commons:commons-csv:jar:1.14.1:compile
    +-commons-codec:commons-codec:jar:1.19.0:compile

Rules about the environment

requireMavenVersion and requireJavaVersion take a version in the Enforcer’s range syntax, the standard Maven range syntax with one change: a bare 3.9.0 means at least 3.9.0 (in a dependency it would only recommend a version). So [3.9.0,) and 3.9.0 mean the same, [17,21] is an inclusive range and (,1.0],[1.2,) is a union of ranges. The Java version is normalised before the comparison (1.5.0_07-b03 becomes 1.5.0-7), and 8 can be written for 1.8. requireJavaVersion accepts display to print the normalised value. The message in the failure above, version 26 which is not in the allowed range [27,), shows the JDK home and the version it found, which is the quickest way to find out which JDK Maven is really using (see Compiling & Toolchains for how that relates to release and toolchains).

The requirement [3.9.0,) is a minimum, not an exact version; to pin the exact Maven that a team uses, see Maven Wrapper. The other environment rules are requireJavaVendor, requireOS (family, name, architecture, version), requireEnvironmentVariable and requireActiveProfile. They follow the same pattern and were not run here.

Rules about dependencies

These are the rules that pay for the plugin. They look at the resolved dependency tree of each module (the mediation rules are in Dependencies, Scopes & Mediation). The second execution of the tidepool parent:

<execution>
  <id>enforce-dependencies</id>
  <goals><goal>enforce</goal></goals>
  <configuration>
    <rules>
      <dependencyConvergence/>
      <requireUpperBoundDeps/>
      <bannedDependencies>
        <excludes>
          <exclude>commons-logging:commons-logging</exclude>
          <exclude>log4j:log4j</exclude>
        </excludes>
        <message>Route logging through SLF4J (use jcl-over-slf4j instead of commons-logging).</message>
      </bannedDependencies>
      <banDuplicatePomDependencyVersions/>
      <requireReleaseDeps>
        <onlyWhenRelease>true</onlyWhenRelease>
        <message>A release must not depend on SNAPSHOT artifacts.</message>
      </requireReleaseDeps>
    </rules>
  </configuration>
</execution>

For the unmodified tidepool modules every rule passes. To see them work, tidepool-engine gets a dependency on httpclient 4.5.14 (declared before commons-csv), which brings its own, older commons-codec and a commons-logging:

<dependency>
  <groupId>org.apache.httpcomponents</groupId>
  <artifactId>httpclient</artifactId>
  <version>4.5.14</version>
</dependency>

This added dependency is this page’s own lab setup, not an example from the Maven documentation.

$ mvn -B validate -pl tidepool-engine -am | sed -n '/^\[ERROR\] Failed to execute/,/^\[ERROR\] -> \[Help/p'
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-enforcer-plugin:3.6.3:enforce (enforce-dependencies) on project tidepool-engine:
[ERROR] Rule 0: org.apache.maven.enforcer.rules.dependency.DependencyConvergence failed with message:
[ERROR] Failed while enforcing releasability.
[ERROR]
[ERROR] Dependency convergence error for commons-codec:commons-codec:jar:1.11. Paths to dependency are:
[ERROR] +-io.tidepool:tidepool-engine:jar:1.0.0-SNAPSHOT
[ERROR]   +-org.apache.httpcomponents:httpclient:jar:4.5.14:compile
[ERROR]     +-commons-codec:commons-codec:jar:1.11:compile
[ERROR] and
[ERROR] +-io.tidepool:tidepool-engine:jar:1.0.0-SNAPSHOT
[ERROR]   +-org.apache.commons:commons-csv:jar:1.14.1:compile
[ERROR]     +-commons-codec:commons-codec:jar:1.19.0:compile
[ERROR]
[ERROR] Rule 1: org.apache.maven.enforcer.rules.dependency.RequireUpperBoundDeps failed with message:
[ERROR] Failed while enforcing RequireUpperBoundDeps. The error(s) are [
[ERROR] Require upper bound dependencies error for commons-codec:commons-codec:1.11. Paths to dependency are:
[ERROR] +-io.tidepool:tidepool-engine:1.0.0-SNAPSHOT
[ERROR]   +-org.apache.httpcomponents:httpclient:4.5.14
[ERROR]     +-commons-codec:commons-codec:1.11
[ERROR] and
[ERROR] +-io.tidepool:tidepool-engine:1.0.0-SNAPSHOT
[ERROR]   +-org.apache.commons:commons-csv:1.14.1
[ERROR]     +-commons-codec:commons-codec:1.19.0
[ERROR] ]
[ERROR] Rule 2: org.apache.maven.enforcer.rules.dependency.BannedDependencies failed with message:
[ERROR] Route logging through SLF4J (use jcl-over-slf4j instead of commons-logging).
[ERROR] io.tidepool:tidepool-engine:jar:1.0.0-SNAPSHOT
[ERROR]    org.apache.httpcomponents:httpclient:jar:4.5.14
[ERROR]       commons-logging:commons-logging:jar:1.2 <--- banned via the exclude/include list
[ERROR] -> [Help 1]

Three rules failed in one run, and each shows the paths to the problem, which dependency:tree would only show after you knew where to look.

  • dependencyConvergence requires that every artifact in the tree is reached with a single version over all paths. Here commons-codec is 1.11 through httpclient and 1.19.0 through commons-csv, and the nearest-wins mediation would hide that by silently picking one.

  • requireUpperBoundDeps is stricter about the direction: the version Maven selects must not be lower than any version found in the tree. Maven picked 1.11 (the first declared at that depth), lower than the 1.19.0 commons-csv was built against, which can break at run time. It ignores the test and provided scopes by default (excludedScopes) and accepts excludes.

  • bannedDependencies bans artifacts by pattern groupId[:artifactId[:version[:type[:scope[:classifier]]]]] with wildcards, looks at transitive dependencies by default (searchTransitive), and can carve exceptions with includes.

The fix is in the POMs, not in the rule: manage the version of the conflicting artifact so that every path gets the same one (see Dependency Management & BOMs), exclude the unwanted dependency, and add the replacement. In tidepool-engine:

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>commons-codec</groupId>
      <artifactId>commons-codec</artifactId>
      <version>1.19.0</version>
    </dependency>
  </dependencies>
</dependencyManagement>
<dependencies>
  <dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.14</version>
    <exclusions>
      <exclusion>
        <groupId>commons-logging</groupId>
        <artifactId>commons-logging</artifactId>
      </exclusion>
    </exclusions>
  </dependency>
  <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jcl-over-slf4j</artifactId>
    <version>2.0.17</version>
  </dependency>
  <!-- the other dependencies of tidepool-engine -->
</dependencies>

and all five rules pass again (run here, BUILD SUCCESS). failFast changes the report: with -Denforcer.failFast=true on the broken POM, and the convergence rule set to WARN as in the previous section, the log showed Rule 0 ... warned and Rule 1 ... RequireUpperBoundDeps failed, and no Rule 2.

The other dependency rules

  • banDuplicatePomDependencyVersions fails when the same dependency is declared twice in one POM. On Maven 3.10.0 and 4.0.0-rc-7 the rule never gets a chance: the POM itself is invalid (the model error is 'dependencies.dependency.(groupId:artifactId:type:classifier)' must be unique: info.picocli:picocli:jar -> version (?) vs 4.7.7, and the build stops with The build could not read 1 project). On Maven 3.9.16 the model builder only warns, and the rule is what fails the build (Found 1 duplicate dependency declaration in this project: - dependencies.dependency[info.picocli:picocli:jar] (2 times)). Run on all three with a second picocli declaration in tidepool-cli.

  • requireReleaseDeps fails if any dependency, transitive ones included by default (searchTransitive), and also the parent (failWhenParentIsSnapshot, default true) is a SNAPSHOT. Since the tidepool modules are themselves snapshots, the parent POM uses onlyWhenRelease, which runs the rule only when the project version is not a SNAPSHOT. Without it the build failed immediately with Parent Cannot be a snapshot: io.tidepool:tidepool-parent:pom:1.0.0-SNAPSHOT, and with failWhenParentIsSnapshot set to false it listed io.tidepool:tidepool-model:jar:1.0.0-SNAPSHOT <--- is not a release dependency. A release whose build still depends on a snapshot is not reproducible, see Releasing.

  • banDynamicVersions bans version ranges, LATEST, RELEASE and SNAPSHOT versions (each can be allowed with allowRanges, allowLatest, allowRelease, allowSnapshots) and takes ignores. It sits in the next execution of the tidepool parent with <ignore>io.tidepool</ignore>, because the modules depend on each other’s snapshots. With the commons-csv version changed to [1.14,1.15) the failure was Dependency org.apache.commons:commons-csv:jar:1.14.1 (compile) is referenced with a banned dynamic version [1.14,1.15). See Versions, Snapshots & Ranges for why ranges hurt reproducibility.

  • banTransitiveDependencies, requireExplicitDependencyScope (every dependency declares its scope in its own POM) and banDependencyManagementScope complete the family. They were not run here.

Rules about the build itself

The last execution of the tidepool parent checks the build definition:

<execution>
  <id>enforce-hygiene</id>
  <goals><goal>enforce</goal></goals>
  <configuration>
    <rules>
      <requirePluginVersions/>
      <banDynamicVersions>
        <ignores>
          <ignore>io.tidepool</ignore>
        </ignores>
      </banDynamicVersions>
      <requireProperty>
        <property>tidepool.channel</property>
        <message>Set tidepool.channel in the parent POM.</message>
        <regex>stable|beta</regex>
        <regexMessage>tidepool.channel must be stable or beta.</regexMessage>
      </requireProperty>
      <bannedRepositories>
        <bannedRepositories>
          <bannedRepository>http://*</bannedRepository>
        </bannedRepositories>
        <bannedPluginRepositories>
          <bannedPluginRepository>http://*</bannedPluginRepository>
        </bannedPluginRepositories>
      </bannedRepositories>
    </rules>
  </configuration>
</execution>
  • requirePluginVersions fails for a plugin without a version, or with LATEST, RELEASE or a SNAPSHOT version. It also bans (banMavenDefaults, default true) versions that come only from the running Maven’s default lifecycle bindings, which is the point of the rule: the build then changes when you upgrade Maven. With the pin of maven-resources-plugin deleted from the parent, mvn package -DskipTests -pl tidepool-model failed on each of the three Maven versions, and the message shows what each of them would have used:

    Maven Message (the Rule 0 text, abbreviated)

    3.9.16

    ... depend on Maven 3.9.16 defaults ... org.apache.maven.plugins:maven-resources-plugin. The version currently in use is 3.4.0 via default lifecycle bindings

    3.10.0

    ... depend on Maven 3.10.0 defaults ... maven-resources-plugin. The version currently in use is 3.5.0 via default lifecycle bindings

    4.0.0-rc-7

    ... depend on Maven 4.0.0-rc-7 defaults ... maven-resources-plugin. The version currently in use is 3.5.0 via default lifecycle bindings

    It is the quickest way to find the plugins you forgot to pin before a Maven upgrade changes them. The phases parameter (default clean,deploy,site) says which lifecycle phases to inspect for bound plugins, and additionalPlugins and unCheckedPluginList extend and relax the check.

  • requireProperty demands that a property exists and optionally matches a regex. With tidepool.channel set to nightly the failure was tidepool.channel must be stable or beta., and with the property removed Set tidepool.channel in the parent POM. (run on Maven 3.10.0). A property that Maven itself defines is always present: removing project.build.sourceEncoding did not fail a rule on it, because since 3.10 Maven defaults it to UTF-8.

  • bannedRepositories and its sibling requireNoRepositories keep repository declarations (and the supply-chain risk they carry, see Repositories, Mirrors & Repository Managers) out of your POMs; here http://* is banned. banDistributionManagement and bannedPlugins do the same for those elements.

  • reactorModuleConvergence checks that the modules of a multi-module build have consistent versions and that every module’s parent is part of the reactor, and requireSameVersions that chosen plugins or dependencies share one version (for example maven-surefire-plugin and maven-failsafe-plugin, see Multi-Module Builds & the Reactor). requireFilesExist, requireFilesDontExist, requireFilesSize, requireFileChecksum, requireTextFileChecksum, requirePrerequisite, requireMatchingCoordinates, requireProfileIdsExist, requireReleaseVersion, requireSnapshotVersion, evaluateBeanshell and externalRules are the remaining built-in rules. They were not run here.

Source: requireProperty

Beyond the built-in rules, the plugin site lists two third-party rule sets: the Sonatype OSS Index rule to ban vulnerable dependencies, and MojoHaus Extra Enforcer Rules. Neither was run here (the first needs access to an external vulnerability service).

Running one rule from the command line

enforcer:enforce also works without any POM configuration, which is a way to test a rule on a project that does not use the plugin yet. In an empty project with only coordinates:

$ mvn -B org.apache.maven.plugins:maven-enforcer-plugin:3.6.3:enforce -Denforcer.rules=alwaysPass,alwaysFail | grep -E "enforcer:|Rule|BUILD"
[INFO] --- enforcer:3.6.3:enforce (default-cli) @ cli-check ---
[INFO] Rule 0: org.apache.maven.enforcer.rules.AlwaysPass passed
[ERROR] BUILD FAILURE
[ERROR] Rule 1: org.apache.maven.enforcer.rules.AlwaysFail failed with message:

Without -Denforcer.rules and without configuration the goal refuses to run: No rules are configured. Use the skip flag if you want to disable execution. (failIfNoRules). When the rule is configured in the POM, the same option selects it, but read this carefully: if the configuration is inside an <execution>, you must name the execution (mvn enforcer:enforce@enforce-hygiene), as with any goal bound with an id, because the goal run from the command line is the default-cli execution, which only sees plugin-level configuration. Run here, mvn enforcer:enforce@enforce-hygiene -pl tidepool-model ran the four rules of that execution.

A gotcha: -Denforcer.rules=requireJavaVersion on a normal mvn validate applies to every execution bound to the build, including the ones that do not configure that rule, and on Maven 3.10.0 the enforce-dependencies execution failed with an internal error (Cannot invoke "org.apache.maven.artifact.versioning.VersionRange.getRecommendedVersion()" because "versionRange" is null) (apparently because the rule was created without its version). Select rules with enforcer:enforce@id or rulesToSkip (which worked: with -Denforcer.skipRules=requireJavaVersion,requirePluginVersions the other rules ran and the build succeeded).

The Enforcer Extension

The plugin is configured in the POM, so a rule that must hold for a whole organisation has to be inherited through a parent POM. The Enforcer Extension keeps the same rules outside the POM, in the project’s .mvn directory (see Extensions and Configuring Maven). Register the extension in .mvn/extensions.xml:

<extensions>
  <extension>
    <groupId>org.apache.maven.extensions</groupId>
    <artifactId>maven-enforcer-extension</artifactId>
    <version>3.6.3</version>
  </extension>
</extensions>

and put the rules into .mvn/enforcer-extension.xml, whose content is like an <execution> of the plugin (the default id is default-extension and the default phase validate):

<extension>
  <executions>
    <execution>
      <id>enforce-maven</id>
      <configuration>
        <rules>
          <requireMavenVersion>
            <version>[3.11.0,)</version>
            <message>Tidepool needs Maven 3.11.0 or newer (this example is meant to fail).</message>
          </requireMavenVersion>
        </rules>
      </configuration>
    </execution>
  </executions>
</extension>

The version requirement was deliberately set higher than the Maven used, with the enforcer section removed from the parent POM, to see the extension work:

$ mvn -B validate | sed -n '/enforcer:/,$p' | grep -E "enforcer:|ERROR\] (Rule|Tidepool)|BUILD"
[INFO] --- enforcer:3.6.3:enforce (enforce-maven) @ tidepool-parent ---
[ERROR] BUILD FAILURE
[ERROR] Rule 0: org.apache.maven.enforcer.rules.version.RequireMavenVersion failed with message:
[ERROR] Tidepool needs Maven 3.11.0 or newer (this example is meant to fail).

That is Maven 3.10.0 and also Maven 3.9.16 (the run printed the same failure, preceded by messages about the extension jar being verified in the local repository). On Maven 4.0.0-rc-7 the build passed, as it should: 4.0.0 satisfies [3.11.0,), and the extension added the enforcer:3.6.3:enforce (enforce-maven) execution to all four projects. The extension adds the execution to every project, so no module needs an enforcer section at all, and -Denforcer.skip skips it (run here: the header was printed and no Rule line). The extension’s documentation also names a third way to register it, in the POM, and says that the plugin makes more sense in that case.

Writing a custom rule

When no built-in rule fits, write one. A rule is a class in a normal JAR project that extends AbstractEnforcerRule (available since API 3.2.1), carries a @Named("ruleName") annotation whose name starts with a lowercase letter, and implements execute(): return when the check passes, throw EnforcerRuleException with a helpful message when it fails. Fields (or setters) become configuration parameters, and Maven components such as MavenProject or MavenSession are injected with @Inject. The enforcer plugin shares its class loader with the rule, so enforcer-api and Maven core are provided scope, and a sisu-maven-plugin main-index execution generates the index that lets Maven find the @Named class. The tidepool project’s rule checks the stations file that the StationReader reads:

@Named("requireStationsCsv")
public class RequireStationsCsv extends AbstractEnforcerRule {

    private final MavenProject project;

    /** Path relative to the project base directory. */
    private String file = "data/stations.csv";

    /** Minimum number of data rows. */
    private int minStations = 1;

    @Inject
    public RequireStationsCsv(MavenProject project) {
        this.project = project;
    }

    @Override
    public void execute() throws EnforcerRuleException {
        Path csv = project.getBasedir().toPath().resolve(file);
        getLog().debug("Checking " + csv);
        try {
            List<String> lines = Files.readAllLines(csv);
            if (lines.isEmpty() || !lines.get(0).startsWith("id,")) {
                throw new EnforcerRuleException(file + " must start with a header line beginning with 'id,'.");
            }
            int rows = lines.size() - 1;
            if (rows < minStations) {
                throw new EnforcerRuleException(file + " has " + rows + " station(s), at least " + minStations + " required.");
            }
        } catch (IOException e) {
            throw new EnforcerRuleException("Cannot read " + csv + ": " + e.getMessage(), e);
        }
    }

    @Override
    public String getCacheId() {
        // The result depends on the module directory, so do not share it between modules.
        return project.getBasedir() + "|" + file + "|" + minStations;
    }
}

(The imports are java.io.IOException, java.nio.file.Files, java.nio.file.Path, java.util.List, javax.inject.Inject, javax.inject.Named, the two enforcer API classes org.apache.maven.enforcer.rule.api.AbstractEnforcerRule and EnforcerRuleException, and org.apache.maven.project.MavenProject.) Its POM, a JAR project with release 17, declares enforcer-api 3.6.3, maven-core 3.9.15 and javax.inject 1 as provided dependencies and the plugin org.eclipse.sisu:sisu-maven-plugin:1.0.0 with the goal main-index. After mvn install the JAR contained META-INF/sisu/javax.inject.Named with the single line io.tidepool.rules.RequireStationsCsv.

The rule is used by adding the JAR as a dependency of the enforcer plugin and writing the rule’s name in <rules>, in tidepool-engine/pom.xml:

<plugin>
  <artifactId>maven-enforcer-plugin</artifactId>
  <dependencies>
    <dependency>
      <groupId>io.tidepool</groupId>
      <artifactId>tidepool-enforcer-rules</artifactId>
      <version>1.0.0</version>
    </dependency>
  </dependencies>
  <executions>
    <execution>
      <id>check-stations</id>
      <goals><goal>enforce</goal></goals>
      <configuration>
        <rules>
          <requireStationsCsv>
            <file>../data/stations.csv</file>
            <minStations>5</minStations>
          </requireStationsCsv>
        </rules>
      </configuration>
    </execution>
  </executions>
</plugin>
$ mvn -B validate -pl tidepool-engine -am | grep -E "check-stations|ERROR\] (Rule|\.\.)|BUILD"
[INFO] --- enforcer:3.6.3:enforce (check-stations) @ tidepool-engine ---
[ERROR] BUILD FAILURE
[ERROR] Rule 0: io.tidepool.rules.RequireStationsCsv failed with message:
[ERROR] ../data/stations.csv has 3 station(s), at least 5 required.

The same failure was printed on Maven 3.10.0, 3.9.16 and 4.0.0-rc-7; with minStations 3 the rule passed (Rule 0: io.tidepool.rules.RequireStationsCsv passed). getCacheId() is there on purpose. According to the documentation a rule that is not cacheable need not override it, and a cacheable rule returns an id that is unique for the parameters and conditions that change its result; results are stored per id. A rule that reads the project must therefore include the module in its id, as above. The rule artifact has to be resolvable when the build runs: here it was installed into the local repository of the scratch build; in a real organisation it is deployed to the company’s repository (see Install, Deploy & Distribution Management). The classes of enforcer-rules are not API and a custom rule must not use them. To break the build immediately instead of collecting failures, execute() can throw EnforcerRuleError.

Changes between Maven versions

Version What changes for the Enforcer

3.9.16

banDuplicatePomDependencyVersions has work to do: the model builder only warns about duplicate dependencies. Default plugin versions seen by requirePluginVersions come from the 3.9 bindings (maven-resources-plugin 3.4.0). Plugin 3.6.3, the extension and the custom rule ran unchanged.

3.10.0

Duplicate dependencies are rejected when the POM is read (stricter POM validation), before any rule runs. The default plugin versions are those of 3.10.0 (maven-resources-plugin 3.5.0), so unpinned plugins change with the upgrade, which requirePluginVersions reports. project.build.sourceEncoding is always defined (UTF-8).

4.0.0-rc-7 (release candidate)

Plugin 3.6.3, the extension and the custom rule ran unchanged. Duplicate dependencies are rejected when the POM is read, as in 3.10.0 (the Unable to find the root directory warning in the log is Maven 4’s, about a missing .mvn directory in the scratch project, and unrelated to the Enforcer).

Where older material is outdated

Material written for Maven 2 predates most of what is on this page. What still circulates with it, and is outdated:

  • Plugin versions were left out of POMs; requirePluginVersions exists to forbid that, and what Maven supplies as a default changes with every Maven release (3.4.0 in 3.9.16, 3.5.0 in 3.10.0 for the resources plugin).

  • Duplicate dependency declarations were tolerated with a warning; Maven 3.10 and 4 now refuse them.

  • A Java version written as 1.8 can be written 8; the version strings of the JDK are normalised before the comparison.

Equivalents in Ant and Gradle

Maven Enforcer Ant Gradle

requireMavenVersion, requireJavaVersion

A condition with antversion or javaversion and a fail task; see Conditions & Control Flow.

The wrapper pins the Gradle version, and a toolchain requirement pins the JDK; there is no single "rules" plugin.

dependencyConvergence, requireUpperBoundDeps

Ivy conflict managers and <ivy:report>; no equivalent check.

resolutionStrategy.failOnVersionConflict() and dependency locking; see Dependency Resolution, Conflicts & Capabilities and Dependency Locking, Verification & Caching.

bannedDependencies

An Ivy exclude.

exclude rules and capability conflicts.

requirePluginVersions

Not applicable: Ant has no plugins.

Pinned plugin versions in the plugins { } block, see Best Practices & Troubleshooting.

Custom rule

A custom task or fail.

A task or plugin in buildSrc or a convention plugin.

The cross-tool picture is in Comparing Ant, Maven and Gradle.

Summary

  • enforcer:enforce (default phase validate) runs rules and fails the build with a message; level, fail, failFast, skip, rulesToExecute and rulesToSkip decide what a failure does.

  • Pin the plugin in pluginManagement and group rules into executions by purpose: environment, dependencies and build hygiene.

  • The rules worth their cost: requireMavenVersion and requireJavaVersion, dependencyConvergence, requireUpperBoundDeps, bannedDependencies, requireReleaseDeps (with onlyWhenRelease), banDynamicVersions and requirePluginVersions.

  • A conflict is fixed in the POMs (manage a version, exclude, replace), not by loosening the rule.

  • The rules can live outside the POM, in .mvn/enforcer-extension.xml, through the extension; a custom rule is a @Named class extending AbstractEnforcerRule and is added as a dependency of the plugin.

  • Use enforcer:enforce@id, not -Denforcer.rules, to run one execution from the command line.