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.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.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";
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) {
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;
}
/**
* 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()) {
String artifactId = plugin.childTextTrimmed(ARTIFACT_ID);
String groupId = plugin.childTextTrimmed(GROUP_ID);
if (MAVEN_TOOLCHAINS_PLUGIN.equals(artifactId)
&& (groupId == null || groupId.isEmpty() || TOOLCHAINS_PLUGIN_GROUP_ID.equals(groupId))) {
// 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.
*
* @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));
Element plugin = DomUtils.createPlugin(plugins, TOOLCHAINS_PLUGIN_GROUP_ID, MAVEN_TOOLCHAINS_PLUGIN, null);
Element executions = 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 + "]");
}
/**
* Returns the major version of the running JDK.
* Extracted as a method so tests can override it.
*/
int getRunningJdkMajor() {
return JdkSourceLevelSupport.getRunningJdkMajor();
}
}