Skip to content

Commit 3e2d01f

Browse files
committed
Add module-aware resource handling for modular sources
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.
1 parent 25c80d8 commit 3e2d01f

File tree

4 files changed

+409
-4
lines changed

4 files changed

+409
-4
lines changed

impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java

Lines changed: 231 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,8 @@ private void initProject(MavenProject project, ModelBuilderResult result) {
669669
boolean hasScript = false;
670670
boolean hasMain = false;
671671
boolean hasTest = false;
672+
boolean hasMainResources = false;
673+
boolean hasTestResources = false;
672674
for (var source : sources) {
673675
var src = DefaultSourceRoot.fromModel(session, baseDir, outputDirectory, source);
674676
project.addSourceRoot(src);
@@ -680,6 +682,13 @@ private void initProject(MavenProject project, ModelBuilderResult result) {
680682
} else {
681683
hasTest |= ProjectScope.TEST.equals(scope);
682684
}
685+
} else if (Language.RESOURCES.equals(language)) {
686+
ProjectScope scope = src.scope();
687+
if (ProjectScope.MAIN.equals(scope)) {
688+
hasMainResources = true;
689+
} else {
690+
hasTestResources |= ProjectScope.TEST.equals(scope);
691+
}
683692
} else {
684693
hasScript |= Language.SCRIPT.equals(language);
685694
}
@@ -700,11 +709,149 @@ private void initProject(MavenProject project, ModelBuilderResult result) {
700709
if (!hasTest) {
701710
project.addTestCompileSourceRoot(build.getTestSourceDirectory());
702711
}
703-
for (Resource resource : project.getBuild().getDelegate().getResources()) {
704-
project.addSourceRoot(new DefaultSourceRoot(baseDir, ProjectScope.MAIN, resource));
712+
// Extract modules from sources to detect modular projects
713+
Set<String> modules = extractModules(sources);
714+
boolean isModularProject = !modules.isEmpty();
715+
716+
logger.debug(
717+
"Module detection for project {}: found {} module(s) {} - modular project: {}",
718+
project.getId(),
719+
modules.size(),
720+
modules,
721+
isModularProject);
722+
723+
/*
724+
* Handle main resources - modular project has highest priority:
725+
* 1. Modular project: use resources from <sources> if present, otherwise inject defaults
726+
* 2. Classic project: use resources from <sources> if present, otherwise use legacy <resources>
727+
*/
728+
List<Resource> resources = project.getBuild().getDelegate().getResources();
729+
if (isModularProject) {
730+
if (hasMainResources) {
731+
// Modular project with resources configured via <sources> - already added above
732+
if (!resources.isEmpty()
733+
&& !hasOnlySuperPomDefaults(resources, baseDir, ProjectScope.MAIN.id())) {
734+
logger.warn("Legacy <resources> element is ignored because main resources are "
735+
+ "configured via <source><lang>resources</lang></source> in <sources>");
736+
}
737+
logger.debug(
738+
"Main resources configured via <sources> element, ignoring legacy <resources> element");
739+
} else {
740+
// Modular project without resources in <sources> - inject module-aware defaults
741+
if (!resources.isEmpty()
742+
&& !hasOnlySuperPomDefaults(resources, baseDir, ProjectScope.MAIN.id())) {
743+
String message =
744+
"Legacy <resources> element is ignored because modular sources are configured. "
745+
+ "Use <source><lang>resources</lang></source> in <sources> for custom resource paths.";
746+
logger.warn(message);
747+
result.getProblemCollector()
748+
.reportProblem(new org.apache.maven.impl.model.DefaultModelProblem(
749+
message,
750+
Severity.WARNING,
751+
Version.V41,
752+
project.getModel().getDelegate(),
753+
-1,
754+
-1,
755+
null));
756+
}
757+
logger.debug("Injecting module-aware main resource roots for {} modules", modules.size());
758+
for (String module : modules) {
759+
Path resourcePath = baseDir.resolve("src")
760+
.resolve(module)
761+
.resolve(ProjectScope.MAIN.id())
762+
.resolve("resources");
763+
logger.debug(" - Adding main resource root: {} (module: {})", resourcePath, module);
764+
project.addSourceRoot(
765+
createModularResourceRoot(baseDir, module, ProjectScope.MAIN, outputDirectory));
766+
}
767+
}
768+
} else {
769+
// Classic (non-modular) project
770+
if (hasMainResources) {
771+
// Resources configured via <sources> - already added above
772+
if (!resources.isEmpty()
773+
&& !hasOnlySuperPomDefaults(resources, baseDir, ProjectScope.MAIN.id())) {
774+
logger.warn("Legacy <resources> element is ignored because main resources are "
775+
+ "configured via <source><lang>resources</lang></source> in <sources>");
776+
}
777+
logger.debug(
778+
"Main resources configured via <sources> element, ignoring legacy <resources> element");
779+
} else {
780+
// Use legacy <resources> element
781+
logger.debug("Using explicit or default resources ({} resources configured)", resources.size());
782+
for (Resource resource : resources) {
783+
project.addSourceRoot(new DefaultSourceRoot(baseDir, ProjectScope.MAIN, resource));
784+
}
785+
}
705786
}
706-
for (Resource resource : project.getBuild().getDelegate().getTestResources()) {
707-
project.addSourceRoot(new DefaultSourceRoot(baseDir, ProjectScope.TEST, resource));
787+
788+
/*
789+
* Handle test resources - same priority as main resources:
790+
* 1. Modular project: use test resources from <sources> if present, otherwise inject defaults
791+
* 2. Classic project: use test resources from <sources> if present, otherwise use legacy <testResources>
792+
*/
793+
List<Resource> testResources = project.getBuild().getDelegate().getTestResources();
794+
if (isModularProject) {
795+
if (hasTestResources) {
796+
// Modular project with test resources configured via <sources> - already added above
797+
if (!testResources.isEmpty()
798+
&& !hasOnlySuperPomDefaults(testResources, baseDir, ProjectScope.TEST.id())) {
799+
logger.warn(
800+
"Legacy <testResources> element is ignored because test resources are "
801+
+ "configured via <source><lang>resources</lang><scope>test</scope></source> in <sources>");
802+
}
803+
logger.debug(
804+
"Test resources configured via <sources> element, ignoring legacy <testResources> element");
805+
} else {
806+
// Modular project without test resources in <sources> - inject module-aware defaults
807+
if (!testResources.isEmpty()
808+
&& !hasOnlySuperPomDefaults(testResources, baseDir, ProjectScope.TEST.id())) {
809+
String message =
810+
"Legacy <testResources> element is ignored because modular sources are configured. "
811+
+ "Use <source><lang>resources</lang><scope>test</scope></source> in <sources> for custom resource paths.";
812+
logger.warn(message);
813+
result.getProblemCollector()
814+
.reportProblem(new org.apache.maven.impl.model.DefaultModelProblem(
815+
message,
816+
Severity.WARNING,
817+
Version.V41,
818+
project.getModel().getDelegate(),
819+
-1,
820+
-1,
821+
null));
822+
}
823+
logger.debug("Injecting module-aware test resource roots for {} modules", modules.size());
824+
for (String module : modules) {
825+
Path resourcePath = baseDir.resolve("src")
826+
.resolve(module)
827+
.resolve(ProjectScope.TEST.id())
828+
.resolve("resources");
829+
logger.debug(" - Adding test resource root: {} (module: {})", resourcePath, module);
830+
project.addSourceRoot(
831+
createModularResourceRoot(baseDir, module, ProjectScope.TEST, outputDirectory));
832+
}
833+
}
834+
} else {
835+
// Classic (non-modular) project
836+
if (hasTestResources) {
837+
// Test resources configured via <sources> - already added above
838+
if (!testResources.isEmpty()
839+
&& !hasOnlySuperPomDefaults(testResources, baseDir, ProjectScope.TEST.id())) {
840+
logger.warn(
841+
"Legacy <testResources> element is ignored because test resources are "
842+
+ "configured via <source><lang>resources</lang><scope>test</scope></source> in <sources>");
843+
}
844+
logger.debug(
845+
"Test resources configured via <sources> element, ignoring legacy <testResources> element");
846+
} else {
847+
// Use legacy <testResources> element
848+
logger.debug(
849+
"Using explicit or default test resources ({} resources configured)",
850+
testResources.size());
851+
for (Resource resource : testResources) {
852+
project.addSourceRoot(new DefaultSourceRoot(baseDir, ProjectScope.TEST, resource));
853+
}
854+
}
708855
}
709856
}
710857

@@ -1099,6 +1246,86 @@ public Set<Entry<K, V>> entrySet() {
10991246
}
11001247
}
11011248

1249+
/**
1250+
* Extracts unique module names from the given list of source elements.
1251+
* A project is considered modular if it has at least one module name.
1252+
*
1253+
* @param sources list of source elements from the build
1254+
* @return set of non-blank module names
1255+
*/
1256+
private Set<String> extractModules(List<org.apache.maven.api.model.Source> sources) {
1257+
return sources.stream()
1258+
.map(org.apache.maven.api.model.Source::getModule)
1259+
.filter(Objects::nonNull)
1260+
.map(String::trim)
1261+
.filter(s -> !s.isBlank())
1262+
.collect(Collectors.toSet());
1263+
}
1264+
1265+
/**
1266+
* Creates a DefaultSourceRoot for module-aware resource directories.
1267+
* Generates paths following the pattern: src/&lt;module&gt;/&lt;scope&gt;/resources
1268+
*
1269+
* @param baseDir base directory of the project
1270+
* @param module module name
1271+
* @param scope project scope (main or test)
1272+
* @param outputDirectory function providing output directory for the scope
1273+
* @return configured DefaultSourceRoot for the module's resources
1274+
*/
1275+
private DefaultSourceRoot createModularResourceRoot(
1276+
Path baseDir, String module, ProjectScope scope, Function<ProjectScope, String> outputDirectory) {
1277+
Path resourceDir =
1278+
baseDir.resolve("src").resolve(module).resolve(scope.id()).resolve("resources");
1279+
1280+
return new DefaultSourceRoot(
1281+
scope,
1282+
Language.RESOURCES,
1283+
module,
1284+
null, // targetVersion
1285+
resourceDir,
1286+
null, // includes
1287+
null, // excludes
1288+
false, // stringFiltering
1289+
Path.of(module), // targetPath - resources go to target/classes/<module>
1290+
true // enabled
1291+
);
1292+
}
1293+
1294+
/**
1295+
* Checks if the given resource list contains only Super POM default resources.
1296+
* Super POM defaults are: src/{scope}/resources and src/{scope}/resources-filtered
1297+
*
1298+
* @param resources list of resources to check
1299+
* @param baseDir project base directory
1300+
* @param scope scope (main or test)
1301+
* @return true if only Super POM defaults are present
1302+
*/
1303+
private boolean hasOnlySuperPomDefaults(List<Resource> resources, Path baseDir, String scope) {
1304+
if (resources.isEmpty()) {
1305+
return false;
1306+
}
1307+
1308+
// Super POM default paths
1309+
String defaultPath =
1310+
baseDir.resolve("src").resolve(scope).resolve("resources").toString();
1311+
String defaultFilteredPath = baseDir.resolve("src")
1312+
.resolve(scope)
1313+
.resolve("resources-filtered")
1314+
.toString();
1315+
1316+
// Check if all resources are Super POM defaults
1317+
for (Resource resource : resources) {
1318+
String resourceDir = resource.getDirectory();
1319+
if (resourceDir != null && !resourceDir.equals(defaultPath) && !resourceDir.equals(defaultFilteredPath)) {
1320+
// Found a non-default resource
1321+
return false;
1322+
}
1323+
}
1324+
1325+
logger.debug("Detected only Super POM default resources for scope: {}", scope);
1326+
return true;
1327+
}
1328+
11021329
private Model injectLifecycleBindings(
11031330
Model model,
11041331
ModelBuilderRequest request,

0 commit comments

Comments
 (0)