EnforcerVersionRangeStrategy.java
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.maven.cling.invoker.mvnup.goals;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import eu.maveniverse.domtrip.Document;
import eu.maveniverse.domtrip.Editor;
import eu.maveniverse.domtrip.Element;
import org.apache.maven.api.cli.mvnup.UpgradeOptions;
import org.apache.maven.api.di.Named;
import org.apache.maven.api.di.Priority;
import org.apache.maven.api.di.Singleton;
import org.apache.maven.cling.invoker.mvnup.UpgradeContext;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.ARTIFACT_ID;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.BUILD;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.CONFIGURATION;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.GROUP_ID;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PLUGIN;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PLUGINS;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PLUGIN_MANAGEMENT;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PROFILE;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.PROFILES;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Elements.VERSION;
import static eu.maveniverse.domtrip.maven.MavenPomElements.Plugins.DEFAULT_MAVEN_PLUGIN_GROUP_ID;
/**
* Strategy for widening {@code requireMavenVersion} ranges in the {@code maven-enforcer-plugin}
* to allow Maven 4.
*
* <p>Many projects use the {@code maven-enforcer-plugin} with a {@code requireMavenVersion} rule
* that has an exclusive upper bound at version 4 (e.g., {@code [3.8.8,4)}, {@code [3,4)}),
* which blocks Maven 4 builds. This strategy widens such ranges to allow Maven 4 by changing
* the upper bound to 5 (e.g., {@code [3.8.8,4)} becomes {@code [3.8.8,5)}).
*
* <p>The strategy handles:
* <ul>
* <li>{@code <configuration><rules><requireMavenVersion>} in plugin declarations</li>
* <li>{@code <executions><execution><configuration><rules><requireMavenVersion>} in executions</li>
* <li>Both {@code build/plugins} and {@code build/pluginManagement/plugins} sections</li>
* <li>Profile-scoped enforcer plugin declarations</li>
* </ul>
*/
@Named
@Singleton
@Priority(18)
public class EnforcerVersionRangeStrategy extends AbstractUpgradeStrategy {
private static final String ENFORCER_ARTIFACT_ID = "maven-enforcer-plugin";
private static final String RULES = "rules";
private static final String REQUIRE_MAVEN_VERSION = "requireMavenVersion";
private static final String EXECUTIONS = "executions";
private static final String EXECUTION = "execution";
/**
* Pattern to match Maven version ranges with an exclusive upper bound at any 4.x version.
* Captures:
* Group 1: opening bracket ([ or ()
* Group 2: lower bound (e.g., 3.8.8, 3, 3.6.3)
* Group 3: upper bound starting with 4 (e.g., 4, 4.0, 4.0.0, 4.1, 4.2.1)
*
* Examples matched: [3.8.8,4), [3,4), (3.6.3,4), [3.8.8,4.0), [3.8.8,4.0.0), [3.8.8,4.1)
* Examples not matched: [3.8.8,), 3.8.8, [3.8.8,5), [3.8.8,4]
*/
static final Pattern MAVEN4_EXCLUSIVE_UPPER_BOUND = Pattern.compile("^(\\[|\\()(.+?),\\s*(4(?:\\.\\d+)*)\\s*\\)$");
@Override
public boolean isApplicable(UpgradeContext context) {
UpgradeOptions options = getOptions(context);
return isOptionEnabled(options, options.model(), true);
}
@Override
public String getDescription() {
return "Widening RequireMavenVersion ranges to allow Maven 4";
}
@Override
protected UpgradeResult doApply(UpgradeContext context, Map<Path, Document> pomMap) {
Set<Path> processedPoms = new HashSet<>();
Set<Path> modifiedPoms = new HashSet<>();
Set<Path> errorPoms = new HashSet<>();
for (Map.Entry<Path, Document> entry : pomMap.entrySet()) {
Path pomPath = entry.getKey();
Document pomDocument = entry.getValue();
processedPoms.add(pomPath);
context.info(pomPath + " (checking for RequireMavenVersion range restrictions)");
context.indent();
try {
boolean hasUpgrades = widenEnforcerVersionRanges(pomDocument, context);
if (hasUpgrades) {
modifiedPoms.add(pomPath);
context.success("RequireMavenVersion ranges widened to allow Maven 4");
} else {
context.success("No RequireMavenVersion range restrictions found");
}
} catch (Exception e) {
context.failure("Failed to widen RequireMavenVersion ranges: " + e.getMessage());
errorPoms.add(pomPath);
} finally {
context.unindent();
}
}
return new UpgradeResult(processedPoms, modifiedPoms, errorPoms);
}
/**
* Finds and widens all RequireMavenVersion ranges in the POM document.
*/
private boolean widenEnforcerVersionRanges(Document pomDocument, UpgradeContext context) {
Element root = pomDocument.root();
boolean hasUpgrades = false;
// Process root-level build/plugins and build/pluginManagement/plugins
hasUpgrades |= processPluginSections(root, context);
// Process profile-scoped plugins
Element profiles = root.childElement(PROFILES).orElse(null);
if (profiles != null) {
for (Element profile : profiles.childElements(PROFILE).toList()) {
hasUpgrades |= processPluginSections(profile, context);
}
}
return hasUpgrades;
}
/**
* Processes both build/plugins and build/pluginManagement/plugins sections.
*/
private boolean processPluginSections(Element parent, UpgradeContext context) {
Element buildElement = parent.childElement(BUILD).orElse(null);
if (buildElement == null) {
return false;
}
boolean hasUpgrades = false;
// Check build/plugins
Element pluginsElement = buildElement.childElement(PLUGINS).orElse(null);
if (pluginsElement != null) {
hasUpgrades |= processPluginsForEnforcer(pluginsElement, context);
}
// Check build/pluginManagement/plugins
Element pluginManagement = buildElement.childElement(PLUGIN_MANAGEMENT).orElse(null);
if (pluginManagement != null) {
Element managedPlugins = pluginManagement.childElement(PLUGINS).orElse(null);
if (managedPlugins != null) {
hasUpgrades |= processPluginsForEnforcer(managedPlugins, context);
}
}
return hasUpgrades;
}
/**
* Finds enforcer plugin elements and processes their requireMavenVersion rules.
*/
private boolean processPluginsForEnforcer(Element pluginsElement, UpgradeContext context) {
return pluginsElement
.childElements(PLUGIN)
.filter(this::isEnforcerPlugin)
.map(plugin -> processEnforcerPlugin(plugin, context))
.reduce(false, Boolean::logicalOr);
}
/**
* Checks whether a plugin element is the maven-enforcer-plugin.
*/
private boolean isEnforcerPlugin(Element plugin) {
String artifactId = plugin.childTextTrimmed(ARTIFACT_ID);
if (!ENFORCER_ARTIFACT_ID.equals(artifactId)) {
return false;
}
String groupId = plugin.childTextTrimmed(GROUP_ID);
return groupId == null || groupId.isEmpty() || DEFAULT_MAVEN_PLUGIN_GROUP_ID.equals(groupId);
}
/**
* Processes an enforcer plugin element, checking both top-level configuration
* and per-execution configurations for requireMavenVersion rules.
*/
private boolean processEnforcerPlugin(Element plugin, UpgradeContext context) {
boolean hasUpgrades = false;
// Check top-level <configuration><rules><requireMavenVersion>
Element configuration = plugin.childElement(CONFIGURATION).orElse(null);
if (configuration != null) {
hasUpgrades |= processRulesElement(configuration, context);
}
// Check <executions><execution><configuration><rules><requireMavenVersion>
Element executionsElement = plugin.childElement(EXECUTIONS).orElse(null);
if (executionsElement != null) {
for (Element execution : executionsElement.childElements(EXECUTION).toList()) {
Element execConfig = execution.childElement(CONFIGURATION).orElse(null);
if (execConfig != null) {
hasUpgrades |= processRulesElement(execConfig, context);
}
}
}
return hasUpgrades;
}
/**
* Processes a configuration element's rules for requireMavenVersion.
*/
private boolean processRulesElement(Element configuration, UpgradeContext context) {
Element rules = configuration.childElement(RULES).orElse(null);
if (rules == null) {
return false;
}
Element requireMavenVersion = rules.childElement(REQUIRE_MAVEN_VERSION).orElse(null);
if (requireMavenVersion == null) {
return false;
}
Element versionElement = requireMavenVersion.childElement(VERSION).orElse(null);
if (versionElement == null) {
return false;
}
String versionRange = versionElement.textContentTrimmed();
if (versionRange == null || versionRange.isEmpty()) {
return false;
}
String widened = widenVersionRange(versionRange);
if (widened != null) {
Editor editor = new Editor(versionElement.document());
editor.setTextContent(versionElement, widened);
context.detail(
"Widened RequireMavenVersion range from " + versionRange + " to " + widened + " to allow Maven 4");
return true;
}
return false;
}
/**
* Widens a Maven version range that has an exclusive upper bound at 4.x.
*
* @param versionRange the version range string (e.g., "[3.8.8,4)")
* @return the widened range (e.g., "[3.8.8,5)"), or null if no widening is needed
*/
static String widenVersionRange(String versionRange) {
Matcher matcher = MAVEN4_EXCLUSIVE_UPPER_BOUND.matcher(versionRange);
if (matcher.matches()) {
String openBracket = matcher.group(1);
String lowerBound = matcher.group(2);
return openBracket + lowerBound + ",5)";
}
return null;
}
}