Skip to content

Commit ee261f7

Browse files
committed
Move some of the codes needed by ProjectSourcesHelper in an utility class that we can reuse in other packages.
1 parent 71bd10f commit ee261f7

File tree

3 files changed

+155
-83
lines changed

3 files changed

+155
-83
lines changed

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

Lines changed: 4 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -655,7 +655,6 @@ private void initProject(MavenProject project, ModelBuilderResult result) {
655655
// only set those on 2nd phase, ignore on 1st pass
656656
if (project.getFile() != null) {
657657
Build build = project.getBuild().getDelegate();
658-
List<org.apache.maven.api.model.Source> sources = build.getSources();
659658
Path baseDir = project.getBaseDirectory();
660659
Function<ProjectScope, String> outputDirectory = (scope) -> {
661660
if (scope == ProjectScope.MAIN) {
@@ -666,23 +665,11 @@ private void initProject(MavenProject project, ModelBuilderResult result) {
666665
return build.getDirectory();
667666
}
668667
};
669-
// Extract modules from sources to detect modular projects
670-
Set<String> modules = extractModules(sources);
671-
boolean isModularProject = !modules.isEmpty();
672-
673-
logger.trace(
674-
"Module detection for project {}: found {} module(s) {} - modular project: {}.",
675-
project.getId(),
676-
modules.size(),
677-
modules,
678-
isModularProject);
679-
680668
// Create source handling context for unified tracking of all lang/scope combinations
681-
SourceHandlingContext sourceContext =
682-
new SourceHandlingContext(project, baseDir, modules, isModularProject, result);
669+
final SourceHandlingContext sourceContext = new SourceHandlingContext(project, result);
683670

684671
// Process all sources, tracking enabled ones and detecting duplicates
685-
for (var source : sources) {
672+
for (org.apache.maven.api.model.Source source : sourceContext.sources) {
686673
var sourceRoot = DefaultSourceRoot.fromModel(session, baseDir, outputDirectory, source);
687674
// Track enabled sources for duplicate detection and hasSources() queries
688675
// Only add source if it's not a duplicate enabled source (first enabled wins)
@@ -711,7 +698,7 @@ private void initProject(MavenProject project, ModelBuilderResult result) {
711698
implicit fallback (only if they match the default, e.g., inherited)
712699
- This allows incremental adoption (e.g., custom resources + default Java)
713700
*/
714-
if (sources.isEmpty()) {
701+
if (sourceContext.sources.isEmpty()) {
715702
// Classic fallback: no <sources> configured, use legacy directories
716703
project.addScriptSourceRoot(build.getScriptSourceDirectory());
717704
project.addCompileSourceRoot(build.getSourceDirectory());
@@ -724,8 +711,7 @@ implicit fallback (only if they match the default, e.g., inherited)
724711
if (!sourceContext.hasSources(Language.SCRIPT, ProjectScope.MAIN)) {
725712
project.addScriptSourceRoot(build.getScriptSourceDirectory());
726713
}
727-
728-
if (isModularProject) {
714+
if (sourceContext.usesModuleSourceHierarchy()) {
729715
// Modular: reject ALL legacy directory configurations
730716
failIfLegacyDirectoryPresent(
731717
build.getSourceDirectory(),
@@ -1243,22 +1229,6 @@ public Set<Entry<K, V>> entrySet() {
12431229
}
12441230
}
12451231

1246-
/**
1247-
* Extracts unique module names from the given list of source elements.
1248-
* A project is considered modular if it has at least one module name.
1249-
*
1250-
* @param sources list of source elements from the build
1251-
* @return set of non-blank module names
1252-
*/
1253-
private static Set<String> extractModules(List<org.apache.maven.api.model.Source> sources) {
1254-
return sources.stream()
1255-
.map(org.apache.maven.api.model.Source::getModule)
1256-
.filter(Objects::nonNull)
1257-
.map(String::trim)
1258-
.filter(s -> !s.isBlank())
1259-
.collect(Collectors.toSet());
1260-
}
1261-
12621232
private Model injectLifecycleBindings(
12631233
Model model,
12641234
ModelBuilderRequest request,

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

Lines changed: 67 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.apache.maven.api.ProjectScope;
2828
import org.apache.maven.api.SourceRoot;
2929
import org.apache.maven.api.model.Resource;
30+
import org.apache.maven.api.model.Source;
3031
import org.apache.maven.api.services.BuilderProblem.Severity;
3132
import org.apache.maven.api.services.ModelBuilderResult;
3233
import org.apache.maven.api.services.ModelProblem.Version;
@@ -37,9 +38,7 @@
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>
@@ -51,7 +50,7 @@
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) {
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.maven.project;
20+
21+
import java.util.Collection;
22+
import java.util.LinkedHashSet;
23+
import java.util.Objects;
24+
import java.util.Set;
25+
26+
import org.apache.maven.api.model.Source;
27+
28+
/**
29+
* Static utility methods for analyzing {@code <source>} elements of a project.
30+
* <p>
31+
* <strong>Warning:</strong> This is an internal utility class, not part of the public API.
32+
* It can be changed or removed without prior notice.
33+
*
34+
* @since 4.0.0
35+
*/
36+
public final class SourceQueries {
37+
private SourceQueries() {}
38+
39+
/**
40+
* Returns whether at least one source in the collection has a non-blank module name,
41+
* indicating a modular source hierarchy.
42+
*
43+
* @param sources the source elements to check
44+
* @return {@code true} if at least one source declares a module
45+
*/
46+
public static boolean usesModuleSourceHierarchy(Collection<Source> sources) {
47+
return sources.stream().map(Source::getModule).filter(Objects::nonNull).anyMatch(s -> !s.isBlank());
48+
}
49+
50+
/**
51+
* Returns whether at least one source in the collection is enabled.
52+
*
53+
* @param sources the source elements to check
54+
* @return {@code true} if at least one source is enabled
55+
*/
56+
public static boolean hasEnabledSources(Collection<Source> sources) {
57+
for (Source source : sources) {
58+
if (source.isEnabled()) {
59+
return true;
60+
}
61+
}
62+
return false;
63+
}
64+
65+
/**
66+
* Extracts unique, non-blank module names from the source elements, preserving declaration order.
67+
* The following relationship should always be true:
68+
*
69+
* <pre>getModuleNames(sources).isEmpty() == !usesModuleSourceHierarchy(sources)</pre>
70+
*
71+
* @param sources the source elements to extract module names from
72+
* @return set of non-blank module names in declaration order
73+
*/
74+
public static Set<String> getModuleNames(Collection<Source> sources) {
75+
var modules = new LinkedHashSet<String>();
76+
sources.stream()
77+
.map(Source::getModule)
78+
.filter(Objects::nonNull)
79+
.map(String::strip)
80+
.filter(s -> !s.isEmpty())
81+
.forEach(modules::add);
82+
return modules;
83+
}
84+
}

0 commit comments

Comments
 (0)