ToolchainPluginStrategy.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.List;
import java.util.Map;
import java.util.Set;
import eu.maveniverse.domtrip.Document;
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.api.model.Build;
import org.apache.maven.api.model.Model;
import org.apache.maven.api.model.Plugin;
import org.apache.maven.api.model.PluginManagement;
import org.apache.maven.api.xml.XmlNode;
import org.apache.maven.cling.invoker.mvnup.UpgradeContext;
import org.apache.maven.impl.JdkSourceLevelSupport;
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.PROPERTIES;
/**
* Strategy for adding the {@code maven-toolchains-plugin} with the {@code select-jdk-toolchain}
* goal when the project's required {@code --source}/{@code --release} level is no longer supported
* by the running JDK.
*
* <p>This strategy detects the project's source level from:
* <ol>
* <li>{@code maven.compiler.release} property</li>
* <li>{@code maven.compiler.source} property</li>
* <li>Compiler plugin {@code <configuration><release>} or {@code <source>}</li>
* </ol>
*
* <p>If the running JDK does not support the detected source level (per JEP 182 retirement
* schedule), and the {@code maven-toolchains-plugin} is not already configured with the
* {@code select-jdk-toolchain} goal, this strategy adds it so that the plugin's built-in
* JDK discovery mechanism can find a compatible JDK at build time.
*
* @see <a href="https://openjdk.org/jeps/182">JEP 182: Policy for Retiring javac -source and -target Options</a>
*/
@Named
@Singleton
@Priority(15)
public class ToolchainPluginStrategy extends AbstractUpgradeStrategy {
static final String MAVEN_TOOLCHAINS_PLUGIN = "maven-toolchains-plugin";
static final String TOOLCHAINS_PLUGIN_GROUP_ID = "org.apache.maven.plugins";
static final String SELECT_JDK_TOOLCHAIN_GOAL = "select-jdk-toolchain";
/**
* Minimum toolchains-plugin version that contains the {@code select-jdk-toolchain} goal.
* Older versions (e.g. 1.1) only have {@code help} and {@code toolchain} goals, so adding
* a {@code select-jdk-toolchain} execution without upgrading would break the build.
*/
static final String TOOLCHAINS_PLUGIN_MIN_VERSION = "3.2.0";
private static final String MAVEN_COMPILER_RELEASE = "maven.compiler.release";
private static final String MAVEN_COMPILER_SOURCE = "maven.compiler.source";
private static final String MAVEN_COMPILER_PLUGIN = "maven-compiler-plugin";
@Override
public boolean isApplicable(UpgradeContext context) {
UpgradeOptions options = getOptions(context);
if (options.all().orElse(false)) {
return true;
}
// Default behavior: run when no specific options are provided,
// or when all options are explicitly disabled (safety net ��� toolchain
// setup should always run as it prevents build failures)
boolean noOptionsSpecified = options.all().isEmpty()
&& options.infer().isEmpty()
&& options.model().isEmpty()
&& options.plugins().isEmpty()
&& options.modelVersion().isEmpty();
boolean allOptionsDisabled = options.all().map(v -> !v).orElse(false)
&& options.infer().map(v -> !v).orElse(false)
&& options.model().map(v -> !v).orElse(false)
&& options.plugins().map(v -> !v).orElse(false)
&& options.modelVersion().isEmpty();
if (noOptionsSpecified || allOptionsDisabled) {
return true;
}
// Run when --model is explicitly set
if (options.model().isPresent()) {
return options.model().get();
}
return false;
}
@Override
public String getDescription() {
return "Adding maven-toolchains-plugin for JDK source level compatibility";
}
@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<>();
int runningJdkMajor = getRunningJdkMajor();
for (Map.Entry<Path, Document> entry : pomMap.entrySet()) {
Path pomPath = entry.getKey();
Document pomDocument = entry.getValue();
processedPoms.add(pomPath);
context.info(pomPath + " (checking JDK source level compatibility)");
context.indent();
try {
int sourceLevel = detectSourceLevel(pomDocument);
if (sourceLevel <= 0) {
// Local POM doesn't declare a source level ��� check inherited config
// from parent POMs (e.g. org.apache.sling:sling-parent setting --source 6)
sourceLevel = detectSourceLevelFromEffectiveModel(context, pomPath);
}
if (sourceLevel <= 0) {
context.success("No source level configured");
continue;
}
if (JdkSourceLevelSupport.supportsSourceLevel(runningJdkMajor, sourceLevel)) {
context.success("Running JDK " + runningJdkMajor + " supports --source " + sourceLevel);
continue;
}
if (hasToolchainsPluginWithSelectGoal(pomDocument)) {
context.success(
"maven-toolchains-plugin with " + SELECT_JDK_TOOLCHAIN_GOAL + " goal already configured");
continue;
}
int latestJdk = JdkSourceLevelSupport.latestJdkForSourceLevel(sourceLevel);
addToolchainsPlugin(pomDocument, latestJdk);
modifiedPoms.add(pomPath);
context.success("Added maven-toolchains-plugin with " + SELECT_JDK_TOOLCHAIN_GOAL + " goal (--source "
+ sourceLevel + " requires JDK <= " + latestJdk + ")");
} catch (Exception e) {
context.failure("Failed to add toolchains plugin: " + e.getMessage());
errorPoms.add(pomPath);
} finally {
context.unindent();
}
}
return new UpgradeResult(processedPoms, modifiedPoms, errorPoms);
}
/**
* Detects the project's required source level from properties or compiler plugin configuration.
*
* @return the source level as a major version, or {@code -1} if none is configured
*/
int detectSourceLevel(Document pomDocument) {
Element root = pomDocument.root();
// Check properties: maven.compiler.release takes precedence
Element properties = root.childElement(PROPERTIES).orElse(null);
if (properties != null) {
Element releaseElement =
properties.childElement(MAVEN_COMPILER_RELEASE).orElse(null);
if (releaseElement != null) {
int level = JdkSourceLevelSupport.normalizeSourceLevel(
releaseElement.textContent().trim());
if (level > 0) {
return level;
}
}
Element sourceElement =
properties.childElement(MAVEN_COMPILER_SOURCE).orElse(null);
if (sourceElement != null) {
int level = JdkSourceLevelSupport.normalizeSourceLevel(
sourceElement.textContent().trim());
if (level > 0) {
return level;
}
}
}
// Check compiler plugin configuration
return detectSourceLevelFromCompilerPlugin(root);
}
private int detectSourceLevelFromCompilerPlugin(Element root) {
Element build = root.childElement(BUILD).orElse(null);
if (build == null) {
return -1;
}
// Check <build><plugins>
int level = detectFromPluginSection(build.childElement(PLUGINS).orElse(null));
if (level > 0) {
return level;
}
// Check <build><pluginManagement><plugins>
Element pluginManagement = build.childElement(PLUGIN_MANAGEMENT).orElse(null);
if (pluginManagement != null) {
level = detectFromPluginSection(
pluginManagement.childElement(PLUGINS).orElse(null));
}
return level;
}
private int detectFromPluginSection(Element pluginsElement) {
if (pluginsElement == null) {
return -1;
}
for (Element plugin : pluginsElement.childElements(PLUGIN).toList()) {
String artifactId = plugin.childTextTrimmed(ARTIFACT_ID);
String groupId = plugin.childTextTrimmed(GROUP_ID);
if (MAVEN_COMPILER_PLUGIN.equals(artifactId)
&& (groupId == null || groupId.isEmpty() || TOOLCHAINS_PLUGIN_GROUP_ID.equals(groupId))) {
Element config = plugin.childElement(CONFIGURATION).orElse(null);
if (config != null) {
// <release> takes precedence
Element releaseNode = config.childElement("release").orElse(null);
if (releaseNode != null
&& releaseNode.textContent() != null
&& !releaseNode.textContent().isBlank()) {
int level = JdkSourceLevelSupport.normalizeSourceLevel(
releaseNode.textContent().trim());
if (level > 0) {
return level;
}
}
Element sourceNode = config.childElement("source").orElse(null);
if (sourceNode != null
&& sourceNode.textContent() != null
&& !sourceNode.textContent().isBlank()) {
int level = JdkSourceLevelSupport.normalizeSourceLevel(
sourceNode.textContent().trim());
if (level > 0) {
return level;
}
}
}
}
}
return -1;
}
/**
* Detects the project's required source level from the effective (fully-resolved) model.
* This resolves inherited configuration from parent POMs on Maven Central.
*
* <p>The Session's request cache ensures that effective model builds are deduplicated
* within this strategy ��� if this strategy already built the effective model for the
* same POM path, the cached result is returned immediately.
*
* @param context the upgrade context for logging
* @param pomPath the path to the POM file
* @return the source level as a major version, or {@code -1} if none is configured
*/
int detectSourceLevelFromEffectiveModel(UpgradeContext context, Path pomPath) {
try {
Model effectiveModel = buildEffectiveModel(context, pomPath);
return detectSourceLevelFromModel(effectiveModel);
} catch (Exception e) {
context.debug("Failed to build effective model for " + pomPath + ": " + e.getMessage());
return -1;
}
}
/**
* Extracts the source level from a resolved effective model by checking properties
* and compiler plugin configuration.
*
* @param effectiveModel the fully-resolved effective model
* @return the source level as a major version, or {@code -1} if none is configured
*/
int detectSourceLevelFromModel(Model effectiveModel) {
Map<String, String> props = effectiveModel.getProperties();
// Check maven.compiler.release property (takes precedence)
String release = props.get(MAVEN_COMPILER_RELEASE);
if (release != null && !release.isBlank()) {
int level = JdkSourceLevelSupport.normalizeSourceLevel(release.trim());
if (level > 0) {
return level;
}
}
// Check maven.compiler.source property
String source = props.get(MAVEN_COMPILER_SOURCE);
if (source != null && !source.isBlank()) {
int level = JdkSourceLevelSupport.normalizeSourceLevel(source.trim());
if (level > 0) {
return level;
}
}
// Check compiler plugin configuration from resolved build
Build build = effectiveModel.getBuild();
if (build != null) {
int level = detectFromEffectivePlugins(build.getPlugins());
if (level > 0) {
return level;
}
PluginManagement pm = build.getPluginManagement();
if (pm != null) {
level = detectFromEffectivePlugins(pm.getPlugins());
if (level > 0) {
return level;
}
}
}
return -1;
}
private int detectFromEffectivePlugins(List<Plugin> plugins) {
for (Plugin plugin : plugins) {
String groupId = plugin.getGroupId();
if (MAVEN_COMPILER_PLUGIN.equals(plugin.getArtifactId())
&& (groupId == null || groupId.isEmpty() || TOOLCHAINS_PLUGIN_GROUP_ID.equals(groupId))) {
XmlNode config = plugin.getConfiguration();
if (config != null) {
// <release> takes precedence
XmlNode releaseNode = config.child("release");
if (releaseNode != null
&& releaseNode.value() != null
&& !releaseNode.value().isBlank()) {
int level = JdkSourceLevelSupport.normalizeSourceLevel(
releaseNode.value().trim());
if (level > 0) {
return level;
}
}
XmlNode sourceNode = config.child("source");
if (sourceNode != null
&& sourceNode.value() != null
&& !sourceNode.value().isBlank()) {
int level = JdkSourceLevelSupport.normalizeSourceLevel(
sourceNode.value().trim());
if (level > 0) {
return level;
}
}
}
}
}
return -1;
}
/**
* Checks whether the POM already has the {@code maven-toolchains-plugin}
* configured with the {@code select-jdk-toolchain} goal.
*/
boolean hasToolchainsPluginWithSelectGoal(Document pomDocument) {
Element root = pomDocument.root();
Element build = root.childElement(BUILD).orElse(null);
if (build == null) {
return false;
}
// Check both <plugins> and <pluginManagement><plugins>
if (hasSelectGoalInPluginSection(build.childElement(PLUGINS).orElse(null))) {
return true;
}
Element pluginManagement = build.childElement(PLUGIN_MANAGEMENT).orElse(null);
if (pluginManagement != null) {
return hasSelectGoalInPluginSection(
pluginManagement.childElement(PLUGINS).orElse(null));
}
return false;
}
private boolean hasSelectGoalInPluginSection(Element pluginsElement) {
if (pluginsElement == null) {
return false;
}
for (Element plugin : pluginsElement.childElements(PLUGIN).toList()) {
if (isToolchainsPlugin(plugin)) {
// Check if it has the select-jdk-toolchain goal in any execution
Element executions = plugin.childElement("executions").orElse(null);
if (executions != null) {
for (Element execution :
executions.childElements("execution").toList()) {
Element goals = execution.childElement("goals").orElse(null);
if (goals != null) {
for (Element goal : goals.childElements("goal").toList()) {
if (SELECT_JDK_TOOLCHAIN_GOAL.equals(goal.textContentTrimmed())) {
return true;
}
}
}
}
}
}
}
return false;
}
/**
* Adds the {@code maven-toolchains-plugin} with {@code select-jdk-toolchain} goal
* to the POM's {@code <build><plugins>} section, configured with a version constraint
* so the plugin selects a JDK that supports the project's source level.
*
* <p>If the plugin already exists in the POM (e.g. with the older {@code toolchain} goal),
* the {@code select-jdk-toolchain} execution is appended to the existing entry.
* Version upgrades are handled by {@link PluginUpgradeStrategy}, which runs before
* this strategy and ensures the plugin is at least version {@value #TOOLCHAINS_PLUGIN_MIN_VERSION}.
*
* @param pomDocument the POM document to modify
* @param maxJdkVersion the latest JDK major version that supports the source level
*/
void addToolchainsPlugin(Document pomDocument, int maxJdkVersion) {
Element root = pomDocument.root();
Element build = root.childElement(BUILD).orElseGet(() -> DomUtils.insertNewElement(BUILD, root));
Element plugins = build.childElement(PLUGINS).orElseGet(() -> DomUtils.insertNewElement(PLUGINS, build));
// Look for an existing toolchains-plugin entry (may have only the older 'toolchain' goal).
// If present, its version has already been upgraded by PluginUpgradeStrategy (priority 10).
Element existingPlugin = findToolchainsPlugin(plugins);
final Element plugin;
if (existingPlugin != null) {
plugin = existingPlugin;
} else {
// New plugin entry ��� set version to the minimum that has select-jdk-toolchain
plugin = DomUtils.createPlugin(
plugins, TOOLCHAINS_PLUGIN_GROUP_ID, MAVEN_TOOLCHAINS_PLUGIN, TOOLCHAINS_PLUGIN_MIN_VERSION);
}
Element executions =
plugin.childElement("executions").orElseGet(() -> DomUtils.insertNewElement("executions", plugin));
Element execution = DomUtils.insertNewElement("execution", executions);
Element goals = DomUtils.insertNewElement("goals", execution);
DomUtils.insertContentElement(goals, "goal", SELECT_JDK_TOOLCHAIN_GOAL);
Element configuration = DomUtils.insertNewElement(CONFIGURATION, execution);
DomUtils.insertContentElement(configuration, "version", "(," + maxJdkVersion + "]");
}
/**
* Checks whether a plugin element is the {@code maven-toolchains-plugin}.
*/
private static boolean isToolchainsPlugin(Element plugin) {
String artifactId = plugin.childTextTrimmed(ARTIFACT_ID);
String groupId = plugin.childTextTrimmed(GROUP_ID);
return MAVEN_TOOLCHAINS_PLUGIN.equals(artifactId)
&& (groupId == null || groupId.isEmpty() || TOOLCHAINS_PLUGIN_GROUP_ID.equals(groupId));
}
/**
* Finds an existing {@code maven-toolchains-plugin} element in a plugins section.
*
* @return the plugin element, or {@code null} if not found
*/
private Element findToolchainsPlugin(Element pluginsElement) {
for (Element plugin : pluginsElement.childElements(PLUGIN).toList()) {
if (isToolchainsPlugin(plugin)) {
return plugin;
}
}
return null;
}
/**
* Returns the major version of the running JDK.
* Extracted as a method so tests can override it.
*/
int getRunningJdkMajor() {
return JdkSourceLevelSupport.getRunningJdkMajor();
}
}