Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,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);
Expand All @@ -680,6 +682,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 {
hasTestResources |= ProjectScope.TEST.equals(scope);
}
} else {
hasScript |= Language.SCRIPT.equals(language);
}
Expand All @@ -700,12 +709,27 @@ 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.debug(
"Module detection for project {}: found {} module(s) {} - modular project: {}",
project.getId(),
modules.size(),
modules,
isModularProject);

// Handle main and test resources using shared method
ResourceHandlingContext context =
new ResourceHandlingContext(project, baseDir, modules, isModularProject, result);
handleResourceConfiguration(
context, project.getBuild().getDelegate().getResources(), hasMainResources, ProjectScope.MAIN);
handleResourceConfiguration(
context,
project.getBuild().getDelegate().getTestResources(),
hasTestResources,
ProjectScope.TEST);
}

project.setActiveProfiles(
Expand Down Expand Up @@ -1099,6 +1123,190 @@ public Set<Entry<K, V>> entrySet() {
}
}

/**
* Context object for resource handling configuration.
* Groups parameters shared between main and test resource handling to reduce method parameter count.
*/
private record ResourceHandlingContext(
MavenProject project,
Path baseDir,
Set<String> modules,
boolean modularProject,
ModelBuilderResult 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 &lt;sources&gt; if present, otherwise inject defaults</li>
* <li>Classic project: use resources from &lt;sources&gt; if present, otherwise use legacy resources</li>
* </ol>
*
* @param ctx the resource handling context containing project info
* @param resources the legacy resource list (from &lt;resources&gt; or &lt;testResources&gt;)
* @param hasResourcesInSources whether resources are configured via &lt;sources&gt;
* @param scope the project scope (MAIN or TEST)
*/
private void handleResourceConfiguration(
ResourceHandlingContext ctx, List<Resource> resources, boolean hasResourcesInSources, ProjectScope scope) {

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 (ctx.modularProject()) {
if (hasResourcesInSources) {
// Modular project with resources configured via <sources> - already added above
if (hasExplicitLegacyResources(resources, ctx.baseDir(), scopeId)) {
logger.warn(
"Legacy {} element is ignored because {} resources are configured via {} in <sources>",
legacyElement,
scopeId,
sourcesConfig);
}
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, ctx.baseDir(), scopeId)) {
String message = "Legacy " + legacyElement
+ " element is ignored because modular sources are configured. "
+ "Use " + sourcesConfig + " in <sources> for custom resource paths.";
logger.warn(message);
ctx.result()
.getProblemCollector()
.reportProblem(new org.apache.maven.impl.model.DefaultModelProblem(
message,
Severity.WARNING,
Version.V41,
ctx.project().getModel().getDelegate(),
-1,
-1,
null));
}
logger.debug(
"Injecting module-aware {} resource roots for {} modules",
scopeId,
ctx.modules().size());
for (String module : ctx.modules()) {
Path resourcePath = ctx.baseDir()
.resolve("src")
.resolve(module)
.resolve(scopeId)
.resolve("resources");
logger.debug(" - Adding {} resource root: {} (module: {})", scopeId, resourcePath, module);
ctx.project().addSourceRoot(createModularResourceRoot(ctx.baseDir(), module, scope));
}
}
} else {
// Classic (non-modular) project
if (hasResourcesInSources) {
// Resources configured via <sources> - already added above
if (hasExplicitLegacyResources(resources, ctx.baseDir(), scopeId)) {
logger.warn(
"Legacy {} element is ignored because {} resources are configured via {} in <sources>",
legacyElement,
scopeId,
sourcesConfig);
}
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) {
ctx.project().addSourceRoot(new DefaultSourceRoot(ctx.baseDir(), scope, resource));
}
}
}
}

/**
* 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());
}

/**
* Creates a DefaultSourceRoot for module-aware resource directories.
* Generates paths following the pattern: src/&lt;module&gt;/&lt;scope&gt;/resources
*
* @param baseDir base directory of the project
* @param module module name
* @param scope project scope (main or test)
* @return configured DefaultSourceRoot for the module's resources
*/
private DefaultSourceRoot createModularResourceRoot(Path baseDir, 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 baseDir project base directory
* @param scope scope (main or test)
* @return true if explicit legacy resources are present that would be ignored
*/
private boolean hasExplicitLegacyResources(List<Resource> resources, Path baseDir, 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;
}
}

logger.debug("Only Super POM default resources found for scope: {}", scope);
return false;
}

private Model injectLifecycleBindings(
Model model,
ModelBuilderRequest request,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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");
}
}
Loading