Hi, based on my work to migrate a number of Java Modules (aka. Jigsaw/JPMS) to Maven (4), I did a deep investigation of the current implementation. I already discussed with some of you on Slack about my work and one thing we found is the missing handling of resources when (Java) modules are used.
At first I just tried to implement proper resource handling (cf. my proposed, yet draft, PR). But the deeper I looked into it, I found Maven support for Java Modules incomplete, in particular wrt the old (Maven <= 3) conventions. Therefore, I have now created a deep analysis based on some typical examples. The analysis culminates in a full matrix of combinations of sources/resources etc. I also propose how to handle the different cases. Please have a look into it and provide feedback about it. The subject is rather complex, so I might have missed important view points. Perhaps other media are better suited to discuss the different cases and their consequences (in particular wrt. to warnings and errors)? If its helpful, I’d be happy to place the stuff into the Maven Wiki (Confluence). However, in the end, the work should result in some decisions. I propose the following Handle model loading and proper handling of sources/resources completely in the core (currently, the compiler plugin contributes to some handling, see below) Implement a strict approach how to handle misconfigurations (this is mostly about warnings vs. errors, see below). Personally, I plead for a very strict handling (errors) here as the feature is new with Maven 4 and have no need to handle legacy code about Java modules. Build this handling into the Maven core and provide proper documentation. Perhaps redraw some of the existing error handling from the compiler plugin as it much better suites into the core (sorry, Martin). Of course, I hereby volunteer to perform the implementation. I have already started with some of it, to be honest. For phase 1 (proper handling of resources in the context of <sources> and <modules> I have implemented a solution (cf. Draft PR <https://github.com/apache/maven/pull/11505> on maven-core). The nice thing about this implementation is, that even M-Resources-P in version 3.x can handle the resources with this Maven core as it is using the proper abstractions. Regards -- Gerd Aschemann --- Veröffentlichen heißt Verändern (Carmen Thomas) +49/173/3264070 -- [email protected] -- http://www.aschemann.net Build Sources Validation This document captures a discussion about validating <sources> configuration in Maven 4.x. 0. Scope This document focuses exclusively on the Maven 4.x core implementation (master branch of apache/maven <https://github.com/apache/maven>). Specifically: The <sources> element introduced in POM model version 4.1.0 The source/resource handling logic in DefaultProjectBuilder.java Validation gaps in the core that should be addressed 0.1 Compiler Plugin Compensation Some validation gaps in the core are currently compensated by the maven-compiler-plugin, which performs its own validation at compile time, e.g., Mixed modular/non-modular sources → compiler plugin fails with "Mix of modular and non-modular sources", cf. ToolExecutor.java#L595-L608 <https://github.com/apache/maven-compiler-plugin/blob/050664790af28be9107aff6172f8e61adcb8ed8a/src/main/java/org/apache/maven/plugin/compiler/ToolExecutor.java#L595-L608> However, relying on plugin-level validation is suboptimal: Later feedback: Errors appear during compilation, not during POM processing No line numbers: Plugin errors cannot reference the exact POM location Plugin-specific: Other language plugins (Kotlin, Groovy, Scala) may not have equivalent validation Incomplete coverage: Some invalid configurations are not caught at all (e.g., test-only modular projects) The goal should be to implement proper validation in the Maven core, providing early, consistent feedback with precise error locations via ModelProblem. 0.2 Origin of This Discussion This discussion was triggered by investigating how to best implement resource handling in the new modular source directory hierarchy. Maven 4.x introduces a unified <sources> element that supports modular layouts like: src/ ├── org.foo.moduleA/ │ ├── main/ │ │ ├── java/ │ │ └── resources/ ← Module-specific resources │ └── test/ │ ├── java/ │ └── resources/ └── org.foo.moduleB/ ├── main/ │ ├── java/ │ └── resources/ ← Module-specific resources └── test/ However, the current implementation does not automatically pick up resources from the modular paths (src/<module>/main/resources). Instead, it always uses the legacy <resources> element which defaults to src/main/resources. Solution: See Phase 1 <http://localhost:63345/markdownPreview/1827621791/markdown-preview-index-9kc3mhenfb2quhf90a0ci0f472.html#41-phase-1-module-aware-resource-handling-implemented> for the implementation that automatically injects module-aware resources. Current Workaround: Users can configure the maven-resources-plugin with explicit executions for each module: <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <executions> <execution> <id>copy-resources-moduleA</id> <phase>process-resources</phase> <goals><goal>copy-resources</goal></goals> <configuration> <resources> <resource> <directory>src/org.foo.moduleA/main/resources</directory> </resource> </resources> <outputDirectory>${project.build.outputDirectory}</outputDirectory> </configuration> </execution> <execution> <id>copy-resources-moduleB</id> <phase>process-resources</phase> <goals><goal>copy-resources</goal></goals> <configuration> <resources> <resource> <directory>src/org.foo.moduleB/main/resources</directory> </resource> </resources> <outputDirectory>${project.build.outputDirectory}</outputDirectory> </configuration> </execution> </executions> </plugin> This workaround is verbose and error-prone. The investigation into implementing this properly in the core led to the Phase 1 implementation <http://localhost:63345/markdownPreview/1827621791/markdown-preview-index-9kc3mhenfb2quhf90a0ci0f472.html#41-phase-1-module-aware-resource-handling-implemented> and revealed the broader set of validation and configuration handling issues documented here. 1. Problem Statement Let's investigate some of the problems with the current implementation of <sources> handling in Maven 4.x. Maven 4.x introduces a new <sources> element that supports modular project layouts (src/<module>/<scope>/<lang>). However, the current implementation has several issues when handling the interaction between the new <sources> element and legacy configuration elements (<sourceDirectory>, <resources>, etc.). This is not a comprehensive list, but highlights key problems that should be addressed in the core. A systematic Analysis and Proposed Solutions follow in subsequent sections. Problem 1: Mixed Modular/Non-Modular Sources Accepted Silently Mixing modular and non-modular sources in <sources> is an invalid configuration that will fail at compile time, but DefaultProjectBuilder accepts it silently without early validation. Example - Mixed configuration: <build> <sources> <source> <scope>main</scope> <lang>java</lang> <module>org.foo.moduleA</module> <!-- Modular --> </source> <source> <scope>main</scope> <lang>java</lang> <!-- No module - classic style --> </source> </sources> </build> Expected Behavior Actual Behavior Early warning/error about inconsistent configuration Silent acceptance by DefaultProjectBuilder Clear feedback during project build Delayed failure at compile time Current Workaround: The maven-compiler-plugin validates this at compile time and fails with: "Mix of modular and non-modular sources." See above (Compiler Plugin Compensation <http://localhost:63345/markdownPreview/1827621791/markdown-preview-index-9kc3mhenfb2quhf90a0ci0f472.html#compiler-plugin-compensation>) for drawbacks of this approach. Problem 2: Test-Only Modular Projects Get Classic Main Sources Injected When a project configures only test sources in <sources> (no main sources), DefaultProjectBuilder silently injects the classic src/main/java as a main source directory. Example - Integration test module with only test sources: <build> <sources> <source> <scope>test</scope> <lang>java</lang> <module>org.foo.integrationTests</module> </source> </sources> </build> Expected Behavior Actual Behavior No main sources (project is test-only) src/main/java silently added as main source Test sources from src/org.foo.integrationTests/test/java Test sources work correctly Root cause: The hasMain boolean remains false when no <source> has scope=main + lang=java. Line 697-698 then adds build.getSourceDirectory() (defaults to src/main/java from Super POM). See: DefaultProjectBuilder.java#L697-L698 <https://github.com/apache/maven/blob/25c80d8ece4252421c9dce5c34fb46a21c7b9f23/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java#L697-L698> Impact: If src/main/java doesn't exist → probably harmless (empty directory) If src/main/java exists with old/unrelated code → it gets compiled! Modular test project silently becomes a mixed modular-test + classic-main project Use cases affected: Integration test modules (only contain tests) Test fixtures modules (shared test utilities) BOM/parent projects with test verification but no main code Note: Unlike Problem 1, this is not caught by the compiler plugin - it silently succeeds but with potentially wrong behavior. Problem 3: Resources Are Not Module-Aware When a project uses modular sources, resources should follow the same modular layout. Currently, they don't. Example - Modular Java sources without explicit resources: <build> <sources> <source> <scope>main</scope> <lang>java</lang> <module>org.foo.moduleA</module> </source> <source> <scope>main</scope> <lang>java</lang> <module>org.foo.moduleB</module> </source> </sources> </build> Expected Behavior Actual Behavior Resources from src/org.foo.moduleA/main/resources Resources from src/main/resources only Resources from src/org.foo.moduleB/main/resources (legacy path, not module-aware) Impact: Module-specific resources (like module-specific configs) cannot be organized per-module. Solution: See Phase 1 <http://localhost:63345/markdownPreview/1827621791/markdown-preview-index-9kc3mhenfb2quhf90a0ci0f472.html#41-phase-1-module-aware-resource-handling-implemented> for the implementation. Workaround (no longer needed): See Origin of This Discussion <http://localhost:63345/markdownPreview/1827621791/markdown-preview-index-9kc3mhenfb2quhf90a0ci0f472.html#origin-of-this-discussion> for how to configure the maven-resources-plugin with explicit executions for each module. Problem 4: Legacy Configuration Silently Ignored When <sources> is present, legacy elements like <sourceDirectory> are silently ignored without warning. Example - Explicit sourceDirectory with sources: <build> <sourceDirectory>src/custom/java</sourceDirectory> <!-- User expects this to be used --> <sources> <source> <scope>main</scope> <lang>java</lang> <module>org.foo.bar</module> </source> </sources> </build> Expected Behavior Actual Behavior Warning: "sourceDirectory is ignored because sources are configured" Silent - user doesn't know their config is ignored Clear feedback to user Confusion when src/custom/java isn't used 2. Analysis 2.1 Core Design Question How should the source reading and validation logic work? The fundamental question is about priority and conflict resolution when both new (<sources>) and legacy (<sourceDirectory>, <resources>) configuration elements are present. Key Design Principle: Modular Sources Have Priority When a project uses the new <sources> element with modular configuration (<module> element), the modular approach should take precedence: Modular sources (<sources> with <module>) demand proper modular layout and behavior Non-modular sources (<sources> without <module>) should not be used (error or warning) in a modular project Legacy configuration (<sourceDirectory>, <resources>) is used only as fallback when no <sources> are configured This principle ensures: Clear, predictable behavior No silent mixing of old and new approaches Users explicitly opt-in to the new model Path Resolution Logic From DefaultSourceRoot.fromModel() — abbreviated pseudo code of the current implementation <https://github.com/apache/maven/blob/25c80d8ece4252421c9dce5c34fb46a21c7b9f23/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultSourceRoot.java#L135-L141>: if (module specified) { // Modular mode path = directory specified ? baseDir.resolve(directory) : baseDir/src/<module>/<scope>/<lang> } else { // Classic mode path = directory specified ? baseDir.resolve(directory) : baseDir/src/<scope>/<lang> } Module Directory Mode Result No No Classic src/<scope>/<lang> (default) No Yes Classic <directory> (override) Yes No Modular src/<module>/<scope>/<lang> (default) Yes Yes Modular <directory> (override, module = metadata) 2.2 Strict vs Lenient Approach Two approaches are possible for handling configuration conflicts: Approach A: Strict (Fail Fast) Configuration conflicts result in errors that fail the build. Rule Condition Severity Message R1 <sources> present AND explicit <sourceDirectory> ERROR "Cannot combine <sources> with explicit <sourceDirectory>. Use one or the other." R2 <sources> with mixed module/no-module elements ERROR "Cannot mix modular and non-modular sources. All <source> elements must either have a module or none." R3 Duplicate <source> (same scope/lang/module) ERROR "Duplicate source definition for scope='X', lang='Y', module='Z'" R4 <sources> configured but classic directory exists on filesystem WARN "Directory 'src/main/java' exists but will be ignored because <sources> is configured" Approach B: Lenient (Warn and Continue) Configuration conflicts result in warnings but the build continues. Rule Condition Severity Message R1' <sources> present AND explicit <sourceDirectory> WARN "Explicit <sourceDirectory> will be ignored because <sources> is configured." R2' <sources> with mixed module/no-module elements WARN "Mixing modular and non-modular sources may lead to unexpected behavior." R3 Duplicate <source> (same scope/lang/module) ERROR "Duplicate source definition for scope='X', lang='Y', module='Z'" R4 <sources> configured but classic directory exists on filesystem WARN "Directory 'src/main/java' exists but will be ignored because <sources> is configured" Comparison Aspect Strict (A) Lenient (B) <sources> + explicit SD/TSD ERROR WARN Mixed module/no-module ERROR WARN Duplicate sources ERROR ERROR Filesystem mismatch WARN WARN Migration friendliness Lower Higher User confusion risk Lower Higher Fail-fast principle Yes No Recommendation Approach A (Strict) is recommended for new projects because: Configuration conflicts indicate user error/misunderstanding Silent ignoring of explicit configuration is confusing Better to fail early than have unexpected behavior at compile time Users can easily fix by removing the conflicting element Approach B (Lenient) could be considered if: Migration from Maven 3.x to 4.x needs to be smoother Tooling (IDEs) may generate both elements during transition A deprecation period is desired before enforcing strict rules 2.3 Unified Permutation Matrix This matrix shows all configuration combinations and their expected behavior under each approach. Legend Abbreviation Meaning SD <sourceDirectory> - classic Maven 3.x element for main Java sources TSD <testSourceDirectory> - classic Maven 3.x element for test Java sources S <source> element within <sources> - Maven 4.x way to define sources R <resources> / <resource> - classic Maven 3.x element for resources M <module> element within <source> - specifies JPMS module name Java Sources # Configuration Current Lenient Strict Compiler Plugin No <sources> (classic mode) 1 SD=implicit (Super POM) src/main/java OK OK - 2 SD=explicit user's path OK OK - <sources> present (new mode) 3 S(no M), SD=implicit src/main/java OK OK - 4 S(no M), SD=explicit Silent ignore WARN ERROR Not caught 5 S(M=X), SD=implicit src/X/main/java OK OK - 6 S(M=X), SD=explicit Silent ignore WARN ERROR Not caught 7 S(M=X) + S(M=Y) Both paths OK OK - 8 S(M=X) + S(no M) Silent accept WARN ERROR Caught ✓ 9 S(dir=custom, no M) custom path OK OK - 10 S(dir=custom, M=X) custom (M=meta) OK OK - 11 Duplicate S Silent accept ERROR ERROR Not caught 12 S(M=X) + src/main/java exists Silent ignore WARN WARN Not caught 13 S(scope=test, M=X) only Injects src/main/java No inject No inject Not caught ⚠️ Resources # S Config R Config Current Proposed (Phase 1) Lenient Strict 1 S(lang=resources) R=any Duplicates! Use S, WARN if R explicit Use S, WARN Use S, WARN 2 S(java, no M) R=implicit src/main/resources src/main/resources OK OK 3 S(java, no M) R=explicit User's path User's path OK OK 4 S(java, M=X) R=implicit src/main/resources Inject modular Inject modular Inject modular 5 S(java, M=X) R=explicit User's path Inject modular, WARN WARN ERROR 3. Current Implementation Status As of commit 25c80d8 <https://github.com/apache/maven/blob/25c80d8ece4252421c9dce5c34fb46a21c7b9f23/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java#L645-L708> Implementation Location The source/resource handling logic is in DefaultProjectBuilder.initProject(): // Lines 669-686: Iterate over <sources> and track what's present boolean hasScript = false; boolean hasMain = false; // true if ANY <source> has lang=java, scope=main boolean hasTest = false; // true if ANY <source> has lang=java, scope=test for (var source : sources) { var src = DefaultSourceRoot.fromModel(session, baseDir, outputDirectory, source); project.addSourceRoot(src); // ... tracking logic } // Lines 694-701: Decide whether to use legacy sourceDirectory/testSourceDirectory // (Lines 687-692 contain a comment explaining this behavior) if (!hasScript) { project.addScriptSourceRoot(build.getScriptSourceDirectory()); } if (!hasMain) { project.addCompileSourceRoot(build.getSourceDirectory()); // Silent fallback } if (!hasTest) { project.addTestCompileSourceRoot(build.getTestSourceDirectory()); // Silent fallback } // Lines 703-708: Resources are ALWAYS added from legacy elements (no checks!) 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)); } Gap Analysis Issue Description Core Status Compiler Plugin No warnings Explicit <sourceDirectory> silently ignored when <sources> present Not implemented Not caught No mixed validation Mixing S(M=X) with S(no M) silently accepted Not implemented Caught ✓ Test-only injection src/main/java injected for test-only modular projects Not implemented Not caught ⚠️ No duplicates check Same source can be defined multiple times Not implemented Not caught No modular resources Resources always from legacy <resources> element ✅ Implemented (Phase 1 <http://localhost:63345/markdownPreview/1827621791/markdown-preview-index-9kc3mhenfb2quhf90a0ci0f472.html#41-phase-1-module-aware-resource-handling-implemented>) N/A Key insight: Only the mixed modular/non-modular validation (#8) is currently caught by the compiler plugin. All other issues require core implementation. What Works Processing <source> elements and adding them as source roots Fallback to legacy <sourceDirectory> when no Java sources in <sources> Path resolution for both modular and classic layouts What's Missing Warnings when explicit configuration is ignored Validation for mixed modular/non-modular sources (currently deferred to compiler plugin) Correct handling of test-only modular projects (no main injection) Duplicate detection Modular resource handling ✅ Implemented in Phase 1 <http://localhost:63345/markdownPreview/1827621791/markdown-preview-index-9kc3mhenfb2quhf90a0ci0f472.html#41-phase-1-module-aware-resource-handling-implemented> 4. Implementation Approaches 4.1 Phase 1: Module-Aware Resource Handling (Implemented) Goal: Enable modular resource handling without requiring explicit maven-resources-plugin configuration. Status: ✅ Implemented in 3e2d01f <https://github.com/support-and-care/maven/commit/3e2d01f7ffc5cd52cbdbd48a39dd87a3ea9b38b3> Implementation: Resource tracking via <sources>: (Lines 672-673 <https://github.com/support-and-care/maven/blob/3e2d01f7ffc5cd52cbdbd48a39dd87a3ea9b38b3/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java#L672-L673>) boolean hasMainResources = false; boolean hasTestResources = false; for (var source : sources) { if (Language.RESOURCES.equals(language)) { if (ProjectScope.MAIN.equals(scope)) { hasMainResources = true; } else { hasTestResources |= ProjectScope.TEST.equals(scope); } } } Module extraction to detect modular projects: (Lines 713-714 <https://github.com/support-and-care/maven/blob/3e2d01f7ffc5cd52cbdbd48a39dd87a3ea9b38b3/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java#L713-L714>, extractModules() at L1256 <https://github.com/support-and-care/maven/blob/3e2d01f7ffc5cd52cbdbd48a39dd87a3ea9b38b3/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java#L1256-L1272>) Set<String> modules = extractModules(sources); boolean isModularProject = !modules.isEmpty(); Module-aware resource injection for modular projects: (Lines 729-770 <https://github.com/support-and-care/maven/blob/3e2d01f7ffc5cd52cbdbd48a39dd87a3ea9b38b3/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java#L729-L770>, createModularResourceRoot() at L1275 <https://github.com/support-and-care/maven/blob/3e2d01f7ffc5cd52cbdbd48a39dd87a3ea9b38b3/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java#L1275-L1300>) If resources configured via <sources>: use them (already added during iteration) If no resources in <sources>: inject module-aware defaults for each module Warn (as ModelProblem) if legacy <resources> is present but ignored if (isModularProject) { if (hasMainResources) { // Already added via <sources>, warn if legacy <resources> present } else { // Inject module-aware defaults: src/<module>/main/resources for (String module : modules) { project.addSourceRoot(createModularResourceRoot(baseDir, module, ProjectScope.MAIN, ...)); } } } Super POM default detection: (hasOnlySuperPomDefaults() at L1303 <https://github.com/support-and-care/maven/blob/3e2d01f7ffc5cd52cbdbd48a39dd87a3ea9b38b3/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java#L1303-L1331>) hasOnlySuperPomDefaults() checks if <resources> contains only inherited defaults Warnings are only issued for explicitly configured legacy resources, not Super POM defaults Priority Hierarchy (Proposed): Priority Condition Behavior 1 Modular project + resources in <sources> Use <sources> resources, warn if legacy present 2 Modular project + no resources in <sources> Inject src/<module>/<scope>/resources for each module 3 Classic project + resources in <sources> Use <sources> resources, warn if legacy present 4 Classic project + no resources in <sources> Use legacy <resources> element Example - Would Work Without Explicit Plugin Configuration: <build> <sources> <source> <scope>main</scope> <lang>java</lang> <module>org.foo.moduleA</module> </source> <source> <scope>main</scope> <lang>java</lang> <module>org.foo.moduleB</module> </source> </sources> </build> Resources would be automatically picked up from: src/org.foo.moduleA/main/resources src/org.foo.moduleB/main/resources What's NOT Addressed in This Proposal: Problem 1: Mixed modular/non-modular sources validation (still deferred to compiler plugin) Problem 2: Test-only modular project main injection Problem 4: Warning when explicit <sourceDirectory> is ignored Duplicate source detection Per-module tracking (project-level booleans still used) 4.2 Phase 2: Comprehensive Refactoring Goal: Full implementation of the design principles from Section 2 with proper validation. Scope: Per-module tracking instead of project-level booleans: record ModuleConfig(boolean hasMain, boolean hasTest, boolean hasMainResources, boolean hasTestResources) {} Map<String, ModuleConfig> moduleConfigs = new HashMap<>(); Validation in ModelValidator (not DefaultProjectBuilder): Move conflict detection to validateEffectiveModel() Report ModelProblem with line numbers and severity Early feedback during POM processing Strict validation rules (from Section 2.2): Rule Condition Severity Message R1 <sources> + explicit <sourceDirectory> ERROR "Cannot combine <sources> with explicit <sourceDirectory>" R2 Mixed modular/non-modular sources ERROR "Cannot mix modular and non-modular sources" R3 Duplicate <source> definition ERROR "Duplicate source for scope='X', lang='Y', module='Z'" R4 <sources> configured but classic dir exists WARN "Directory 'src/main/java' exists but will be ignored" Fix test-only modular project injection (Problem 2): // Don't inject legacy main sources for modular projects if (!hasMain && !isModularProject) { project.addCompileSourceRoot(build.getSourceDirectory()); } Warning when explicit configuration is ignored (Problem 4): if (hasMain && isExplicitSourceDirectory(build)) { // Report ModelProblem with line number } InputLocation-based detection for explicit vs inherited configuration: InputLocation location = build.getLocation("sourceDirectory"); boolean isExplicit = location != null && !isSuperPomLocation(location); Implementation Location: Component Responsibility DefaultModelValidator Validation rules R1-R4, early feedback with line numbers DefaultProjectBuilder Source root creation, fallback logic DefaultSourceRoot Path resolution (already implemented) Implementation Timeline Phase Scope Status Phase 1 Module-aware resource handling ✅ Implemented (3e2d01f <https://github.com/support-and-care/maven/commit/3e2d01f7ffc5cd52cbdbd48a39dd87a3ea9b38b3>) Phase 2 Full validation + per-module tracking + ModelValidator integration Pending 5. Reference Material Model Version vs Maven Version Model Version Maven Version Notes 4.0.0 Maven 3.x Classic POM model, no <sources> element 4.1.0 Maven 4.x Introduces <sources> element 4.2.0 Future Reserved for future use The <sources> element requires model version 4.1.0: <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> ... </project> The Maven 4.x Unified Source Model In Maven 4.x, all source types can be configured via <sources>: <sources> <!-- Java sources --> <source> <scope>main</scope> <lang>java</lang> <module>org.foo.bar</module> </source> <!-- Resources --> <source> <scope>main</scope> <lang>resources</lang> <module>org.foo.bar</module> </source> </sources> The <lang> element accepts: java - compiled by compiler plugin (default if omitted) resources - processed by resources plugin script - deprecated, use resources instead Extensible via LanguageProvider SPI for other languages (Kotlin, Groovy, etc.). Processing Flow ┌─────────────────────────────────────────────────────────────────┐ │ POM Model │ │ <sources> │ │ <source><lang>java</lang><module>X</module></source> │ │ <source><lang>resources</lang><module>X</module></source> │ │ </sources> │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ DefaultProjectBuilder │ │ Creates SourceRoot objects from <source> elements │ │ Adds to project.addSourceRoot(...) │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ MavenProject │ │ getEnabledSourceRoots(ProjectScope scope, Language language) │ │ - Plugins query this to find relevant sources │ └─────────────────────────────────────────────────────────────────┘ │ ┌───────────────┴───────────────┐ ▼ ▼ ┌─────────────────────────┐ ┌─────────────────────────┐ │ Compiler Plugin │ │ Resources Plugin │ │ Queries: lang=java │ │ Queries: lang=resources │ └─────────────────────────┘ └─────────────────────────┘ Detecting Explicit vs Implicit Configuration To detect whether <sourceDirectory> was explicitly configured (vs inherited from Super POM): // Option 1: Compare to known Super POM default String superPomDefault = "${project.basedir}/src/main/java"; String resolvedDefault = baseDir.resolve("src/main/java").toString(); boolean isExplicit = !build.getSourceDirectory().equals(resolvedDefault); // Option 2: Use InputLocation tracking InputLocation location = build.getLocation("sourceDirectory"); boolean isExplicit = location != null && !isSuperPomLocation(location); Where to Implement Validation Option Location Pros Cons ModelValidator (Recommended) validateEffectiveModel() Line numbers, early, standard Values not fully resolved DefaultProjectBuilder initProject() All values resolved Later, less standard Lifecycle phase validate - Too late Related Code Locations impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelValidator.java impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultSourceRoot.java impl/maven-impl/src/main/resources/org/apache/maven/model/pom-4.0.0.xml (Super POM 4.0) impl/maven-impl/src/main/resources/org/apache/maven/model/pom-4.1.0.xml (Super POM 4.1) Open Questions Should we validate at validateRawModel() or validateEffectiveModel()? How to reliably detect "explicit" configuration (InputLocation vs value comparison)? Should the filesystem warning (R4) be optional/configurable?
