This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch maven-4.0.x
in repository https://gitbox.apache.org/repos/asf/maven.git
The following commit(s) were added to refs/heads/maven-4.0.x by this push:
new 2a721dd049 [Backport 4.0.x] Add mvnup SourceStrategy for migrating to
<source> elements (#12357)
2a721dd049 is described below
commit 2a721dd04958b3c5751d4aff53a090060eaa3867
Author: Guillaume Nodet <[email protected]>
AuthorDate: Thu Jun 25 20:02:43 2026 +0200
[Backport 4.0.x] Add mvnup SourceStrategy for migrating to <source>
elements (#12357)
* Add mvnup SourceStrategy for migrating to <source> elements
Adds a new mvnup upgrade strategy that migrates legacy source
configuration to Maven 4.1.0+ <source> elements. Handles four
migration phases:
- Compiler properties (maven.compiler.release, source/target)
to <source><targetVersion>
- Compiler plugin configuration (<release>, <source>/<target>)
to <source><targetVersion>
- Custom source/test directories to <source><directory>
- Resource/testResource sections to <source> with lang=resources
Applies when --model-version is 4.1.0+ or --all is set.
Runs at @Priority(20), after all other strategies.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* Use domtrip path() API in SourceStrategy tests
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* Fix review issues in SourceStrategy
- Check groupId in findCompilerPlugin (not just artifactId)
- Clean up compiler plugin config when properties already provide
targetVersion (previously left stale release/source/target elements)
- Clean up empty pluginManagement after plugin removal
- Clean up empty build element after all children removed
- Add test for pluginManagement compiler plugin migration
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* Extract copyPatternList to deduplicate includes/excludes handling
Address review feedback from desruisseaux: extract shared logic
from copyIncludesExcludes into a static copyPatternList method.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* Address Copilot review comments on SourceStrategy
- Reuse existing main/java <source> element when adding targetVersion
instead of always creating a new one (avoids duplicate source elements)
- Only remove compiler plugin config (release/source/target) when values
match the migrated property value (preserves intentional overrides)
- Add tests for both edge cases
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
---
.../cling/invoker/mvnup/goals/SourceStrategy.java | 559 ++++++++++++++
.../invoker/mvnup/goals/SourceStrategyTest.java | 860 +++++++++++++++++++++
2 files changed, 1419 insertions(+)
diff --git
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/SourceStrategy.java
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/SourceStrategy.java
new file mode 100644
index 0000000000..f6a6865255
--- /dev/null
+++
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/SourceStrategy.java
@@ -0,0 +1,559 @@
+/*
+ * 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.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.PROPERTIES;
+import static
eu.maveniverse.domtrip.maven.MavenPomElements.Elements.SOURCE_DIRECTORY;
+import static
eu.maveniverse.domtrip.maven.MavenPomElements.Elements.TEST_SOURCE_DIRECTORY;
+import static
eu.maveniverse.domtrip.maven.MavenPomElements.ModelVersions.MODEL_VERSION_4_1_0;
+import static
eu.maveniverse.domtrip.maven.MavenPomElements.Plugins.DEFAULT_MAVEN_PLUGIN_GROUP_ID;
+
+/**
+ * Strategy for migrating legacy source configuration to Maven 4.1.0+ {@code
<source>} elements.
+ *
+ * <p>Handles four migration phases:
+ * <ol>
+ * <li>Compiler properties ({@code maven.compiler.release}, {@code
maven.compiler.source/target})
+ * → {@code <source><targetVersion>}</li>
+ * <li>Compiler plugin configuration ({@code <release>}, {@code
<source>/<target>})
+ * → {@code <source><targetVersion>}</li>
+ * <li>Custom source/test directories → {@code <source>} with {@code
<directory>}</li>
+ * <li>Resource directories → {@code <source>} with {@code
<lang>resources</lang>}</li>
+ * </ol>
+ */
+@Named
+@Singleton
+@Priority(20)
+public class SourceStrategy extends AbstractUpgradeStrategy {
+
+ 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_TARGET =
"maven.compiler.target";
+ private static final String MAVEN_COMPILER_PLUGIN =
"maven-compiler-plugin";
+
+ private static final String DEFAULT_SOURCE_DIR = "src/main/java";
+ private static final String DEFAULT_TEST_SOURCE_DIR = "src/test/java";
+ private static final String DEFAULT_RESOURCE_DIR = "src/main/resources";
+ private static final String DEFAULT_TEST_RESOURCE_DIR =
"src/test/resources";
+
+ @Override
+ public boolean isApplicable(UpgradeContext context) {
+ UpgradeOptions options = getOptions(context);
+
+ if (options.all().orElse(false)) {
+ return true;
+ }
+
+ String modelVersion = options.modelVersion().orElse(null);
+ return modelVersion != null &&
ModelVersionUtils.isVersionGreaterOrEqual(modelVersion, MODEL_VERSION_4_1_0);
+ }
+
+ @Override
+ public String getDescription() {
+ return "Migrating source configuration to <source> elements";
+ }
+
+ @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);
+
+ String currentVersion =
ModelVersionUtils.detectModelVersion(pomDocument);
+ context.info(pomPath + " (current: " + currentVersion + ")");
+ context.indent();
+
+ try {
+ if (!ModelVersionUtils.isVersionGreaterOrEqual(currentVersion,
MODEL_VERSION_4_1_0)) {
+ context.success("Skipping (model version " +
currentVersion + " < 4.1.0)");
+ continue;
+ }
+
+ boolean hasChanges = migrateSources(context, pomDocument);
+
+ if (hasChanges) {
+ modifiedPoms.add(pomPath);
+ context.success("Source configuration migrated to <source>
elements");
+ } else {
+ context.success("No source configuration to migrate");
+ }
+ } catch (Exception e) {
+ context.failure("Failed to migrate source configuration: " +
e.getMessage());
+ errorPoms.add(pomPath);
+ } finally {
+ context.unindent();
+ }
+ }
+
+ return new UpgradeResult(processedPoms, modifiedPoms, errorPoms);
+ }
+
+ private boolean migrateSources(UpgradeContext context, Document
pomDocument) {
+ Element root = pomDocument.root();
+ boolean hasChanges = false;
+
+ String targetVersion = extractTargetVersionFromProperties(context,
root);
+
+ if (targetVersion == null) {
+ targetVersion = extractTargetVersionFromCompilerPlugin(context,
root);
+ } else {
+ cleanupCompilerPluginConfig(context, root, targetVersion);
+ }
+
+ if (targetVersion != null) {
+ Element sourcesElement = ensureSourcesElement(root);
+ Element sourceElement = findOrCreateMainJavaSource(sourcesElement);
+ DomUtils.insertContentElement(sourceElement, "targetVersion",
targetVersion);
+ context.detail("Set targetVersion: " + targetVersion);
+ hasChanges = true;
+ }
+
+ hasChanges |= migrateSourceDirectories(context, root);
+ hasChanges |= migrateResources(context, root);
+
+ cleanupEmptyBuild(root);
+
+ return hasChanges;
+ }
+
+ String extractTargetVersionFromProperties(UpgradeContext context, Element
root) {
+ Element properties = root.childElement(PROPERTIES).orElse(null);
+ if (properties == null) {
+ return null;
+ }
+
+ String targetVersion = null;
+
+ Element releaseElement =
properties.childElement(MAVEN_COMPILER_RELEASE).orElse(null);
+ if (releaseElement != null) {
+ targetVersion = releaseElement.textContent().trim();
+ DomUtils.removeElement(releaseElement);
+ context.detail("Migrated property: " + MAVEN_COMPILER_RELEASE + "
= " + targetVersion);
+
+ // Also remove source/target if present (release takes precedence)
+ properties.childElement(MAVEN_COMPILER_SOURCE).ifPresent(e -> {
+ DomUtils.removeElement(e);
+ context.detail("Removed property: " + MAVEN_COMPILER_SOURCE +
" (release takes precedence)");
+ });
+ properties.childElement(MAVEN_COMPILER_TARGET).ifPresent(e -> {
+ DomUtils.removeElement(e);
+ context.detail("Removed property: " + MAVEN_COMPILER_TARGET +
" (release takes precedence)");
+ });
+ } else {
+ Element sourceElement =
+
properties.childElement(MAVEN_COMPILER_SOURCE).orElse(null);
+ Element targetElement =
+
properties.childElement(MAVEN_COMPILER_TARGET).orElse(null);
+
+ if (sourceElement != null && targetElement != null) {
+ String sourceValue = sourceElement.textContent().trim();
+ String targetValue = targetElement.textContent().trim();
+
+ if (sourceValue.equals(targetValue)) {
+ targetVersion = sourceValue;
+ DomUtils.removeElement(sourceElement);
+ DomUtils.removeElement(targetElement);
+ context.detail("Migrated properties: " +
MAVEN_COMPILER_SOURCE + " = " + MAVEN_COMPILER_TARGET
+ + " = " + targetVersion);
+ }
+ }
+ }
+
+ if (targetVersion != null) {
+ removeIfEmpty(properties);
+ }
+
+ return targetVersion;
+ }
+
+ String extractTargetVersionFromCompilerPlugin(UpgradeContext context,
Element root) {
+ String targetVersion = extractFromPluginSection(
+ context,
+ root.childElement(BUILD).flatMap(b ->
b.childElement(PLUGINS)).orElse(null));
+
+ if (targetVersion == null) {
+ targetVersion = extractFromPluginSection(
+ context,
+ root.childElement(BUILD)
+ .flatMap(b -> b.childElement(PLUGIN_MANAGEMENT))
+ .flatMap(pm -> pm.childElement(PLUGINS))
+ .orElse(null));
+ }
+
+ return targetVersion;
+ }
+
+ private String extractFromPluginSection(UpgradeContext context, Element
pluginsElement) {
+ if (pluginsElement == null) {
+ return null;
+ }
+
+ Element compilerPlugin = findCompilerPlugin(pluginsElement);
+ if (compilerPlugin == null) {
+ return null;
+ }
+
+ Element configuration =
compilerPlugin.childElement(CONFIGURATION).orElse(null);
+ if (configuration == null) {
+ return null;
+ }
+
+ String targetVersion = null;
+
+ Element releaseElement =
configuration.childElement("release").orElse(null);
+ if (releaseElement != null) {
+ targetVersion = releaseElement.textContent().trim();
+ DomUtils.removeElement(releaseElement);
+ context.detail("Migrated compiler plugin <release>: " +
targetVersion);
+
+ configuration.childElement("source").ifPresent(e -> {
+ DomUtils.removeElement(e);
+ context.detail("Removed compiler plugin <source> (release
takes precedence)");
+ });
+ configuration.childElement("target").ifPresent(e -> {
+ DomUtils.removeElement(e);
+ context.detail("Removed compiler plugin <target> (release
takes precedence)");
+ });
+ } else {
+ Element sourceElement =
configuration.childElement("source").orElse(null);
+ Element targetElement =
configuration.childElement("target").orElse(null);
+
+ if (sourceElement != null && targetElement != null) {
+ String sourceValue = sourceElement.textContent().trim();
+ String targetValue = targetElement.textContent().trim();
+
+ if (sourceValue.equals(targetValue)) {
+ targetVersion = sourceValue;
+ DomUtils.removeElement(sourceElement);
+ DomUtils.removeElement(targetElement);
+ context.detail("Migrated compiler plugin
<source>/<target>: " + targetVersion);
+ }
+ }
+ }
+
+ if (targetVersion != null) {
+ cleanupCompilerPlugin(compilerPlugin, configuration,
pluginsElement);
+ }
+
+ return targetVersion;
+ }
+
+ private Element findCompilerPlugin(Element pluginsElement) {
+ return pluginsElement.childElements(PLUGIN).toList().stream()
+ .filter(plugin -> {
+ String artifactId = plugin.childTextTrimmed(ARTIFACT_ID);
+ String groupId = plugin.childTextTrimmed(GROUP_ID);
+ return MAVEN_COMPILER_PLUGIN.equals(artifactId)
+ && (groupId == null || groupId.isEmpty() ||
DEFAULT_MAVEN_PLUGIN_GROUP_ID.equals(groupId));
+ })
+ .findFirst()
+ .orElse(null);
+ }
+
+ private void cleanupCompilerPluginConfig(UpgradeContext context, Element
root, String migratedValue) {
+ cleanupPluginSectionConfig(
+ context,
+ root.childElement(BUILD).flatMap(b ->
b.childElement(PLUGINS)).orElse(null),
+ migratedValue);
+ cleanupPluginSectionConfig(
+ context,
+ root.childElement(BUILD)
+ .flatMap(b -> b.childElement(PLUGIN_MANAGEMENT))
+ .flatMap(pm -> pm.childElement(PLUGINS))
+ .orElse(null),
+ migratedValue);
+ }
+
+ private void cleanupPluginSectionConfig(UpgradeContext context, Element
pluginsElement, String migratedValue) {
+ if (pluginsElement == null) {
+ return;
+ }
+ Element compilerPlugin = findCompilerPlugin(pluginsElement);
+ if (compilerPlugin == null) {
+ return;
+ }
+ Element configuration =
compilerPlugin.childElement(CONFIGURATION).orElse(null);
+ if (configuration == null) {
+ return;
+ }
+ boolean removed = false;
+ Element releaseElement =
configuration.childElement("release").orElse(null);
+ if (releaseElement != null
+ && migratedValue.equals(releaseElement.textContent().trim())) {
+ DomUtils.removeElement(releaseElement);
+ context.detail("Removed compiler plugin <release> (matches
migrated value)");
+ removed = true;
+ }
+ Element sourceElement =
configuration.childElement("source").orElse(null);
+ if (sourceElement != null
+ && migratedValue.equals(sourceElement.textContent().trim())) {
+ DomUtils.removeElement(sourceElement);
+ context.detail("Removed compiler plugin <source> (matches migrated
value)");
+ removed = true;
+ }
+ Element targetElement =
configuration.childElement("target").orElse(null);
+ if (targetElement != null
+ && migratedValue.equals(targetElement.textContent().trim())) {
+ DomUtils.removeElement(targetElement);
+ context.detail("Removed compiler plugin <target> (matches migrated
value)");
+ removed = true;
+ }
+ if (removed) {
+ cleanupCompilerPlugin(compilerPlugin, configuration,
pluginsElement);
+ }
+ }
+
+ private void cleanupCompilerPlugin(Element compilerPlugin, Element
configuration, Element pluginsElement) {
+ removeIfEmpty(configuration);
+
+ boolean hasConfig =
compilerPlugin.childElement(CONFIGURATION).isPresent();
+ boolean hasExecutions =
compilerPlugin.childElement("executions").isPresent();
+ boolean hasDeps =
compilerPlugin.childElement("dependencies").isPresent();
+
+ if (!hasConfig && !hasExecutions && !hasDeps) {
+ Element pluginsParent = pluginsElement.parent() instanceof Element
parent ? parent : null;
+ DomUtils.removeElement(compilerPlugin);
+ removeIfEmpty(pluginsElement);
+ if (pluginsParent != null &&
PLUGIN_MANAGEMENT.equals(pluginsParent.name())) {
+ removeIfEmpty(pluginsParent);
+ }
+ }
+ }
+
+ private static void cleanupEmptyBuild(Element root) {
+ root.childElement(BUILD).ifPresent(build -> {
+ if (!build.childElements().findAny().isPresent()) {
+ DomUtils.removeElement(build);
+ }
+ });
+ }
+
+ boolean migrateSourceDirectories(UpgradeContext context, Element root) {
+ Element buildElement = root.childElement(BUILD).orElse(null);
+ if (buildElement == null) {
+ return false;
+ }
+
+ boolean hasChanges = false;
+
+ Element sourceDir =
buildElement.childElement(SOURCE_DIRECTORY).orElse(null);
+ if (sourceDir != null) {
+ String dir = sourceDir.textContent().trim();
+ DomUtils.removeElement(sourceDir);
+
+ if (!DEFAULT_SOURCE_DIR.equals(dir)) {
+ Element sourcesElement = ensureSourcesElement(root);
+ Element mainSource =
findOrCreateMainJavaSource(sourcesElement);
+ DomUtils.insertContentElement(mainSource, "directory", dir);
+ context.detail("Migrated sourceDirectory: " + dir);
+ } else {
+ context.detail("Removed default sourceDirectory");
+ }
+ hasChanges = true;
+ }
+
+ Element testSourceDir =
buildElement.childElement(TEST_SOURCE_DIRECTORY).orElse(null);
+ if (testSourceDir != null) {
+ String dir = testSourceDir.textContent().trim();
+ DomUtils.removeElement(testSourceDir);
+
+ if (!DEFAULT_TEST_SOURCE_DIR.equals(dir)) {
+ Element sourcesElement = ensureSourcesElement(root);
+ Element testSource = DomUtils.insertNewElement("source",
sourcesElement);
+ DomUtils.insertContentElement(testSource, "scope", "test");
+ DomUtils.insertContentElement(testSource, "directory", dir);
+ context.detail("Migrated testSourceDirectory: " + dir);
+ } else {
+ context.detail("Removed default testSourceDirectory");
+ }
+ hasChanges = true;
+ }
+
+ return hasChanges;
+ }
+
+ boolean migrateResources(UpgradeContext context, Element root) {
+ Element buildElement = root.childElement(BUILD).orElse(null);
+ if (buildElement == null) {
+ return false;
+ }
+
+ boolean hasChanges = false;
+
+ hasChanges |= migrateResourceSection(context, root, buildElement,
"resources", "resource", "main");
+ hasChanges |= migrateResourceSection(context, root, buildElement,
"testResources", "testResource", "test");
+
+ return hasChanges;
+ }
+
+ private boolean migrateResourceSection(
+ UpgradeContext context,
+ Element root,
+ Element buildElement,
+ String containerName,
+ String elementName,
+ String scope) {
+ Element container =
buildElement.childElement(containerName).orElse(null);
+ if (container == null) {
+ return false;
+ }
+
+ List<Element> resources =
container.childElements(elementName).toList();
+
+ for (Element resource : resources) {
+ if (isDefaultResource(resource, scope)) {
+ continue;
+ }
+
+ Element sourcesElement = ensureSourcesElement(root);
+ Element sourceElement = DomUtils.insertNewElement("source",
sourcesElement);
+
+ if ("test".equals(scope)) {
+ DomUtils.insertContentElement(sourceElement, "scope", "test");
+ }
+ DomUtils.insertContentElement(sourceElement, "lang", "resources");
+
+ String directory = resource.childTextTrimmed("directory");
+ String defaultDir = "main".equals(scope) ? DEFAULT_RESOURCE_DIR :
DEFAULT_TEST_RESOURCE_DIR;
+ if (directory != null && !directory.isEmpty() &&
!defaultDir.equals(directory)) {
+ DomUtils.insertContentElement(sourceElement, "directory",
directory);
+ }
+
+ String filtering = resource.childTextTrimmed("filtering");
+ if ("true".equals(filtering)) {
+ DomUtils.insertContentElement(sourceElement,
"stringFiltering", "true");
+ }
+
+ copyIncludesExcludes(resource, sourceElement);
+
+ String targetPath = resource.childTextTrimmed("targetPath");
+ if (targetPath != null && !targetPath.isEmpty()) {
+ DomUtils.insertContentElement(sourceElement, "targetPath",
targetPath);
+ }
+
+ context.detail("Migrated " + scope + " resource: " + (directory !=
null ? directory : defaultDir));
+ }
+
+ DomUtils.removeElement(container);
+ return true;
+ }
+
+ private boolean isDefaultResource(Element resource, String scope) {
+ String directory = resource.childTextTrimmed("directory");
+ String defaultDir = "main".equals(scope) ? DEFAULT_RESOURCE_DIR :
DEFAULT_TEST_RESOURCE_DIR;
+ boolean isDefaultDir = directory == null || directory.isEmpty() ||
defaultDir.equals(directory);
+ if (!isDefaultDir) {
+ return false;
+ }
+
+ String filtering = resource.childTextTrimmed("filtering");
+ if ("true".equals(filtering)) {
+ return false;
+ }
+
+ String targetPath = resource.childTextTrimmed("targetPath");
+ if (targetPath != null && !targetPath.isEmpty()) {
+ return false;
+ }
+
+ if (resource.childElement("includes").isPresent()) {
+ return false;
+ }
+ if (resource.childElement("excludes").isPresent()) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private static void copyIncludesExcludes(Element source, Element target) {
+ copyPatternList(source, target, "includes", "include");
+ copyPatternList(source, target, "excludes", "exclude");
+ }
+
+ private static void copyPatternList(Element source, Element target, String
containerName, String elementName) {
+ source.childElement(containerName).ifPresent(container -> {
+ Element newContainer = DomUtils.insertNewElement(containerName,
target);
+ container.childElements(elementName).forEach(element -> {
+ String value = element.textContent();
+ if (value != null && !value.trim().isEmpty()) {
+ DomUtils.insertContentElement(newContainer, elementName,
value.trim());
+ }
+ });
+ });
+ }
+
+ private Element ensureSourcesElement(Element root) {
+ Element buildElement = root.childElement(BUILD).orElse(null);
+ if (buildElement == null) {
+ buildElement = DomUtils.insertNewElement(BUILD, root);
+ }
+
+ Element sourcesElement =
buildElement.childElement("sources").orElse(null);
+ if (sourcesElement == null) {
+ sourcesElement = DomUtils.insertNewElement("sources",
buildElement);
+ }
+ return sourcesElement;
+ }
+
+ private Element findOrCreateMainJavaSource(Element sourcesElement) {
+ // Look for an existing main/java source element (e.g. one created for
targetVersion)
+ for (Element source : sourcesElement.childElements("source").toList())
{
+ String scope = source.childTextTrimmed("scope");
+ String lang = source.childTextTrimmed("lang");
+ if ((scope == null || scope.isEmpty() || "main".equals(scope))
+ && (lang == null || lang.isEmpty() ||
"java".equals(lang))) {
+ return source;
+ }
+ }
+ return DomUtils.insertNewElement("source", sourcesElement);
+ }
+
+ private static void removeIfEmpty(Element element) {
+ if (element != null && !element.childElements().findAny().isPresent())
{
+ DomUtils.removeElement(element);
+ }
+ }
+}
diff --git
a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/SourceStrategyTest.java
b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/SourceStrategyTest.java
new file mode 100644
index 0000000000..ea87657996
--- /dev/null
+++
b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/SourceStrategyTest.java
@@ -0,0 +1,860 @@
+/*
+ * 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.Paths;
+import java.util.HashMap;
+import java.util.Map;
+
+import eu.maveniverse.domtrip.Document;
+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;
+
+@DisplayName("SourceStrategy")
+class SourceStrategyTest {
+
+ private SourceStrategy strategy;
+
+ @BeforeEach
+ void setUp() {
+ strategy = new SourceStrategy();
+ }
+
+ @Nested
+ @DisplayName("Applicability")
+ class ApplicabilityTests {
+
+ @Test
+ @DisplayName("should not be applicable by default")
+ void shouldNotBeApplicableByDefault() {
+ UpgradeContext context =
TestUtils.createMockContext(TestUtils.createDefaultOptions());
+ assertFalse(strategy.isApplicable(context));
+ }
+
+ @Test
+ @DisplayName("should be applicable with --model-version 4.1.0")
+ void shouldBeApplicableWithModelVersion410() {
+ UpgradeContext context =
TestUtils.createMockContext(TestUtils.createOptionsWithModelVersion("4.1.0"));
+ assertTrue(strategy.isApplicable(context));
+ }
+
+ @Test
+ @DisplayName("should be applicable with --model-version 4.2.0")
+ void shouldBeApplicableWithModelVersion420() {
+ UpgradeContext context =
TestUtils.createMockContext(TestUtils.createOptionsWithModelVersion("4.2.0"));
+ assertTrue(strategy.isApplicable(context));
+ }
+
+ @Test
+ @DisplayName("should not be applicable with --model-version 4.0.0")
+ void shouldNotBeApplicableWithModelVersion400() {
+ UpgradeContext context =
TestUtils.createMockContext(TestUtils.createOptionsWithModelVersion("4.0.0"));
+ assertFalse(strategy.isApplicable(context));
+ }
+
+ @Test
+ @DisplayName("should be applicable with --all")
+ void shouldBeApplicableWithAll() {
+ UpgradeContext context =
TestUtils.createMockContext(TestUtils.createOptionsWithAll(true));
+ assertTrue(strategy.isApplicable(context));
+ }
+ }
+
+ @Nested
+ @DisplayName("Compiler Properties Migration")
+ class CompilerPropertiesTests {
+
+ @Test
+ @DisplayName("should migrate maven.compiler.release to targetVersion")
+ void shouldMigrateCompilerRelease() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.1.0">
+ <modelVersion>4.1.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <properties>
+ <maven.compiler.release>17</maven.compiler.release>
+ </properties>
+ </project>
+ """;
+
+ Document doc = Document.of(pomXml);
+ UpgradeContext context =
TestUtils.createMockContext(TestUtils.createOptionsWithModelVersion("4.1.0"));
+ strategy.doApply(context, new
HashMap<>(Map.of(Paths.get("pom.xml"), doc)));
+
+ Element source = doc.root().path("build", "sources",
"source").orElseThrow();
+
+ assertEquals("17", source.childTextTrimmed("targetVersion"));
+ assertFalse(doc.root().childElement("properties").isPresent());
+ }
+
+ @Test
+ @DisplayName("should migrate matching maven.compiler.source and
target")
+ void shouldMigrateMatchingSourceTarget() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.1.0">
+ <modelVersion>4.1.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <properties>
+ <maven.compiler.source>11</maven.compiler.source>
+ <maven.compiler.target>11</maven.compiler.target>
+ </properties>
+ </project>
+ """;
+
+ Document doc = Document.of(pomXml);
+ UpgradeContext context =
TestUtils.createMockContext(TestUtils.createOptionsWithModelVersion("4.1.0"));
+ strategy.doApply(context, new
HashMap<>(Map.of(Paths.get("pom.xml"), doc)));
+
+ Element source = doc.root().path("build", "sources",
"source").orElseThrow();
+
+ assertEquals("11", source.childTextTrimmed("targetVersion"));
+ assertFalse(doc.root().childElement("properties").isPresent());
+ }
+
+ @Test
+ @DisplayName("should skip when source and target differ")
+ void shouldSkipWhenSourceTargetDiffer() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.1.0">
+ <modelVersion>4.1.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <properties>
+ <maven.compiler.source>11</maven.compiler.source>
+ <maven.compiler.target>17</maven.compiler.target>
+ </properties>
+ </project>
+ """;
+
+ Document doc = Document.of(pomXml);
+ UpgradeContext context =
TestUtils.createMockContext(TestUtils.createOptionsWithModelVersion("4.1.0"));
+ UpgradeResult result = strategy.doApply(context, new
HashMap<>(Map.of(Paths.get("pom.xml"), doc)));
+
+ assertFalse(doc.root().childElement("build").isPresent());
+ assertTrue(doc.root().childElement("properties").isPresent());
+ assertEquals(0, result.modifiedCount());
+ }
+
+ @Test
+ @DisplayName("should prefer release over source/target")
+ void shouldPreferReleaseOverSourceTarget() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.1.0">
+ <modelVersion>4.1.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.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 doc = Document.of(pomXml);
+ UpgradeContext context =
TestUtils.createMockContext(TestUtils.createOptionsWithModelVersion("4.1.0"));
+ strategy.doApply(context, new
HashMap<>(Map.of(Paths.get("pom.xml"), doc)));
+
+ Element source = doc.root().path("build", "sources",
"source").orElseThrow();
+
+ assertEquals("21", source.childTextTrimmed("targetVersion"));
+ assertFalse(doc.root().childElement("properties").isPresent());
+ }
+
+ @Test
+ @DisplayName("should keep other properties when removing compiler
properties")
+ void shouldKeepOtherProperties() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.1.0">
+ <modelVersion>4.1.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <properties>
+ <maven.compiler.release>17</maven.compiler.release>
+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ </properties>
+ </project>
+ """;
+
+ Document doc = Document.of(pomXml);
+ UpgradeContext context =
TestUtils.createMockContext(TestUtils.createOptionsWithModelVersion("4.1.0"));
+ strategy.doApply(context, new
HashMap<>(Map.of(Paths.get("pom.xml"), doc)));
+
+ Element properties =
doc.root().childElement("properties").orElseThrow();
+ assertEquals("UTF-8",
properties.childTextTrimmed("project.build.sourceEncoding"));
+
assertFalse(properties.childElement("maven.compiler.release").isPresent());
+ }
+ }
+
+ @Nested
+ @DisplayName("Compiler Plugin Migration")
+ class CompilerPluginTests {
+
+ @Test
+ @DisplayName("should migrate plugin release configuration")
+ void shouldMigratePluginRelease() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.1.0">
+ <modelVersion>4.1.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <build>
+ <plugins>
+ <plugin>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <release>17</release>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ </project>
+ """;
+
+ Document doc = Document.of(pomXml);
+ UpgradeContext context =
TestUtils.createMockContext(TestUtils.createOptionsWithModelVersion("4.1.0"));
+ strategy.doApply(context, new
HashMap<>(Map.of(Paths.get("pom.xml"), doc)));
+
+ Element source = doc.root().path("build", "sources",
"source").orElseThrow();
+
+ assertEquals("17", source.childTextTrimmed("targetVersion"));
+
+ // Plugin should be removed since it has no remaining config
+ assertFalse(doc.root().path("build", "plugins").isPresent());
+ }
+
+ @Test
+ @DisplayName("should migrate plugin source/target configuration")
+ void shouldMigratePluginSourceTarget() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.1.0">
+ <modelVersion>4.1.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <build>
+ <plugins>
+ <plugin>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <source>11</source>
+ <target>11</target>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ </project>
+ """;
+
+ Document doc = Document.of(pomXml);
+ UpgradeContext context =
TestUtils.createMockContext(TestUtils.createOptionsWithModelVersion("4.1.0"));
+ strategy.doApply(context, new
HashMap<>(Map.of(Paths.get("pom.xml"), doc)));
+
+ Element source = doc.root().path("build", "sources",
"source").orElseThrow();
+
+ assertEquals("11", source.childTextTrimmed("targetVersion"));
+ }
+
+ @Test
+ @DisplayName("should keep plugin with remaining executions")
+ void shouldKeepPluginWithExecutions() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.1.0">
+ <modelVersion>4.1.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <build>
+ <plugins>
+ <plugin>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <release>17</release>
+ </configuration>
+ <executions>
+ <execution>
+ <id>custom</id>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </project>
+ """;
+
+ Document doc = Document.of(pomXml);
+ UpgradeContext context =
TestUtils.createMockContext(TestUtils.createOptionsWithModelVersion("4.1.0"));
+ strategy.doApply(context, new
HashMap<>(Map.of(Paths.get("pom.xml"), doc)));
+
+ Element plugin = doc.root().path("build", "plugins",
"plugin").orElseThrow();
+ assertFalse(plugin.childElement("configuration").isPresent());
+ assertTrue(plugin.childElement("executions").isPresent());
+ }
+
+ @Test
+ @DisplayName("should not extract from plugin when properties already
provided targetVersion")
+ void shouldNotDuplicateTargetVersion() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.1.0">
+ <modelVersion>4.1.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <properties>
+ <maven.compiler.release>21</maven.compiler.release>
+ </properties>
+ <build>
+ <plugins>
+ <plugin>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <release>21</release>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ </project>
+ """;
+
+ Document doc = Document.of(pomXml);
+ UpgradeContext context =
TestUtils.createMockContext(TestUtils.createOptionsWithModelVersion("4.1.0"));
+ strategy.doApply(context, new
HashMap<>(Map.of(Paths.get("pom.xml"), doc)));
+
+ var sources = doc.root()
+ .path("build", "sources")
+ .orElseThrow()
+ .childElements("source")
+ .toList();
+
+ assertEquals(1, sources.size());
+ assertEquals("21",
sources.get(0).childTextTrimmed("targetVersion"));
+ assertFalse(doc.root().path("build", "plugins").isPresent());
+ }
+
+ @Test
+ @DisplayName("should preserve plugin config when it differs from
migrated property value")
+ void shouldPreservePluginConfigWhenDifferent() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.1.0">
+ <modelVersion>4.1.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <properties>
+ <maven.compiler.release>17</maven.compiler.release>
+ </properties>
+ <build>
+ <plugins>
+ <plugin>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <release>21</release>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ </project>
+ """;
+
+ Document doc = Document.of(pomXml);
+ UpgradeContext context =
TestUtils.createMockContext(TestUtils.createOptionsWithModelVersion("4.1.0"));
+ strategy.doApply(context, new
HashMap<>(Map.of(Paths.get("pom.xml"), doc)));
+
+ Element plugin = doc.root().path("build", "plugins",
"plugin").orElseThrow();
+ Element configuration =
plugin.childElement("configuration").orElseThrow();
+ assertEquals("21", configuration.childTextTrimmed("release"));
+
+ Element source = doc.root().path("build", "sources",
"source").orElseThrow();
+ assertEquals("17", source.childTextTrimmed("targetVersion"));
+ }
+
+ @Test
+ @DisplayName("should migrate compiler plugin from pluginManagement")
+ void shouldMigrateFromPluginManagement() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.1.0">
+ <modelVersion>4.1.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <build>
+ <pluginManagement>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+
<artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <release>17</release>
+ </configuration>
+ </plugin>
+ </plugins>
+ </pluginManagement>
+ </build>
+ </project>
+ """;
+
+ Document doc = Document.of(pomXml);
+ UpgradeContext context =
TestUtils.createMockContext(TestUtils.createOptionsWithModelVersion("4.1.0"));
+ strategy.doApply(context, new
HashMap<>(Map.of(Paths.get("pom.xml"), doc)));
+
+ Element source = doc.root().path("build", "sources",
"source").orElseThrow();
+ assertEquals("17", source.childTextTrimmed("targetVersion"));
+ assertFalse(doc.root().path("build",
"pluginManagement").isPresent());
+ }
+ }
+
+ @Nested
+ @DisplayName("Source Directory Migration")
+ class SourceDirectoryTests {
+
+ @Test
+ @DisplayName("should migrate non-default sourceDirectory")
+ void shouldMigrateNonDefaultSourceDirectory() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.1.0">
+ <modelVersion>4.1.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <build>
+ <sourceDirectory>src/main/java-custom</sourceDirectory>
+ </build>
+ </project>
+ """;
+
+ Document doc = Document.of(pomXml);
+ UpgradeContext context =
TestUtils.createMockContext(TestUtils.createOptionsWithModelVersion("4.1.0"));
+ strategy.doApply(context, new
HashMap<>(Map.of(Paths.get("pom.xml"), doc)));
+
+ Element source = doc.root().path("build", "sources",
"source").orElseThrow();
+
+ assertEquals("src/main/java-custom",
source.childTextTrimmed("directory"));
+ assertFalse(doc.root().path("build",
"sourceDirectory").isPresent());
+ }
+
+ @Test
+ @DisplayName("should migrate non-default testSourceDirectory")
+ void shouldMigrateNonDefaultTestSourceDirectory() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.1.0">
+ <modelVersion>4.1.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <build>
+
<testSourceDirectory>src/test/java-custom</testSourceDirectory>
+ </build>
+ </project>
+ """;
+
+ Document doc = Document.of(pomXml);
+ UpgradeContext context =
TestUtils.createMockContext(TestUtils.createOptionsWithModelVersion("4.1.0"));
+ strategy.doApply(context, new
HashMap<>(Map.of(Paths.get("pom.xml"), doc)));
+
+ Element source = doc.root().path("build", "sources",
"source").orElseThrow();
+
+ assertEquals("test", source.childTextTrimmed("scope"));
+ assertEquals("src/test/java-custom",
source.childTextTrimmed("directory"));
+ }
+
+ @Test
+ @DisplayName("should remove default sourceDirectory without creating
source element")
+ void shouldRemoveDefaultSourceDirectory() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.1.0">
+ <modelVersion>4.1.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <build>
+ <sourceDirectory>src/main/java</sourceDirectory>
+ </build>
+ </project>
+ """;
+
+ Document doc = Document.of(pomXml);
+ UpgradeContext context =
TestUtils.createMockContext(TestUtils.createOptionsWithModelVersion("4.1.0"));
+ UpgradeResult result = strategy.doApply(context, new
HashMap<>(Map.of(Paths.get("pom.xml"), doc)));
+
+ assertFalse(doc.root().childElement("build").isPresent());
+ assertEquals(1, result.modifiedCount());
+ }
+ }
+
+ @Nested
+ @DisplayName("Resource Migration")
+ class ResourceTests {
+
+ @Test
+ @DisplayName("should migrate resource with filtering")
+ void shouldMigrateResourceWithFiltering() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.1.0">
+ <modelVersion>4.1.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <build>
+ <resources>
+ <resource>
+ <directory>src/main/resources</directory>
+ <filtering>true</filtering>
+ </resource>
+ </resources>
+ </build>
+ </project>
+ """;
+
+ Document doc = Document.of(pomXml);
+ UpgradeContext context =
TestUtils.createMockContext(TestUtils.createOptionsWithModelVersion("4.1.0"));
+ strategy.doApply(context, new
HashMap<>(Map.of(Paths.get("pom.xml"), doc)));
+
+ Element source = doc.root().path("build", "sources",
"source").orElseThrow();
+
+ assertEquals("resources", source.childTextTrimmed("lang"));
+ assertEquals("true", source.childTextTrimmed("stringFiltering"));
+ assertFalse(doc.root().path("build", "resources").isPresent());
+ }
+
+ @Test
+ @DisplayName("should migrate resource with includes and excludes")
+ void shouldMigrateResourceWithIncludesExcludes() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.1.0">
+ <modelVersion>4.1.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <build>
+ <resources>
+ <resource>
+ <directory>src/main/resources</directory>
+ <includes>
+ <include>**/*.xml</include>
+ </includes>
+ <excludes>
+ <exclude>**/*.bak</exclude>
+ </excludes>
+ </resource>
+ </resources>
+ </build>
+ </project>
+ """;
+
+ Document doc = Document.of(pomXml);
+ UpgradeContext context =
TestUtils.createMockContext(TestUtils.createOptionsWithModelVersion("4.1.0"));
+ strategy.doApply(context, new
HashMap<>(Map.of(Paths.get("pom.xml"), doc)));
+
+ Element source = doc.root().path("build", "sources",
"source").orElseThrow();
+
+ assertEquals("resources", source.childTextTrimmed("lang"));
+ assertEquals("**/*.xml",
source.path("includes").orElseThrow().childTextTrimmed("include"));
+ assertEquals("**/*.bak",
source.path("excludes").orElseThrow().childTextTrimmed("exclude"));
+ }
+
+ @Test
+ @DisplayName("should migrate resource with targetPath")
+ void shouldMigrateResourceWithTargetPath() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.1.0">
+ <modelVersion>4.1.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <build>
+ <resources>
+ <resource>
+ <directory>src/main/resources</directory>
+ <targetPath>META-INF</targetPath>
+ </resource>
+ </resources>
+ </build>
+ </project>
+ """;
+
+ Document doc = Document.of(pomXml);
+ UpgradeContext context =
TestUtils.createMockContext(TestUtils.createOptionsWithModelVersion("4.1.0"));
+ strategy.doApply(context, new
HashMap<>(Map.of(Paths.get("pom.xml"), doc)));
+
+ Element source = doc.root().path("build", "sources",
"source").orElseThrow();
+
+ assertEquals("resources", source.childTextTrimmed("lang"));
+ assertEquals("META-INF", source.childTextTrimmed("targetPath"));
+ }
+
+ @Test
+ @DisplayName("should migrate test resource")
+ void shouldMigrateTestResource() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.1.0">
+ <modelVersion>4.1.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <build>
+ <testResources>
+ <testResource>
+ <directory>src/test/resources</directory>
+ <filtering>true</filtering>
+ </testResource>
+ </testResources>
+ </build>
+ </project>
+ """;
+
+ Document doc = Document.of(pomXml);
+ UpgradeContext context =
TestUtils.createMockContext(TestUtils.createOptionsWithModelVersion("4.1.0"));
+ strategy.doApply(context, new
HashMap<>(Map.of(Paths.get("pom.xml"), doc)));
+
+ Element source = doc.root().path("build", "sources",
"source").orElseThrow();
+
+ assertEquals("test", source.childTextTrimmed("scope"));
+ assertEquals("resources", source.childTextTrimmed("lang"));
+ assertEquals("true", source.childTextTrimmed("stringFiltering"));
+ }
+
+ @Test
+ @DisplayName("should remove default resource without creating source
element")
+ void shouldRemoveDefaultResource() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.1.0">
+ <modelVersion>4.1.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <build>
+ <resources>
+ <resource>
+ <directory>src/main/resources</directory>
+ </resource>
+ </resources>
+ </build>
+ </project>
+ """;
+
+ Document doc = Document.of(pomXml);
+ UpgradeContext context =
TestUtils.createMockContext(TestUtils.createOptionsWithModelVersion("4.1.0"));
+ UpgradeResult result = strategy.doApply(context, new
HashMap<>(Map.of(Paths.get("pom.xml"), doc)));
+
+ assertFalse(doc.root().childElement("build").isPresent());
+ assertEquals(1, result.modifiedCount());
+ }
+ }
+
+ @Nested
+ @DisplayName("Model Version Filtering")
+ class ModelVersionFilteringTests {
+
+ @Test
+ @DisplayName("should skip POM at model version 4.0.0")
+ void shouldSkipPomAt400() {
+ 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>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <properties>
+ <maven.compiler.release>17</maven.compiler.release>
+ </properties>
+ </project>
+ """;
+
+ Document doc = Document.of(pomXml);
+ UpgradeContext context =
TestUtils.createMockContext(TestUtils.createOptionsWithModelVersion("4.1.0"));
+ UpgradeResult result = strategy.doApply(context, new
HashMap<>(Map.of(Paths.get("pom.xml"), doc)));
+
+ assertTrue(doc.root().childElement("properties").isPresent());
+ assertEquals(0, result.modifiedCount());
+ }
+
+ @Test
+ @DisplayName("should process POM at model version 4.1.0")
+ void shouldProcessPomAt410() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.1.0">
+ <modelVersion>4.1.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <properties>
+ <maven.compiler.release>17</maven.compiler.release>
+ </properties>
+ </project>
+ """;
+
+ Document doc = Document.of(pomXml);
+ UpgradeContext context =
TestUtils.createMockContext(TestUtils.createOptionsWithModelVersion("4.1.0"));
+ UpgradeResult result = strategy.doApply(context, new
HashMap<>(Map.of(Paths.get("pom.xml"), doc)));
+
+ assertEquals(1, result.modifiedCount());
+ }
+ }
+
+ @Nested
+ @DisplayName("Combined Migration")
+ class CombinedTests {
+
+ @Test
+ @DisplayName("should merge targetVersion and directory into single
source element")
+ void shouldMergeTargetVersionAndDirectory() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.1.0">
+ <modelVersion>4.1.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <properties>
+ <maven.compiler.release>17</maven.compiler.release>
+ </properties>
+ <build>
+ <sourceDirectory>src/main/java-custom</sourceDirectory>
+ </build>
+ </project>
+ """;
+
+ Document doc = Document.of(pomXml);
+ UpgradeContext context =
TestUtils.createMockContext(TestUtils.createOptionsWithModelVersion("4.1.0"));
+ strategy.doApply(context, new
HashMap<>(Map.of(Paths.get("pom.xml"), doc)));
+
+ var sources = doc.root()
+ .path("build", "sources")
+ .orElseThrow()
+ .childElements("source")
+ .toList();
+
+ assertEquals(1, sources.size());
+ assertEquals("17",
sources.get(0).childTextTrimmed("targetVersion"));
+ assertEquals("src/main/java-custom",
sources.get(0).childTextTrimmed("directory"));
+ }
+
+ @Test
+ @DisplayName("should reuse existing source element when adding
targetVersion")
+ void shouldReuseExistingSourceElement() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.1.0">
+ <modelVersion>4.1.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <properties>
+ <maven.compiler.release>17</maven.compiler.release>
+ </properties>
+ <build>
+ <sources>
+ <source>
+ <directory>src/main/java-custom</directory>
+ </source>
+ </sources>
+ </build>
+ </project>
+ """;
+
+ Document doc = Document.of(pomXml);
+ UpgradeContext context =
TestUtils.createMockContext(TestUtils.createOptionsWithModelVersion("4.1.0"));
+ strategy.doApply(context, new
HashMap<>(Map.of(Paths.get("pom.xml"), doc)));
+
+ var sources = doc.root()
+ .path("build", "sources")
+ .orElseThrow()
+ .childElements("source")
+ .toList();
+
+ assertEquals(1, sources.size());
+ assertEquals("17",
sources.get(0).childTextTrimmed("targetVersion"));
+ assertEquals("src/main/java-custom",
sources.get(0).childTextTrimmed("directory"));
+ }
+
+ @Test
+ @DisplayName("should create multiple source elements for different
resource configs")
+ void shouldCreateMultipleResourceSources() {
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.1.0">
+ <modelVersion>4.1.0</modelVersion>
+ <groupId>com.example</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0</version>
+ <build>
+ <resources>
+ <resource>
+ <directory>src/main/resources</directory>
+ <filtering>true</filtering>
+ </resource>
+ <resource>
+ <directory>src/main/resources-extra</directory>
+ </resource>
+ </resources>
+ </build>
+ </project>
+ """;
+
+ Document doc = Document.of(pomXml);
+ UpgradeContext context =
TestUtils.createMockContext(TestUtils.createOptionsWithModelVersion("4.1.0"));
+ strategy.doApply(context, new
HashMap<>(Map.of(Paths.get("pom.xml"), doc)));
+
+ var sources = doc.root()
+ .path("build", "sources")
+ .orElseThrow()
+ .childElements("source")
+ .toList();
+
+ assertEquals(2, sources.size());
+ }
+ }
+}