You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by gn...@apache.org on 2026/09/02 17:02:18 UTC
(maven) branch backport/fix-12989-to-4.0.x created (now 6b0b5166e5)
This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a change to branch backport/fix-12989-to-4.0.x
in repository https://gitbox.apache.org/repos/asf/maven.git
at 6b0b5166e5 Fix #12989: mvnup plugin upgrades should respect project JDK version
This branch includes the following new commits:
new 6b0b5166e5 Fix #12989: mvnup plugin upgrades should respect project JDK version
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(maven) 01/01: Fix #12989: mvnup plugin upgrades should respect project JDK version
Posted by gn...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch backport/fix-12989-to-4.0.x
in repository https://gitbox.apache.org/repos/asf/maven.git
commit 6b0b5166e526584f316e526abb9e597fe907d898
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Wed Sep 2 18:50:12 2026 +0200
Fix #12989: mvnup plugin upgrades should respect project JDK version
* Fix #12989: mvnup plugin upgrades should respect project JDK version
Add JDK compatibility checking to PluginUpgradeStrategy so that plugins
requiring a higher JDK than the project targets are skipped rather than
blindly upgraded. This prevents UnsupportedClassVersionError at build
time (e.g., checkstyle-plugin 3.6.0 pulls Checkstyle 10.x which
requires JDK 21, breaking projects that target JDK 17).
Changes:
- Add minJdk field to PluginUpgrade record and PluginUpgradeInfo class
- Add detectProjectJdkVersion() method that reads maven.compiler.release,
maven.compiler.source, maven.compiler.target, and compiler plugin config
- Add JDK compatibility guard in upgradePluginVersion() (similar to the
existing Quarkus 2.x skip pattern)
- Add JDK compatibility guard in effective model analysis path
- Register maven-checkstyle-plugin 3.6.0 with minJdk=21
- Add comprehensive tests in PluginUpgradeJdkCompatibilityTest
Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
* Address review feedback: extract JDK skip helper, sort POMs by path depth
- Extract duplicated JDK compatibility check into shouldSkipForJdkIncompatibility()
helper method, eliminating the inline logic in both build/plugins and
pluginManagement/plugins analysis paths
- Sort pomMap entries by path depth (shortest first) when detecting the project
JDK version, so the root POM is checked before child modules in multi-module
projects
Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <no...@anthropic.com>
---
.../cling/invoker/mvnup/goals/PluginUpgrade.java | 16 +-
.../invoker/mvnup/goals/PluginUpgradeStrategy.java | 212 +++++++-
.../goals/PluginUpgradeJdkCompatibilityTest.java | 596 +++++++++++++++++++++
3 files changed, 813 insertions(+), 11 deletions(-)
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/PluginUpgrade.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/PluginUpgrade.java
index e6895cfd57..820a7dd84d 100644
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/PluginUpgrade.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/PluginUpgrade.java
@@ -30,14 +30,24 @@
* the plugin has no 4.x pre-release line. Used to upgrade old 4.x alpha/beta/RC versions
* to the latest pre-release rather than downgrading to a 3.x version.
* @param reason the reason why this plugin needs to be upgraded
+ * @param minJdk the minimum JDK version required by this plugin version, or {@code 0} if
+ * the plugin works with any JDK. When the project's detected JDK is below this value,
+ * the upgrade is skipped to avoid {@code UnsupportedClassVersionError}.
*/
public record PluginUpgrade(
- String groupId, String artifactId, String minVersion, String latestPreRelease, String reason) {
+ String groupId, String artifactId, String minVersion, String latestPreRelease, String reason, int minJdk) {
/**
- * Convenience constructor for plugins without a 4.x pre-release line.
+ * Convenience constructor for plugins without a 4.x pre-release line and no JDK requirement.
*/
public PluginUpgrade(String groupId, String artifactId, String minVersion, String reason) {
- this(groupId, artifactId, minVersion, null, reason);
+ this(groupId, artifactId, minVersion, null, reason, 0);
+ }
+
+ /**
+ * Convenience constructor for plugins with a 4.x pre-release line but no JDK requirement.
+ */
+ public PluginUpgrade(String groupId, String artifactId, String minVersion, String latestPreRelease, String reason) {
+ this(groupId, artifactId, minVersion, latestPreRelease, reason, 0);
}
}
diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/PluginUpgradeStrategy.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/PluginUpgradeStrategy.java
index 3f853e7aa9..6e3145569d 100644
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/PluginUpgradeStrategy.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/PluginUpgradeStrategy.java
@@ -20,6 +20,7 @@
import java.nio.file.Path;
import java.util.ArrayList;
+import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -42,6 +43,7 @@
import org.apache.maven.api.model.Plugin;
import org.apache.maven.api.model.PluginManagement;
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;
@@ -169,7 +171,14 @@ public class PluginUpgradeStrategy extends AbstractUpgradeStrategy {
"bnd-maven-plugin",
"5.1.0",
"Versions before 5.1.0 have internal collection mutation bugs (FELIX-6259)"
- + " that throw ConcurrentModificationException on JDK 17+"));
+ + " that throw ConcurrentModificationException on JDK 17+"),
+ new PluginUpgrade(
+ DEFAULT_MAVEN_PLUGIN_GROUP_ID,
+ "maven-checkstyle-plugin",
+ "3.6.0",
+ null,
+ MAVEN_4_COMPATIBILITY_REASON,
+ 21));
private static final List<PluginUpgrade> PLUGIN_DEPENDENCY_UPGRADES = List.of(new PluginUpgrade(
"org.codehaus.mojo",
@@ -341,7 +350,8 @@ private Map<String, PluginUpgradeInfo> getPluginUpgradesMap() {
upgrade.groupId(),
upgrade.artifactId(),
upgrade.minVersion(),
- upgrade.latestPreRelease())));
+ upgrade.latestPreRelease(),
+ upgrade.minJdk())));
}
/**
@@ -453,6 +463,22 @@ private boolean upgradePluginVersion(
}
}
+ // Check JDK compatibility: if the plugin requires a higher JDK than the project
+ // uses, skip the upgrade to avoid UnsupportedClassVersionError at build time.
+ if (upgrade.minJdk > 0) {
+ int projectJdk = detectProjectJdkVersion(pomDocument);
+ if (shouldSkipForJdkIncompatibility(
+ context,
+ upgrade.groupId,
+ upgrade.artifactId,
+ upgrade.minVersion,
+ upgrade.minJdk,
+ projectJdk,
+ sectionName)) {
+ return false;
+ }
+ }
+
if (isProperty) {
// For Quarkus plugins, check if the property is shared with a Quarkus BOM
if (isQuarkusPlugin(upgrade.groupId, upgrade.artifactId)
@@ -699,6 +725,20 @@ private PluginAnalysisResults analyzePluginsUsingEffectiveModels(
Map<String, PluginUpgrade> basePluginUpgrades = getPluginUpgradesAsMap();
String shadePluginKey = DEFAULT_MAVEN_PLUGIN_GROUP_ID + ":maven-shade-plugin";
+ // Detect the project JDK version, preferring the root POM (shortest path depth).
+ // In multi-module projects, child modules may declare a different JDK level,
+ // so we sort by path depth to check the root POM first.
+ int projectJdk = -1;
+ List<Map.Entry<Path, Document>> sortedEntries = pomMap.entrySet().stream()
+ .sorted(Comparator.comparingInt(e -> e.getKey().getNameCount()))
+ .toList();
+ for (Map.Entry<Path, Document> jdkEntry : sortedEntries) {
+ projectJdk = detectProjectJdkVersion(jdkEntry.getValue());
+ if (projectJdk > 0) {
+ break;
+ }
+ }
+
for (Map.Entry<Path, Document> entry : pomMap.entrySet()) {
Path originalPomPath = entry.getKey();
@@ -720,7 +760,8 @@ private PluginAnalysisResults analyzePluginsUsingEffectiveModels(
}
// Build effective model using Maven 4 API
- PluginAnalysis analysis = analyzeEffectiveModelForPlugins(context, tempPomPath, pluginUpgrades);
+ PluginAnalysis analysis =
+ analyzeEffectiveModelForPlugins(context, tempPomPath, pluginUpgrades, projectJdk);
// Determine where to add plugin management (last local parent)
Path targetPom =
@@ -843,9 +884,9 @@ private Map<String, PluginUpgrade> getPluginUpgradesAsMap() {
}
private PluginAnalysis analyzeEffectiveModelForPlugins(
- UpgradeContext context, Path tempPomPath, Map<String, PluginUpgrade> pluginUpgrades) {
+ UpgradeContext context, Path tempPomPath, Map<String, PluginUpgrade> pluginUpgrades, int projectJdk) {
Model effectiveModel = buildEffectiveModel(context, tempPomPath);
- return analyzePluginsFromEffectiveModel(context, effectiveModel, pluginUpgrades);
+ return analyzePluginsFromEffectiveModel(context, effectiveModel, pluginUpgrades, projectJdk);
}
/**
@@ -854,7 +895,7 @@ private PluginAnalysis analyzeEffectiveModelForPlugins(
* explicitly in an inherited parent's build/plugins, not via pluginManagement).
*/
private PluginAnalysis analyzePluginsFromEffectiveModel(
- UpgradeContext context, Model effectiveModel, Map<String, PluginUpgrade> pluginUpgrades) {
+ UpgradeContext context, Model effectiveModel, Map<String, PluginUpgrade> pluginUpgrades, int projectJdk) {
Set<String> needsManagement = new HashSet<>();
Set<String> needsDirectOverride = new HashSet<>();
@@ -875,6 +916,16 @@ private PluginAnalysis analyzePluginsFromEffectiveModel(
String pluginKey = getPluginKey(plugin);
PluginUpgrade upgrade = pluginUpgrades.get(pluginKey);
if (upgrade != null) {
+ if (shouldSkipForJdkIncompatibility(
+ context,
+ upgrade.groupId(),
+ upgrade.artifactId(),
+ upgrade.minVersion(),
+ upgrade.minJdk(),
+ projectJdk,
+ "effective model")) {
+ continue;
+ }
String effectiveVersion = plugin.getVersion();
if (isVersionBelow(context, effectiveVersion, upgrade.minVersion())) {
needsManagement.add(pluginKey);
@@ -901,6 +952,16 @@ private PluginAnalysis analyzePluginsFromEffectiveModel(
String pluginKey = getPluginKey(plugin);
PluginUpgrade upgrade = pluginUpgrades.get(pluginKey);
if (upgrade != null && !needsManagement.contains(pluginKey)) {
+ if (shouldSkipForJdkIncompatibility(
+ context,
+ upgrade.groupId(),
+ upgrade.artifactId(),
+ upgrade.minVersion(),
+ upgrade.minJdk(),
+ projectJdk,
+ "effective model")) {
+ continue;
+ }
String effectiveVersion = plugin.getVersion();
if (isVersionBelow(context, effectiveVersion, upgrade.minVersion())) {
needsManagement.add(pluginKey);
@@ -1232,6 +1293,101 @@ private void collectCustomTransformers(Element configElement, List<String> custo
}));
}
+ /**
+ * Detects the project's JDK version from the POM document.
+ * Checks the following sources in order:
+ * <ol>
+ * <li>{@code maven.compiler.release} property</li>
+ * <li>{@code maven.compiler.source} property</li>
+ * <li>{@code maven.compiler.target} property</li>
+ * <li>Compiler plugin configuration ({@code <release>} or {@code <source>})</li>
+ * </ol>
+ *
+ * @param pomDocument the POM document to inspect
+ * @return the detected JDK major version, or {@code -1} if not found
+ */
+ int detectProjectJdkVersion(Document pomDocument) {
+ Element root = pomDocument.root();
+
+ // Check properties: maven.compiler.release takes precedence, then source, then target
+ Element properties = root.childElement(PROPERTIES).orElse(null);
+ if (properties != null) {
+ for (String propName :
+ List.of("maven.compiler.release", "maven.compiler.source", "maven.compiler.target")) {
+ Element propElement = properties.childElement(propName).orElse(null);
+ if (propElement != null) {
+ String value = propElement.textContentTrimmed();
+ if (value != null && !value.isEmpty() && !value.startsWith("${")) {
+ int level = JdkSourceLevelSupport.normalizeSourceLevel(value);
+ if (level > 0) {
+ return level;
+ }
+ }
+ }
+ }
+ }
+
+ // Check compiler plugin configuration
+ Element build = root.childElement(BUILD).orElse(null);
+ if (build != null) {
+ int level = detectJdkFromPluginSection(build.childElement(PLUGINS).orElse(null));
+ if (level > 0) {
+ return level;
+ }
+
+ Element pluginManagement = build.childElement(PLUGIN_MANAGEMENT).orElse(null);
+ if (pluginManagement != null) {
+ level = detectJdkFromPluginSection(
+ pluginManagement.childElement(PLUGINS).orElse(null));
+ if (level > 0) {
+ return level;
+ }
+ }
+ }
+
+ return -1;
+ }
+
+ private int detectJdkFromPluginSection(Element pluginsElement) {
+ if (pluginsElement == null) {
+ return -1;
+ }
+
+ for (Element plugin : pluginsElement.childElements(PLUGIN).toList()) {
+ String artifactId = getChildText(plugin, ARTIFACT_ID);
+ String groupId = getChildText(plugin, GROUP_ID);
+ if ("maven-compiler-plugin".equals(artifactId)
+ && (groupId == null || groupId.isEmpty() || DEFAULT_MAVEN_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) {
+ String text = releaseNode.textContentTrimmed();
+ if (text != null && !text.isEmpty() && !text.startsWith("${")) {
+ int level = JdkSourceLevelSupport.normalizeSourceLevel(text);
+ if (level > 0) {
+ return level;
+ }
+ }
+ }
+ Element sourceNode = config.childElement("source").orElse(null);
+ if (sourceNode != null) {
+ String text = sourceNode.textContentTrimmed();
+ if (text != null && !text.isEmpty() && !text.startsWith("${")) {
+ int level = JdkSourceLevelSupport.normalizeSourceLevel(text);
+ if (level > 0) {
+ return level;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return -1;
+ }
+
/**
* Checks if the given plugin is a Quarkus Maven plugin.
*/
@@ -1240,6 +1396,36 @@ private boolean isQuarkusPlugin(String groupId, String artifactId) {
&& ("io.quarkus".equals(groupId) || "io.quarkus.platform".equals(groupId));
}
+ /**
+ * Checks whether a plugin upgrade should be skipped because the plugin requires a higher JDK
+ * than the project targets. If the plugin has a {@code minJdk} constraint and the project's
+ * detected JDK is below it, emits a warning and returns {@code true}.
+ *
+ * @param context the upgrade context for logging
+ * @param groupId the plugin's groupId
+ * @param artifactId the plugin's artifactId
+ * @param minVersion the target upgrade version
+ * @param minJdk the minimum JDK version required by the plugin, or {@code 0} if unrestricted
+ * @param projectJdk the project's detected JDK major version, or {@code -1} if unknown
+ * @param location description of where the plugin was found (for logging)
+ * @return {@code true} if the upgrade should be skipped, {@code false} otherwise
+ */
+ private boolean shouldSkipForJdkIncompatibility(
+ UpgradeContext context,
+ String groupId,
+ String artifactId,
+ String minVersion,
+ int minJdk,
+ int projectJdk,
+ String location) {
+ if (minJdk > 0 && projectJdk > 0 && projectJdk < minJdk) {
+ context.warning("Skipping " + groupId + ":" + artifactId + " upgrade to " + minVersion + " in " + location
+ + ": plugin requires JDK " + minJdk + " but project targets JDK " + projectJdk);
+ return true;
+ }
+ return false;
+ }
+
/**
* Detects the Quarkus platform version used by the project.
*
@@ -1495,6 +1681,9 @@ public static class PluginUpgradeInfo {
/** The latest available 4.x pre-release version, or null if none exists */
final String latestPreRelease;
+ /** The minimum JDK version required by this plugin, or 0 if any JDK works */
+ final int minJdk;
+
/**
* Creates a new plugin upgrade information holder.
*
@@ -1506,16 +1695,23 @@ public static class PluginUpgradeInfo {
* the minimum version required for Maven 4 compatibility
* @param latestPreRelease
* the latest 4.x pre-release version, or null
+ * @param minJdk
+ * the minimum JDK version required by this plugin, or 0 if any JDK works
*/
- PluginUpgradeInfo(String groupId, String artifactId, String minVersion, String latestPreRelease) {
+ PluginUpgradeInfo(String groupId, String artifactId, String minVersion, String latestPreRelease, int minJdk) {
this.groupId = groupId;
this.artifactId = artifactId;
this.minVersion = minVersion;
this.latestPreRelease = latestPreRelease;
+ this.minJdk = minJdk;
+ }
+
+ PluginUpgradeInfo(String groupId, String artifactId, String minVersion, String latestPreRelease) {
+ this(groupId, artifactId, minVersion, latestPreRelease, 0);
}
PluginUpgradeInfo(String groupId, String artifactId, String minVersion) {
- this(groupId, artifactId, minVersion, null);
+ this(groupId, artifactId, minVersion, null, 0);
}
}
}
diff --git a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/PluginUpgradeJdkCompatibilityTest.java b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/PluginUpgradeJdkCompatibilityTest.java
new file mode 100644
index 0000000000..6cb8936f54
--- /dev/null
+++ b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/PluginUpgradeJdkCompatibilityTest.java
@@ -0,0 +1,596 @@
+/*
+ * 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.nio.file.Paths;
+import java.util.Map;
+
+import eu.maveniverse.domtrip.Document;
+import eu.maveniverse.domtrip.Editor;
+import eu.maveniverse.domtrip.Element;
+import org.apache.maven.cling.invoker.mvnup.UpgradeContext;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests for JDK compatibility checking in {@link PluginUpgradeStrategy}.
+ * Verifies that plugin upgrades requiring a higher JDK than the project targets
+ * are skipped to avoid {@code UnsupportedClassVersionError}.
+ *
+ * @see <a href="https://github.com/apache/maven/issues/12989">apache/maven#12989</a>
+ */
+@DisplayName("PluginUpgradeStrategy — JDK Compatibility")
+class PluginUpgradeJdkCompatibilityTest {
+
+ private PluginUpgradeStrategy strategy;
+
+ @BeforeEach
+ void setUp() {
+ strategy = new PluginUpgradeStrategy();
+ }
+
+ private UpgradeContext createMockContext() {
+ return TestUtils.createMockContext();
+ }
+
+ @Nested
+ @DisplayName("Project JDK Detection")
+ class ProjectJdkDetectionTests {
+
+ @Test
+ @DisplayName("should detect JDK version from maven.compiler.release property")
+ void shouldDetectFromCompilerRelease() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>test</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0.0</version>
+ <properties>
+ <maven.compiler.release>17</maven.compiler.release>
+ </properties>
+ </project>
+ """;
+
+ Document document = Document.of(pomXml);
+ assertEquals(17, strategy.detectProjectJdkVersion(document));
+ }
+
+ @Test
+ @DisplayName("should detect JDK version from maven.compiler.source property")
+ void shouldDetectFromCompilerSource() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>test</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0.0</version>
+ <properties>
+ <maven.compiler.source>11</maven.compiler.source>
+ </properties>
+ </project>
+ """;
+
+ Document document = Document.of(pomXml);
+ assertEquals(11, strategy.detectProjectJdkVersion(document));
+ }
+
+ @Test
+ @DisplayName("should detect JDK version from maven.compiler.target property")
+ void shouldDetectFromCompilerTarget() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>test</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0.0</version>
+ <properties>
+ <maven.compiler.target>17</maven.compiler.target>
+ </properties>
+ </project>
+ """;
+
+ Document document = Document.of(pomXml);
+ assertEquals(17, strategy.detectProjectJdkVersion(document));
+ }
+
+ @Test
+ @DisplayName("should prefer maven.compiler.release over source/target")
+ void shouldPreferReleaseOverSource() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>test</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0.0</version>
+ <properties>
+ <maven.compiler.release>21</maven.compiler.release>
+ <maven.compiler.source>17</maven.compiler.source>
+ <maven.compiler.target>17</maven.compiler.target>
+ </properties>
+ </project>
+ """;
+
+ Document document = Document.of(pomXml);
+ assertEquals(21, strategy.detectProjectJdkVersion(document));
+ }
+
+ @Test
+ @DisplayName("should normalize old-style version like 1.8 to 8")
+ void shouldNormalizeOldStyleVersion() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>test</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0.0</version>
+ <properties>
+ <maven.compiler.source>1.8</maven.compiler.source>
+ </properties>
+ </project>
+ """;
+
+ Document document = Document.of(pomXml);
+ assertEquals(8, strategy.detectProjectJdkVersion(document));
+ }
+
+ @Test
+ @DisplayName("should detect JDK version from compiler plugin configuration")
+ void shouldDetectFromCompilerPluginConfig() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>test</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0.0</version>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <release>17</release>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ </project>
+ """;
+
+ Document document = Document.of(pomXml);
+ assertEquals(17, strategy.detectProjectJdkVersion(document));
+ }
+
+ @Test
+ @DisplayName("should return -1 when no JDK version is configured")
+ void shouldReturnNegativeOneWhenNoJdkConfigured() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>test</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0.0</version>
+ </project>
+ """;
+
+ Document document = Document.of(pomXml);
+ assertEquals(-1, strategy.detectProjectJdkVersion(document));
+ }
+
+ @Test
+ @DisplayName("should skip property references (${...}) and return -1")
+ void shouldSkipPropertyReferences() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>test</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0.0</version>
+ <properties>
+ <maven.compiler.release>${java.version}</maven.compiler.release>
+ </properties>
+ </project>
+ """;
+
+ Document document = Document.of(pomXml);
+ assertEquals(-1, strategy.detectProjectJdkVersion(document));
+ }
+ }
+
+ @Nested
+ @DisplayName("JDK Compatibility Skip")
+ class JdkCompatibilitySkipTests {
+
+ @Test
+ @DisplayName("should skip checkstyle-plugin upgrade when project targets JDK 17")
+ void shouldSkipCheckstyleUpgradeForJdk17() throws Exception {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>test</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0.0</version>
+ <properties>
+ <maven.compiler.release>17</maven.compiler.release>
+ </properties>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-checkstyle-plugin</artifactId>
+ <version>3.4.0</version>
+ </plugin>
+ </plugins>
+ </build>
+ </project>
+ """;
+
+ Document document = Document.of(pomXml);
+ Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
+
+ UpgradeContext context = createMockContext();
+ UpgradeResult result = strategy.doApply(context, pomMap);
+
+ assertTrue(result.success(), "Strategy should succeed");
+
+ String xml = DomUtils.toXml(document);
+ assertTrue(
+ xml.contains("<version>3.4.0</version>"),
+ "checkstyle-plugin should NOT be upgraded when project targets JDK 17");
+ assertFalse(xml.contains("<version>3.6.0</version>"), "checkstyle-plugin should NOT be set to 3.6.0");
+ }
+
+ @Test
+ @DisplayName("should skip checkstyle-plugin upgrade when project targets JDK 11")
+ void shouldSkipCheckstyleUpgradeForJdk11() throws Exception {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>test</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0.0</version>
+ <properties>
+ <maven.compiler.source>11</maven.compiler.source>
+ <maven.compiler.target>11</maven.compiler.target>
+ </properties>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-checkstyle-plugin</artifactId>
+ <version>3.3.0</version>
+ </plugin>
+ </plugins>
+ </build>
+ </project>
+ """;
+
+ Document document = Document.of(pomXml);
+ Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
+
+ UpgradeContext context = createMockContext();
+ UpgradeResult result = strategy.doApply(context, pomMap);
+
+ assertTrue(result.success(), "Strategy should succeed");
+
+ String xml = DomUtils.toXml(document);
+ assertTrue(
+ xml.contains("<version>3.3.0</version>"),
+ "checkstyle-plugin should NOT be upgraded when project targets JDK 11");
+ }
+
+ @Test
+ @DisplayName("should upgrade checkstyle-plugin when project targets JDK 21")
+ void shouldUpgradeCheckstyleForJdk21() throws Exception {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>test</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0.0</version>
+ <properties>
+ <maven.compiler.release>21</maven.compiler.release>
+ </properties>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-checkstyle-plugin</artifactId>
+ <version>3.4.0</version>
+ </plugin>
+ </plugins>
+ </build>
+ </project>
+ """;
+
+ Document document = Document.of(pomXml);
+ Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
+
+ UpgradeContext context = createMockContext();
+ UpgradeResult result = strategy.doApply(context, pomMap);
+
+ assertTrue(result.success(), "Strategy should succeed");
+
+ Editor editor = new Editor(document);
+ String version = editor.root()
+ .path("build", "plugins", "plugin", "version")
+ .map(Element::textContentTrimmed)
+ .orElse(null);
+ assertEquals("3.6.0", version, "checkstyle-plugin should be upgraded to 3.6.0 for JDK 21");
+ }
+
+ @Test
+ @DisplayName("should upgrade checkstyle-plugin when project targets JDK 23")
+ void shouldUpgradeCheckstyleForJdk23() throws Exception {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>test</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0.0</version>
+ <properties>
+ <maven.compiler.release>23</maven.compiler.release>
+ </properties>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-checkstyle-plugin</artifactId>
+ <version>3.4.0</version>
+ </plugin>
+ </plugins>
+ </build>
+ </project>
+ """;
+
+ Document document = Document.of(pomXml);
+ Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
+
+ UpgradeContext context = createMockContext();
+ UpgradeResult result = strategy.doApply(context, pomMap);
+
+ assertTrue(result.success(), "Strategy should succeed");
+
+ Editor editor = new Editor(document);
+ String version = editor.root()
+ .path("build", "plugins", "plugin", "version")
+ .map(Element::textContentTrimmed)
+ .orElse(null);
+ assertEquals("3.6.0", version, "checkstyle-plugin should be upgraded to 3.6.0 for JDK 23");
+ }
+
+ @Test
+ @DisplayName("should upgrade checkstyle-plugin when no JDK version is configured")
+ void shouldUpgradeCheckstyleWhenNoJdkConfigured() throws Exception {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>test</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0.0</version>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-checkstyle-plugin</artifactId>
+ <version>3.4.0</version>
+ </plugin>
+ </plugins>
+ </build>
+ </project>
+ """;
+
+ Document document = Document.of(pomXml);
+ Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
+
+ UpgradeContext context = createMockContext();
+ UpgradeResult result = strategy.doApply(context, pomMap);
+
+ assertTrue(result.success(), "Strategy should succeed");
+
+ Editor editor = new Editor(document);
+ String version = editor.root()
+ .path("build", "plugins", "plugin", "version")
+ .map(Element::textContentTrimmed)
+ .orElse(null);
+ assertEquals("3.6.0", version, "checkstyle-plugin should be upgraded when no JDK version is configured");
+ }
+
+ @Test
+ @DisplayName("should skip checkstyle-plugin upgrade via property version for JDK 17")
+ void shouldSkipCheckstylePropertyUpgradeForJdk17() throws Exception {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>test</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0.0</version>
+ <properties>
+ <maven.compiler.release>17</maven.compiler.release>
+ <checkstyle-plugin.version>3.4.0</checkstyle-plugin.version>
+ </properties>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-checkstyle-plugin</artifactId>
+ <version>${checkstyle-plugin.version}</version>
+ </plugin>
+ </plugins>
+ </build>
+ </project>
+ """;
+
+ Document document = Document.of(pomXml);
+ Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
+
+ UpgradeContext context = createMockContext();
+ UpgradeResult result = strategy.doApply(context, pomMap);
+
+ assertTrue(result.success(), "Strategy should succeed");
+
+ String xml = DomUtils.toXml(document);
+ assertTrue(
+ xml.contains("<checkstyle-plugin.version>3.4.0</checkstyle-plugin.version>"),
+ "checkstyle-plugin property should NOT be upgraded for JDK 17");
+ }
+
+ @Test
+ @DisplayName("should not skip plugins without minJdk requirement")
+ void shouldNotSkipPluginsWithoutMinJdk() throws Exception {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>test</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0.0</version>
+ <properties>
+ <maven.compiler.release>11</maven.compiler.release>
+ </properties>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <version>3.0.0</version>
+ </plugin>
+ </plugins>
+ </build>
+ </project>
+ """;
+
+ Document document = Document.of(pomXml);
+ Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
+
+ UpgradeContext context = createMockContext();
+ UpgradeResult result = strategy.doApply(context, pomMap);
+
+ assertTrue(result.success(), "Strategy should succeed");
+
+ Editor editor = new Editor(document);
+ String version = editor.root()
+ .path("build", "plugins", "plugin", "version")
+ .map(Element::textContentTrimmed)
+ .orElse(null);
+ assertEquals("3.5.2", version, "surefire-plugin should still be upgraded (no minJdk requirement)");
+ }
+
+ @Test
+ @DisplayName("should skip checkstyle-plugin in pluginManagement for JDK 17")
+ void shouldSkipCheckstyleInPluginManagementForJdk17() throws Exception {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>test</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0.0</version>
+ <properties>
+ <maven.compiler.release>17</maven.compiler.release>
+ </properties>
+ <build>
+ <pluginManagement>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-checkstyle-plugin</artifactId>
+ <version>3.4.0</version>
+ </plugin>
+ </plugins>
+ </pluginManagement>
+ </build>
+ </project>
+ """;
+
+ Document document = Document.of(pomXml);
+ Map<Path, Document> pomMap = Map.of(Paths.get("pom.xml"), document);
+
+ UpgradeContext context = createMockContext();
+ UpgradeResult result = strategy.doApply(context, pomMap);
+
+ assertTrue(result.success(), "Strategy should succeed");
+
+ String xml = DomUtils.toXml(document);
+ assertTrue(
+ xml.contains("<version>3.4.0</version>"),
+ "checkstyle-plugin in pluginManagement should NOT be upgraded for JDK 17");
+ }
+ }
+
+ @Nested
+ @DisplayName("PluginUpgrade Record")
+ class PluginUpgradeRecordTests {
+
+ @Test
+ @DisplayName("should have minJdk=0 for convenience constructor without JDK")
+ void shouldHaveDefaultMinJdk() {
+ PluginUpgrade upgrade = new PluginUpgrade("g", "a", "1.0", "reason");
+ assertEquals(0, upgrade.minJdk(), "Default minJdk should be 0");
+ }
+
+ @Test
+ @DisplayName("should have minJdk=0 for five-arg convenience constructor")
+ void shouldHaveDefaultMinJdkForFiveArgConstructor() {
+ PluginUpgrade upgrade = new PluginUpgrade("g", "a", "1.0", "2.0-beta-1", "reason");
+ assertEquals(0, upgrade.minJdk(), "Default minJdk should be 0 for five-arg constructor");
+ }
+
+ @Test
+ @DisplayName("should preserve minJdk value in full constructor")
+ void shouldPreserveMinJdk() {
+ PluginUpgrade upgrade = new PluginUpgrade("g", "a", "1.0", null, "reason", 21);
+ assertEquals(21, upgrade.minJdk(), "minJdk should be preserved");
+ }
+
+ @Test
+ @DisplayName("checkstyle plugin upgrade should have minJdk=21")
+ void checkstylePluginShouldHaveMinJdk21() {
+ PluginUpgrade checkstyleUpgrade = PluginUpgradeStrategy.getPluginUpgrades().stream()
+ .filter(u -> "maven-checkstyle-plugin".equals(u.artifactId()))
+ .findFirst()
+ .orElse(null);
+
+ assertTrue(checkstyleUpgrade != null, "checkstyle-plugin should be in PLUGIN_UPGRADES");
+ assertEquals(21, checkstyleUpgrade.minJdk(), "checkstyle-plugin should require JDK 21");
+ assertEquals("3.6.0", checkstyleUpgrade.minVersion(), "checkstyle-plugin minVersion should be 3.6.0");
+ }
+ }
+}