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