2727import org .apache .maven .api .ProjectScope ;
2828import org .apache .maven .api .SourceRoot ;
2929import org .apache .maven .api .model .Resource ;
30+ import org .apache .maven .api .model .Source ;
3031import org .apache .maven .api .services .BuilderProblem .Severity ;
3132import org .apache .maven .api .services .ModelBuilderResult ;
3233import org .apache .maven .api .services .ModelProblem .Version ;
3738
3839/**
3940 * Handles source configuration for Maven projects with unified tracking for all language/scope combinations.
40- * <p>
41- * This class replaces the previous approach of hardcoded boolean flags (hasMain, hasTest, etc.)
42- * with a flexible set-based tracking mechanism that works for any language and scope combination.
41+ * This class uses a flexible set-based tracking mechanism that works for any language and scope combination.
4342 * <p>
4443 * Key features:
4544 * <ul>
5150 *
5251 * @since 4.0.0
5352 */
54- class SourceHandlingContext {
53+ final class SourceHandlingContext {
5554
5655 private static final Logger LOGGER = LoggerFactory .getLogger (SourceHandlingContext .class );
5756
@@ -60,26 +59,38 @@ class SourceHandlingContext {
6059 */
6160 record SourceKey (Language language , ProjectScope scope , String module , Path directory ) {}
6261
62+ /**
63+ * The {@code <source>} elements declared in the {@code <build>} elements.
64+ */
65+ final List <Source > sources ;
66+
6367 private final MavenProject project ;
64- private final Path baseDir ;
6568 private final Set <String > modules ;
66- private final boolean modularProject ;
6769 private final ModelBuilderResult result ;
6870 private final Set <SourceKey > declaredSources ;
6971
70- SourceHandlingContext (
71- MavenProject project ,
72- Path baseDir ,
73- Set <String > modules ,
74- boolean modularProject ,
75- ModelBuilderResult result ) {
72+ SourceHandlingContext (MavenProject project , ModelBuilderResult result ) {
7673 this .project = project ;
77- this .baseDir = baseDir ;
78- this .modules = modules ;
79- this .modularProject = modularProject ;
74+ this .sources = project .getBuild ().getDelegate ().getSources ();
75+ this .modules = SourceQueries .getModuleNames (sources );
8076 this .result = result ;
8177 // Each module typically has main, test, main resources, test resources = 4 sources
8278 this .declaredSources = new HashSet <>(4 * modules .size ());
79+ if (usesModuleSourceHierarchy ()) {
80+ LOGGER .trace ("Found {} module(s) in the \" {}\" project: {}." , project .getId (), modules .size (), modules );
81+ } else {
82+ LOGGER .trace ("Project \" {}\" is non-modular." , project .getId ());
83+ }
84+ }
85+
86+ /**
87+ * Whether the project uses module source hierarchy.
88+ * Note that this is not synonymous of whether the project is modular,
89+ * because it is possible to create a single Java module in a classic Maven project
90+ * (i.e., using package hierarchy).
91+ */
92+ boolean usesModuleSourceHierarchy () {
93+ return !modules .isEmpty ();
8394 }
8495
8596 /**
@@ -112,7 +123,7 @@ boolean shouldAddSource(SourceRoot sourceRoot) {
112123 SourceKey key = new SourceKey (
113124 sourceRoot .language (), sourceRoot .scope (), sourceRoot .module ().orElse (null ), normalizedDir );
114125
115- if (declaredSources .contains (key )) {
126+ if (! declaredSources .add (key )) {
116127 String message = String .format (
117128 "Duplicate enabled source detected: lang=%s, scope=%s, module=%s, directory=%s. "
118129 + "First enabled source wins, this duplicate is ignored." ,
@@ -130,7 +141,6 @@ boolean shouldAddSource(SourceRoot sourceRoot) {
130141 return false ; // Don't add duplicate enabled source
131142 }
132143
133- declaredSources .add (key );
134144 LOGGER .debug (
135145 "Adding and tracking enabled source: lang={}, scope={}, module={}, dir={}" ,
136146 key .language (),
@@ -151,6 +161,13 @@ boolean hasSources(Language language, ProjectScope scope) {
151161 return declaredSources .stream ().anyMatch (key -> language .equals (key .language ()) && scope .equals (key .scope ()));
152162 }
153163
164+ /**
165+ * {@return the source directory as defined by Maven conventions}
166+ */
167+ private Path getStandardSourceDirectory () {
168+ return project .getBaseDirectory ().resolve ("src" );
169+ }
170+
154171 /**
155172 * Fails the build if modular and classic (non-modular) sources are mixed within {@code <sources>}.
156173 * <p>
@@ -164,30 +181,32 @@ boolean hasSources(Language language, ProjectScope scope) {
164181 void failIfMixedModularAndClassicSources () {
165182 for (ProjectScope scope : List .of (ProjectScope .MAIN , ProjectScope .TEST )) {
166183 for (Language language : List .of (Language .JAVA_FAMILY , Language .RESOURCES )) {
167- boolean hasModular = declaredSources .stream ()
168- .anyMatch (key ->
169- language .equals (key .language ()) && scope .equals (key .scope ()) && key .module () != null );
170- boolean hasClassic = declaredSources .stream ()
171- .anyMatch (key ->
172- language .equals (key .language ()) && scope .equals (key .scope ()) && key .module () == null );
173-
174- if (hasModular && hasClassic ) {
175- String message = String .format (
176- "Mixed modular and classic sources detected for lang=%s, scope=%s. "
177- + "A project must be either fully modular (all sources have a module) "
178- + "or fully classic (no sources have a module). "
179- + "The compiler plugin cannot handle mixed configurations." ,
180- language .id (), scope .id ());
181- LOGGER .error (message );
182- result .getProblemCollector ()
183- .reportProblem (new DefaultModelProblem (
184- message ,
185- Severity .ERROR ,
186- Version .V41 ,
187- project .getModel ().getDelegate (),
188- -1 ,
189- -1 ,
190- null ));
184+ boolean hasModular = false ;
185+ boolean hasClassic = false ;
186+ for (SourceKey key : declaredSources ) {
187+ if (language .equals (key .language ()) && scope .equals (key .scope ())) {
188+ String module = key .module ();
189+ hasModular |= (module != null );
190+ hasClassic |= (module == null );
191+ if (hasModular && hasClassic ) {
192+ String message = String .format (
193+ "Mixed modular and classic sources detected for lang=%s, scope=%s. "
194+ + "A project must be either fully modular (all sources have a module) "
195+ + "or fully classic (no sources have a module)." ,
196+ language .id (), scope .id ());
197+ LOGGER .error (message );
198+ result .getProblemCollector ()
199+ .reportProblem (new DefaultModelProblem (
200+ message ,
201+ Severity .ERROR ,
202+ Version .V41 ,
203+ project .getModel ().getDelegate (),
204+ -1 ,
205+ -1 ,
206+ null ));
207+ break ;
208+ }
209+ }
191210 }
192211 }
193212 }
@@ -219,7 +238,7 @@ void handleResourceConfiguration(ProjectScope scope) {
219238 ? "<source><lang>resources</lang></source>"
220239 : "<source><lang>resources</lang><scope>test</scope></source>" ;
221240
222- if (modularProject ) {
241+ if (usesModuleSourceHierarchy () ) {
223242 if (hasResourcesInSources ) {
224243 // Modular project with resources configured via <sources> - already added above
225244 if (hasExplicitLegacyResources (resources , scopeId )) {
@@ -298,6 +317,7 @@ void handleResourceConfiguration(ProjectScope scope) {
298317 // Use legacy resources element
299318 LOGGER .debug (
300319 "Using explicit or default {} resources ({} resources configured)." , scopeId , resources .size ());
320+ Path baseDir = project .getBaseDirectory ();
301321 for (Resource resource : resources ) {
302322 project .addSourceRoot (new DefaultSourceRoot (baseDir , scope , resource ));
303323 }
@@ -315,7 +335,7 @@ void handleResourceConfiguration(ProjectScope scope) {
315335 */
316336 private DefaultSourceRoot createModularResourceRoot (String module , ProjectScope scope ) {
317337 Path resourceDir =
318- baseDir . resolve ( "src" ).resolve (module ).resolve (scope .id ()).resolve ("resources" );
338+ getStandardSourceDirectory ( ).resolve (module ).resolve (scope .id ()).resolve ("resources" );
319339
320340 return new DefaultSourceRoot (
321341 scope ,
@@ -345,12 +365,10 @@ private boolean hasExplicitLegacyResources(List<Resource> resources, String scop
345365 }
346366
347367 // Super POM default paths
348- String defaultPath =
349- baseDir .resolve ("src" ).resolve (scope ).resolve ("resources" ).toString ();
350- String defaultFilteredPath = baseDir .resolve ("src" )
351- .resolve (scope )
352- .resolve ("resources-filtered" )
353- .toString ();
368+ Path srcDir = getStandardSourceDirectory ();
369+ String defaultPath = srcDir .resolve (scope ).resolve ("resources" ).toString ();
370+ String defaultFilteredPath =
371+ srcDir .resolve (scope ).resolve ("resources-filtered" ).toString ();
354372
355373 // Check if any resource differs from Super POM defaults
356374 for (Resource resource : resources ) {
0 commit comments