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

desruisseaux pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven.git


The following commit(s) were added to refs/heads/master by this push:
     new 74ef127617 Add module-aware resource handling for modular sources 
(#11505)
74ef127617 is described below

commit 74ef127617fd8264110ac25346a4c3d200645f8d
Author: Gerd Aschemann <[email protected]>
AuthorDate: Mon Dec 29 12:12:43 2025 +0100

    Add module-aware resource handling for modular sources (#11505)
    
    Maven 4.x introduces a unified <sources> element that supports modular
    project layouts (src/<module>/<scope>/<lang>). However, resource handling
    did not follow the modular layout - resources were always loaded from
    the legacy <resources> element which defaults to src/main/resources.
    
    This change implements automatic module-aware resource injection:
    
    - Detect modular projects (projects with at least one module in sources)
    - For modular projects without resource configuration in <sources>,
      automatically inject resource roots following the modular layout:
      src/<module>/main/resources and src/<module>/test/resources
    - Resources configured via <sources> take priority over legacy <resources>
    - Issue warnings (as ModelProblem) when explicit legacy resources are 
ignored
    
    Example: A project with modular sources for org.foo.moduleA will now
    automatically pick up resources from:
    - src/org.foo.moduleA/main/resources
    - src/org.foo.moduleA/test/resources
    
    This eliminates the need for explicit maven-resources-plugin executions
    when using modular project layouts.
---
 .../maven/project/DefaultProjectBuilder.java       |  48 ++++-
 .../maven/project/ResourceHandlingContext.java     | 213 +++++++++++++++++++++
 .../apache/maven/project/ProjectBuilderTest.java   |  99 ++++++++++
 .../pom.xml                                        |  39 ++++
 .../project-builder/modular-sources/pom.xml        |  40 ++++
 5 files changed, 432 insertions(+), 7 deletions(-)

diff --git 
a/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java
 
b/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java
index a1a331ab69..99289fc7af 100644
--- 
a/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java
+++ 
b/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java
@@ -62,7 +62,6 @@
 import org.apache.maven.api.model.Plugin;
 import org.apache.maven.api.model.Profile;
 import org.apache.maven.api.model.ReportPlugin;
-import org.apache.maven.api.model.Resource;
 import org.apache.maven.api.services.ArtifactResolver;
 import org.apache.maven.api.services.ArtifactResolverException;
 import org.apache.maven.api.services.ArtifactResolverRequest;
@@ -669,6 +668,8 @@ private void initProject(MavenProject project, 
ModelBuilderResult result) {
                 boolean hasScript = false;
                 boolean hasMain = false;
                 boolean hasTest = false;
+                boolean hasMainResources = false;
+                boolean hasTestResources = false;
                 for (var source : sources) {
                     var src = DefaultSourceRoot.fromModel(session, baseDir, 
outputDirectory, source);
                     project.addSourceRoot(src);
@@ -680,6 +681,13 @@ private void initProject(MavenProject project, 
ModelBuilderResult result) {
                         } else {
                             hasTest |= ProjectScope.TEST.equals(scope);
                         }
+                    } else if (Language.RESOURCES.equals(language)) {
+                        ProjectScope scope = src.scope();
+                        if (ProjectScope.MAIN.equals(scope)) {
+                            hasMainResources = true;
+                        } else if (ProjectScope.TEST.equals(scope)) {
+                            hasTestResources = true;
+                        }
                     } else {
                         hasScript |= Language.SCRIPT.equals(language);
                     }
@@ -700,12 +708,22 @@ private void initProject(MavenProject project, 
ModelBuilderResult result) {
                 if (!hasTest) {
                     
project.addTestCompileSourceRoot(build.getTestSourceDirectory());
                 }
-                for (Resource resource : 
project.getBuild().getDelegate().getResources()) {
-                    project.addSourceRoot(new DefaultSourceRoot(baseDir, 
ProjectScope.MAIN, resource));
-                }
-                for (Resource resource : 
project.getBuild().getDelegate().getTestResources()) {
-                    project.addSourceRoot(new DefaultSourceRoot(baseDir, 
ProjectScope.TEST, resource));
-                }
+                // Extract modules from sources to detect modular projects
+                Set<String> modules = extractModules(sources);
+                boolean isModularProject = !modules.isEmpty();
+
+                logger.trace(
+                        "Module detection for project {}: found {} module(s) 
{} - modular project: {}.",
+                        project.getId(),
+                        modules.size(),
+                        modules,
+                        isModularProject);
+
+                // Handle main and test resources
+                ResourceHandlingContext resourceContext =
+                        new ResourceHandlingContext(project, baseDir, modules, 
isModularProject, result);
+                resourceContext.handleResourceConfiguration(ProjectScope.MAIN, 
hasMainResources);
+                resourceContext.handleResourceConfiguration(ProjectScope.TEST, 
hasTestResources);
             }
 
             project.setActiveProfiles(
@@ -1099,6 +1117,22 @@ public Set<Entry<K, V>> entrySet() {
         }
     }
 
+    /**
+     * Extracts unique module names from the given list of source elements.
+     * A project is considered modular if it has at least one module name.
+     *
+     * @param sources list of source elements from the build
+     * @return set of non-blank module names
+     */
+    private static Set<String> 
extractModules(List<org.apache.maven.api.model.Source> sources) {
+        return sources.stream()
+                .map(org.apache.maven.api.model.Source::getModule)
+                .filter(Objects::nonNull)
+                .map(String::trim)
+                .filter(s -> !s.isBlank())
+                .collect(Collectors.toSet());
+    }
+
     private Model injectLifecycleBindings(
             Model model,
             ModelBuilderRequest request,
diff --git 
a/impl/maven-core/src/main/java/org/apache/maven/project/ResourceHandlingContext.java
 
b/impl/maven-core/src/main/java/org/apache/maven/project/ResourceHandlingContext.java
new file mode 100644
index 0000000000..48fc9e7e03
--- /dev/null
+++ 
b/impl/maven-core/src/main/java/org/apache/maven/project/ResourceHandlingContext.java
@@ -0,0 +1,213 @@
+/*
+ * 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.project;
+
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.maven.api.Language;
+import org.apache.maven.api.ProjectScope;
+import org.apache.maven.api.model.Resource;
+import org.apache.maven.api.services.BuilderProblem.Severity;
+import org.apache.maven.api.services.ModelBuilderResult;
+import org.apache.maven.api.services.ModelProblem.Version;
+import org.apache.maven.impl.DefaultSourceRoot;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Handles resource configuration for Maven projects.
+ * Groups parameters shared between main and test resource handling.
+ */
+class ResourceHandlingContext {
+
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(ResourceHandlingContext.class);
+
+    private final MavenProject project;
+    private final Path baseDir;
+    private final Set<String> modules;
+    private final boolean modularProject;
+    private final ModelBuilderResult result;
+
+    ResourceHandlingContext(
+            MavenProject project,
+            Path baseDir,
+            Set<String> modules,
+            boolean modularProject,
+            ModelBuilderResult result) {
+        this.project = project;
+        this.baseDir = baseDir;
+        this.modules = modules;
+        this.modularProject = modularProject;
+        this.result = result;
+    }
+
+    /**
+     * Handles resource configuration for a given scope (main or test).
+     * This method applies the resource priority rules:
+     * <ol>
+     *   <li>Modular project: use resources from {@code <sources>} if present, 
otherwise inject defaults</li>
+     *   <li>Classic project: use resources from {@code <sources>} if present, 
otherwise use legacy resources</li>
+     * </ol>
+     *
+     * @param scope the project scope (MAIN or TEST)
+     * @param hasResourcesInSources whether resources are configured via 
{@code <sources>}
+     */
+    void handleResourceConfiguration(ProjectScope scope, boolean 
hasResourcesInSources) {
+        List<Resource> resources = scope == ProjectScope.MAIN
+                ? project.getBuild().getDelegate().getResources()
+                : project.getBuild().getDelegate().getTestResources();
+
+        String scopeId = scope.id();
+        String scopeName = scope == ProjectScope.MAIN ? "Main" : "Test";
+        String legacyElement = scope == ProjectScope.MAIN ? "<resources>" : 
"<testResources>";
+        String sourcesConfig = scope == ProjectScope.MAIN
+                ? "<source><lang>resources</lang></source>"
+                : "<source><lang>resources</lang><scope>test</scope></source>";
+
+        if (modularProject) {
+            if (hasResourcesInSources) {
+                // Modular project with resources configured via <sources> - 
already added above
+                if (hasExplicitLegacyResources(resources, scopeId)) {
+                    LOGGER.warn(
+                            "Legacy {} element is ignored because {} resources 
are configured via {} in <sources>.",
+                            legacyElement,
+                            scopeId,
+                            sourcesConfig);
+                } else {
+                    LOGGER.debug(
+                            "{} resources configured via <sources> element, 
ignoring legacy {} element.",
+                            scopeName,
+                            legacyElement);
+                }
+            } else {
+                // Modular project without resources in <sources> - inject 
module-aware defaults
+                if (hasExplicitLegacyResources(resources, scopeId)) {
+                    String message = "Legacy " + legacyElement
+                            + " element is ignored because modular sources are 
configured. "
+                            + "Use " + sourcesConfig + " in <sources> for 
custom resource paths.";
+                    LOGGER.warn(message);
+                    result.getProblemCollector()
+                            .reportProblem(new 
org.apache.maven.impl.model.DefaultModelProblem(
+                                    message,
+                                    Severity.WARNING,
+                                    Version.V41,
+                                    project.getModel().getDelegate(),
+                                    -1,
+                                    -1,
+                                    null));
+                }
+                for (String module : modules) {
+                    project.addSourceRoot(createModularResourceRoot(module, 
scope));
+                }
+                if (!modules.isEmpty()) {
+                    LOGGER.debug(
+                            "Injected {} module-aware {} resource root(s) for 
modules: {}.",
+                            modules.size(),
+                            scopeId,
+                            modules);
+                }
+            }
+        } else {
+            // Classic (non-modular) project
+            if (hasResourcesInSources) {
+                // Resources configured via <sources> - already added above
+                if (hasExplicitLegacyResources(resources, scopeId)) {
+                    LOGGER.warn(
+                            "Legacy {} element is ignored because {} resources 
are configured via {} in <sources>.",
+                            legacyElement,
+                            scopeId,
+                            sourcesConfig);
+                } else {
+                    LOGGER.debug(
+                            "{} resources configured via <sources> element, 
ignoring legacy {} element.",
+                            scopeName,
+                            legacyElement);
+                }
+            } else {
+                // Use legacy resources element
+                LOGGER.debug(
+                        "Using explicit or default {} resources ({} resources 
configured).", scopeId, resources.size());
+                for (Resource resource : resources) {
+                    project.addSourceRoot(new DefaultSourceRoot(baseDir, 
scope, resource));
+                }
+            }
+        }
+    }
+
+    /**
+     * Creates a DefaultSourceRoot for module-aware resource directories.
+     * Generates paths following the pattern: {@code 
src/<module>/<scope>/resources}
+     *
+     * @param module module name
+     * @param scope project scope (main or test)
+     * @return configured DefaultSourceRoot for the module's resources
+     */
+    private DefaultSourceRoot createModularResourceRoot(String module, 
ProjectScope scope) {
+        Path resourceDir =
+                
baseDir.resolve("src").resolve(module).resolve(scope.id()).resolve("resources");
+
+        return new DefaultSourceRoot(
+                scope,
+                Language.RESOURCES,
+                module,
+                null, // targetVersion
+                resourceDir,
+                null, // includes
+                null, // excludes
+                false, // stringFiltering
+                Path.of(module), // targetPath - resources go to 
target/classes/<module>
+                true // enabled
+                );
+    }
+
+    /**
+     * Checks if the given resource list contains explicit legacy resources 
that differ
+     * from Super POM defaults. Super POM defaults are: src/{scope}/resources 
and src/{scope}/resources-filtered
+     *
+     * @param resources list of resources to check
+     * @param scope scope (main or test)
+     * @return true if explicit legacy resources are present that would be 
ignored
+     */
+    private boolean hasExplicitLegacyResources(List<Resource> resources, 
String scope) {
+        if (resources.isEmpty()) {
+            return false; // No resources means no explicit legacy resources 
to warn about
+        }
+
+        // Super POM default paths
+        String defaultPath =
+                
baseDir.resolve("src").resolve(scope).resolve("resources").toString();
+        String defaultFilteredPath = baseDir.resolve("src")
+                .resolve(scope)
+                .resolve("resources-filtered")
+                .toString();
+
+        // Check if any resource differs from Super POM defaults
+        for (Resource resource : resources) {
+            String resourceDir = resource.getDirectory();
+            if (resourceDir != null && !resourceDir.equals(defaultPath) && 
!resourceDir.equals(defaultFilteredPath)) {
+                // Found an explicit legacy resource
+                return true;
+            }
+        }
+
+        return false;
+    }
+}
diff --git 
a/impl/maven-core/src/test/java/org/apache/maven/project/ProjectBuilderTest.java
 
b/impl/maven-core/src/test/java/org/apache/maven/project/ProjectBuilderTest.java
index 69b1aef227..a8825bc524 100644
--- 
a/impl/maven-core/src/test/java/org/apache/maven/project/ProjectBuilderTest.java
+++ 
b/impl/maven-core/src/test/java/org/apache/maven/project/ProjectBuilderTest.java
@@ -26,9 +26,14 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Properties;
+import java.util.Set;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
 
 import org.apache.maven.AbstractCoreMavenComponentTestCase;
+import org.apache.maven.api.Language;
+import org.apache.maven.api.ProjectScope;
+import org.apache.maven.api.SourceRoot;
 import org.apache.maven.execution.MavenSession;
 import org.apache.maven.model.Dependency;
 import org.apache.maven.model.InputLocation;
@@ -371,4 +376,98 @@ void testLocationTrackingResolution() throws Exception {
         assertEquals(
                 "org.apache.maven.its:parent:0.1", 
pluginLocation.getSource().getModelId());
     }
+    /**
+     * Tests that a project with multiple modules defined in sources is 
detected as modular,
+     * and module-aware resource roots are injected for each module.
+     */
+    @Test
+    void testModularSourcesInjectResourceRoots() throws Exception {
+        File pom = getProject("modular-sources");
+
+        MavenSession session = createMavenSession(pom);
+        MavenProject project = session.getCurrentProject();
+
+        // Get all resource source roots for main scope
+        List<SourceRoot> mainResourceRoots = 
project.getEnabledSourceRoots(ProjectScope.MAIN, Language.RESOURCES)
+                .collect(Collectors.toList());
+
+        // Should have resource roots for both modules
+        Set<String> modules = mainResourceRoots.stream()
+                .map(SourceRoot::module)
+                .filter(opt -> opt.isPresent())
+                .map(opt -> opt.get())
+                .collect(Collectors.toSet());
+
+        assertEquals(2, modules.size(), "Should have resource roots for 2 
modules");
+        assertTrue(modules.contains("org.foo.moduleA"), "Should have resource 
root for moduleA");
+        assertTrue(modules.contains("org.foo.moduleB"), "Should have resource 
root for moduleB");
+
+        // Get all resource source roots for test scope
+        List<SourceRoot> testResourceRoots = 
project.getEnabledSourceRoots(ProjectScope.TEST, Language.RESOURCES)
+                .collect(Collectors.toList());
+
+        // Should have test resource roots for both modules
+        Set<String> testModules = testResourceRoots.stream()
+                .map(SourceRoot::module)
+                .filter(opt -> opt.isPresent())
+                .map(opt -> opt.get())
+                .collect(Collectors.toSet());
+
+        assertEquals(2, testModules.size(), "Should have test resource roots 
for 2 modules");
+        assertTrue(testModules.contains("org.foo.moduleA"), "Should have test 
resource root for moduleA");
+        assertTrue(testModules.contains("org.foo.moduleB"), "Should have test 
resource root for moduleB");
+    }
+
+    /**
+     * Tests that when modular sources are configured alongside explicit 
legacy resources,
+     * the legacy resources are ignored and a warning is issued.
+     *
+     * This verifies the behavior described in the design:
+     * - Modular projects with explicit legacy {@code <resources>} 
configuration should issue a warning
+     * - The modular resource roots are injected instead of using the legacy 
configuration
+     */
+    @Test
+    void testModularSourcesWithExplicitResourcesIssuesWarning() throws 
Exception {
+        File pom = getProject("modular-sources-with-explicit-resources");
+
+        MavenSession mavenSession = createMavenSession(null);
+        ProjectBuildingRequest configuration = new 
DefaultProjectBuildingRequest();
+        
configuration.setRepositorySession(mavenSession.getRepositorySession());
+
+        ProjectBuildingResult result = getContainer()
+                .lookup(org.apache.maven.project.ProjectBuilder.class)
+                .build(pom, configuration);
+
+        MavenProject project = result.getProject();
+
+        // Verify warnings are issued for ignored legacy resources
+        List<ModelProblem> warnings = result.getProblems().stream()
+                .filter(p -> p.getSeverity() == ModelProblem.Severity.WARNING)
+                .filter(p -> p.getMessage().contains("Legacy") && 
p.getMessage().contains("ignored"))
+                .collect(Collectors.toList());
+
+        assertEquals(2, warnings.size(), "Should have 2 warnings (one for 
resources, one for testResources)");
+        assertTrue(
+                warnings.stream().anyMatch(w -> 
w.getMessage().contains("<resources>")),
+                "Should warn about ignored <resources>");
+        assertTrue(
+                warnings.stream().anyMatch(w -> 
w.getMessage().contains("<testResources>")),
+                "Should warn about ignored <testResources>");
+
+        // Verify modular resources are still injected correctly
+        List<SourceRoot> mainResourceRoots = 
project.getEnabledSourceRoots(ProjectScope.MAIN, Language.RESOURCES)
+                .collect(Collectors.toList());
+
+        assertEquals(2, mainResourceRoots.size(), "Should have 2 modular 
resource roots (one per module)");
+
+        Set<String> mainModules = mainResourceRoots.stream()
+                .map(SourceRoot::module)
+                .filter(opt -> opt.isPresent())
+                .map(opt -> opt.get())
+                .collect(Collectors.toSet());
+
+        assertEquals(2, mainModules.size(), "Should have resource roots for 2 
modules");
+        assertTrue(mainModules.contains("org.foo.moduleA"), "Should have 
resource root for moduleA");
+        assertTrue(mainModules.contains("org.foo.moduleB"), "Should have 
resource root for moduleB");
+    }
 }
diff --git 
a/impl/maven-core/src/test/projects/project-builder/modular-sources-with-explicit-resources/pom.xml
 
b/impl/maven-core/src/test/projects/project-builder/modular-sources-with-explicit-resources/pom.xml
new file mode 100644
index 0000000000..d2bd1a614b
--- /dev/null
+++ 
b/impl/maven-core/src/test/projects/project-builder/modular-sources-with-explicit-resources/pom.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.1.0";
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 
https://maven.apache.org/xsd/maven-4.1.0.xsd";>
+    <modelVersion>4.1.0</modelVersion>
+
+    <groupId>org.apache.maven.tests</groupId>
+    <artifactId>modular-sources-explicit-resources-test</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <packaging>jar</packaging>
+
+    <build>
+        <sources>
+            <!-- Module A - main java -->
+            <source>
+                <scope>main</scope>
+                <lang>java</lang>
+                <module>org.foo.moduleA</module>
+            </source>
+            <!-- Module B - main java -->
+            <source>
+                <scope>main</scope>
+                <lang>java</lang>
+                <module>org.foo.moduleB</module>
+            </source>
+        </sources>
+        <!-- Legacy resources are IGNORED when modular sources are configured 
- a warning should be issued -->
+        <resources>
+            <resource>
+                <directory>src/custom/resources</directory>
+            </resource>
+        </resources>
+        <testResources>
+            <testResource>
+                <directory>src/custom/test-resources</directory>
+            </testResource>
+        </testResources>
+    </build>
+</project>
\ No newline at end of file
diff --git 
a/impl/maven-core/src/test/projects/project-builder/modular-sources/pom.xml 
b/impl/maven-core/src/test/projects/project-builder/modular-sources/pom.xml
new file mode 100644
index 0000000000..2f9b1e7b03
--- /dev/null
+++ b/impl/maven-core/src/test/projects/project-builder/modular-sources/pom.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.1.0";
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 
https://maven.apache.org/xsd/maven-4.1.0.xsd";>
+    <modelVersion>4.1.0</modelVersion>
+
+    <groupId>org.apache.maven.tests</groupId>
+    <artifactId>modular-sources-test</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <packaging>jar</packaging>
+
+    <build>
+        <sources>
+            <!-- Module A - main java -->
+            <source>
+                <scope>main</scope>
+                <lang>java</lang>
+                <module>org.foo.moduleA</module>
+            </source>
+            <!-- Module A - test java -->
+            <source>
+                <scope>test</scope>
+                <lang>java</lang>
+                <module>org.foo.moduleA</module>
+            </source>
+            <!-- Module B - main java -->
+            <source>
+                <scope>main</scope>
+                <lang>java</lang>
+                <module>org.foo.moduleB</module>
+            </source>
+            <!-- Module B - test java -->
+            <source>
+                <scope>test</scope>
+                <lang>java</lang>
+                <module>org.foo.moduleB</module>
+            </source>
+        </sources>
+    </build>
+</project>
\ No newline at end of file

Reply via email to