This is an automated email from the ASF dual-hosted git repository.

gnodet pushed a commit to branch feature/mvnup-source-strategy
in repository https://gitbox.apache.org/repos/asf/maven.git

commit 63979cdaa02710ebdb59793f51755588a46ffc75
Author: Guillaume Nodet <[email protected]>
AuthorDate: Tue Jun 23 22:40:42 2026 +0200

    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]>
---
 .../cling/invoker/mvnup/goals/SourceStrategy.java  | 493 ++++++++++++
 .../invoker/mvnup/goals/SourceStrategyTest.java    | 855 +++++++++++++++++++++
 2 files changed, 1348 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..2fc07311ea
--- /dev/null
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/SourceStrategy.java
@@ -0,0 +1,493 @@
+/*
+ * 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.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;
+
+/**
+ * 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);
+        }
+
+        if (targetVersion != null) {
+            Element sourcesElement = ensureSourcesElement(root);
+            Element sourceElement = DomUtils.insertNewElement("source", 
sourcesElement);
+            DomUtils.insertContentElement(sourceElement, "targetVersion", 
targetVersion);
+            context.detail("Set targetVersion: " + targetVersion);
+            hasChanges = true;
+        }
+
+        hasChanges |= migrateSourceDirectories(context, root);
+        hasChanges |= migrateResources(context, 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);
+                    return MAVEN_COMPILER_PLUGIN.equals(artifactId);
+                })
+                .findFirst()
+                .orElse(null);
+    }
+
+    private void cleanupCompilerPlugin(Element compilerPlugin, Element 
configuration, Element pluginsElement) {
+        if (!configuration.childElements().findAny().isPresent()) {
+            DomUtils.removeElement(configuration);
+        }
+
+        boolean hasConfig = 
compilerPlugin.childElement(CONFIGURATION).isPresent();
+        boolean hasExecutions = 
compilerPlugin.childElement("executions").isPresent();
+        boolean hasDeps = 
compilerPlugin.childElement("dependencies").isPresent();
+
+        if (!hasConfig && !hasExecutions && !hasDeps) {
+            DomUtils.removeElement(compilerPlugin);
+            removeIfEmpty(pluginsElement);
+        }
+    }
+
+    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 void copyIncludesExcludes(Element source, Element target) {
+        source.childElement("includes").ifPresent(includes -> {
+            Element newIncludes = DomUtils.insertNewElement("includes", 
target);
+            includes.childElements("include").forEach(include -> {
+                String value = include.textContent();
+                if (value != null && !value.trim().isEmpty()) {
+                    DomUtils.insertContentElement(newIncludes, "include", 
value.trim());
+                }
+            });
+        });
+
+        source.childElement("excludes").ifPresent(excludes -> {
+            Element newExcludes = DomUtils.insertNewElement("excludes", 
target);
+            excludes.childElements("exclude").forEach(exclude -> {
+                String value = exclude.textContent();
+                if (value != null && !value.trim().isEmpty()) {
+                    DomUtils.insertContentElement(newExcludes, "exclude", 
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.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..69dc742971
--- /dev/null
+++ 
b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/SourceStrategyTest.java
@@ -0,0 +1,855 @@
+/*
+ * 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.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+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()
+                    .childElement("build")
+                    .orElseThrow()
+                    .childElement("sources")
+                    .orElseThrow()
+                    .childElement("source")
+                    .orElseThrow();
+
+            assertEquals("17", source.childTextTrimmed("targetVersion"));
+            assertNull(DomUtils.findChildElement(doc.root(), "properties"));
+        }
+
+        @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()
+                    .childElement("build")
+                    .orElseThrow()
+                    .childElement("sources")
+                    .orElseThrow()
+                    .childElement("source")
+                    .orElseThrow();
+
+            assertEquals("11", source.childTextTrimmed("targetVersion"));
+            assertNull(DomUtils.findChildElement(doc.root(), "properties"));
+        }
+
+        @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());
+            assertNotNull(DomUtils.findChildElement(doc.root(), "properties"));
+            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()
+                    .childElement("build")
+                    .orElseThrow()
+                    .childElement("sources")
+                    .orElseThrow()
+                    .childElement("source")
+                    .orElseThrow();
+
+            assertEquals("21", source.childTextTrimmed("targetVersion"));
+            assertNull(DomUtils.findChildElement(doc.root(), "properties"));
+        }
+
+        @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 = DomUtils.findChildElement(doc.root(), 
"properties");
+            assertNotNull(properties);
+            assertEquals("UTF-8", 
properties.childTextTrimmed("project.build.sourceEncoding"));
+            assertNull(DomUtils.findChildElement(properties, 
"maven.compiler.release"));
+        }
+    }
+
+    @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()
+                    .childElement("build")
+                    .orElseThrow()
+                    .childElement("sources")
+                    .orElseThrow()
+                    .childElement("source")
+                    .orElseThrow();
+
+            assertEquals("17", source.childTextTrimmed("targetVersion"));
+
+            // Plugin should be removed since it has no remaining config
+            assertFalse(doc.root()
+                    .childElement("build")
+                    .orElseThrow()
+                    .childElement("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()
+                    .childElement("build")
+                    .orElseThrow()
+                    .childElement("sources")
+                    .orElseThrow()
+                    .childElement("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)));
+
+            // Plugin should still exist (has executions)
+            Element plugin = doc.root()
+                    .childElement("build")
+                    .orElseThrow()
+                    .childElement("plugins")
+                    .orElseThrow()
+                    .childElement("plugin")
+                    .orElseThrow();
+
+            assertNotNull(plugin);
+            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)));
+
+            // Should have exactly one <source> element with targetVersion 
from properties
+            var sources = doc.root()
+                    .childElement("build")
+                    .orElseThrow()
+                    .childElement("sources")
+                    .orElseThrow()
+                    .childElements("source")
+                    .toList();
+
+            assertEquals(1, sources.size());
+            assertEquals("21", 
sources.get(0).childTextTrimmed("targetVersion"));
+        }
+    }
+
+    @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()
+                    .childElement("build")
+                    .orElseThrow()
+                    .childElement("sources")
+                    .orElseThrow()
+                    .childElement("source")
+                    .orElseThrow();
+
+            assertEquals("src/main/java-custom", 
source.childTextTrimmed("directory"));
+            assertNull(
+                    
DomUtils.findChildElement(doc.root().childElement("build").orElseThrow(), 
"sourceDirectory"));
+        }
+
+        @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()
+                    .childElement("build")
+                    .orElseThrow()
+                    .childElement("sources")
+                    .orElseThrow()
+                    .childElement("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)));
+
+            assertNull(
+                    
DomUtils.findChildElement(doc.root().childElement("build").orElseThrow(), 
"sourceDirectory"));
+            assertFalse(doc.root()
+                    .childElement("build")
+                    .orElseThrow()
+                    .childElement("sources")
+                    .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()
+                    .childElement("build")
+                    .orElseThrow()
+                    .childElement("sources")
+                    .orElseThrow()
+                    .childElement("source")
+                    .orElseThrow();
+
+            assertEquals("resources", source.childTextTrimmed("lang"));
+            assertEquals("true", source.childTextTrimmed("stringFiltering"));
+            assertNull(
+                    
DomUtils.findChildElement(doc.root().childElement("build").orElseThrow(), 
"resources"));
+        }
+
+        @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()
+                    .childElement("build")
+                    .orElseThrow()
+                    .childElement("sources")
+                    .orElseThrow()
+                    .childElement("source")
+                    .orElseThrow();
+
+            assertEquals("resources", source.childTextTrimmed("lang"));
+
+            Element includes = source.childElement("includes").orElseThrow();
+            assertEquals("**/*.xml", includes.childTextTrimmed("include"));
+
+            Element excludes = source.childElement("excludes").orElseThrow();
+            assertEquals("**/*.bak", excludes.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()
+                    .childElement("build")
+                    .orElseThrow()
+                    .childElement("sources")
+                    .orElseThrow()
+                    .childElement("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()
+                    .childElement("build")
+                    .orElseThrow()
+                    .childElement("sources")
+                    .orElseThrow()
+                    .childElement("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)));
+
+            assertNull(
+                    
DomUtils.findChildElement(doc.root().childElement("build").orElseThrow(), 
"resources"));
+            assertFalse(doc.root()
+                    .childElement("build")
+                    .orElseThrow()
+                    .childElement("sources")
+                    .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)));
+
+            assertNotNull(DomUtils.findChildElement(doc.root(), "properties"));
+            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()
+                    .childElement("build")
+                    .orElseThrow()
+                    .childElement("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()
+                    .childElement("build")
+                    .orElseThrow()
+                    .childElement("sources")
+                    .orElseThrow()
+                    .childElements("source")
+                    .toList();
+
+            assertEquals(2, sources.size());
+        }
+    }
+}

Reply via email to