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 a1a331ab6940..99289fc7aff6 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 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> 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 extractModules(List 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 000000000000..48fc9e7e03c9 --- /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 modules; + private final boolean modularProject; + private final ModelBuilderResult result; + + ResourceHandlingContext( + MavenProject project, + Path baseDir, + Set 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: + *
    + *
  1. Modular project: use resources from {@code } if present, otherwise inject defaults
  2. + *
  3. Classic project: use resources from {@code } if present, otherwise use legacy resources
  4. + *
+ * + * @param scope the project scope (MAIN or TEST) + * @param hasResourcesInSources whether resources are configured via {@code } + */ + void handleResourceConfiguration(ProjectScope scope, boolean hasResourcesInSources) { + List 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 ? "" : ""; + String sourcesConfig = scope == ProjectScope.MAIN + ? "resources" + : "resourcestest"; + + if (modularProject) { + if (hasResourcesInSources) { + // Modular project with resources configured via - already added above + if (hasExplicitLegacyResources(resources, scopeId)) { + LOGGER.warn( + "Legacy {} element is ignored because {} resources are configured via {} in .", + legacyElement, + scopeId, + sourcesConfig); + } else { + LOGGER.debug( + "{} resources configured via element, ignoring legacy {} element.", + scopeName, + legacyElement); + } + } else { + // Modular project without resources in - inject module-aware defaults + if (hasExplicitLegacyResources(resources, scopeId)) { + String message = "Legacy " + legacyElement + + " element is ignored because modular sources are configured. " + + "Use " + sourcesConfig + " in 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 - already added above + if (hasExplicitLegacyResources(resources, scopeId)) { + LOGGER.warn( + "Legacy {} element is ignored because {} resources are configured via {} in .", + legacyElement, + scopeId, + sourcesConfig); + } else { + LOGGER.debug( + "{} resources configured via 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///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/ + 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 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 69b1aef2270e..a8825bc5245b 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 mainResourceRoots = project.getEnabledSourceRoots(ProjectScope.MAIN, Language.RESOURCES) + .collect(Collectors.toList()); + + // Should have resource roots for both modules + Set 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 testResourceRoots = project.getEnabledSourceRoots(ProjectScope.TEST, Language.RESOURCES) + .collect(Collectors.toList()); + + // Should have test resource roots for both modules + Set 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 } 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 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("")), + "Should warn about ignored "); + assertTrue( + warnings.stream().anyMatch(w -> w.getMessage().contains("")), + "Should warn about ignored "); + + // Verify modular resources are still injected correctly + List mainResourceRoots = project.getEnabledSourceRoots(ProjectScope.MAIN, Language.RESOURCES) + .collect(Collectors.toList()); + + assertEquals(2, mainResourceRoots.size(), "Should have 2 modular resource roots (one per module)"); + + Set 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 000000000000..d2bd1a614b3f --- /dev/null +++ b/impl/maven-core/src/test/projects/project-builder/modular-sources-with-explicit-resources/pom.xml @@ -0,0 +1,39 @@ + + + 4.1.0 + + org.apache.maven.tests + modular-sources-explicit-resources-test + 1.0-SNAPSHOT + jar + + + + + + main + java + org.foo.moduleA + + + + main + java + org.foo.moduleB + + + + + + src/custom/resources + + + + + src/custom/test-resources + + + + \ 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 000000000000..2f9b1e7b0371 --- /dev/null +++ b/impl/maven-core/src/test/projects/project-builder/modular-sources/pom.xml @@ -0,0 +1,40 @@ + + + 4.1.0 + + org.apache.maven.tests + modular-sources-test + 1.0-SNAPSHOT + jar + + + + + + main + java + org.foo.moduleA + + + + test + java + org.foo.moduleA + + + + main + java + org.foo.moduleB + + + + test + java + org.foo.moduleB + + + + \ No newline at end of file