diff --git a/components/sbm-core/src/main/java/org/springframework/sbm/build/api/ApplicationModules.java b/components/sbm-core/src/main/java/org/springframework/sbm/build/api/ApplicationModules.java index 640e0acd4..6c8c5b9e0 100644 --- a/components/sbm-core/src/main/java/org/springframework/sbm/build/api/ApplicationModules.java +++ b/components/sbm-core/src/main/java/org/springframework/sbm/build/api/ApplicationModules.java @@ -21,10 +21,7 @@ import org.springframework.sbm.build.impl.OpenRewriteMavenBuildFile; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -43,10 +40,21 @@ public Stream stream() { } public Module getRootModule() { + // 1st try: module explicitly marked as having a root build file return modules.stream() .filter(m -> m.getBuildFile().isRootBuildFile()) .findFirst() - .orElseThrow(() -> new RootBuildFileNotFoundException("Module with root build file is missing")); + // 2nd try (fallback): no module marked as root → choose the one + // whose build file path is closest to the project root + .orElseGet(() -> modules.stream() + .min(Comparator.comparingInt(m -> pathDepth(m.getBuildFile()))) + .orElseThrow(() -> new RootBuildFileNotFoundException("Module with root build file is missing"))); + } + + private int pathDepth(BuildFile buildFile) { + Path path = buildFile.getSourcePath(); + // if for some reason there is no path, treat it as "very deep" + return (path == null) ? Integer.MAX_VALUE : path.getNameCount(); } public List list() { diff --git a/components/sbm-core/src/main/java/org/springframework/sbm/build/api/RootBuildFileFilter.java b/components/sbm-core/src/main/java/org/springframework/sbm/build/api/RootBuildFileFilter.java index 3cf9e2ea0..3d7542ba6 100644 --- a/components/sbm-core/src/main/java/org/springframework/sbm/build/api/RootBuildFileFilter.java +++ b/components/sbm-core/src/main/java/org/springframework/sbm/build/api/RootBuildFileFilter.java @@ -15,17 +15,42 @@ */ package org.springframework.sbm.build.api; +import org.jetbrains.annotations.NotNull; import org.springframework.sbm.project.resource.ProjectResourceSet; import org.springframework.sbm.project.resource.filter.ProjectResourceFinder; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + public class RootBuildFileFilter implements ProjectResourceFinder { + @Override - public BuildFile apply(ProjectResourceSet projectResourceSet) { - return projectResourceSet.stream() + public BuildFile apply(@NotNull ProjectResourceSet projectResourceSet) { + // collect all build files (pom.xml, etc.) + List buildFiles = projectResourceSet.stream() .filter(pr -> BuildFile.class.isAssignableFrom(pr.getClass())) .map(BuildFile.class::cast) - .filter(bf -> bf.isRootBuildFile()) + .collect(Collectors.toList()); + + if (buildFiles.isEmpty()) { + throw new RootBuildFileNotFoundException("Could not find any BuildFile in project."); + } + + // 1st try: existing logic – respect explicit isRootBuildFile flag + return buildFiles.stream() + .filter(BuildFile::isRootBuildFile) .findFirst() - .orElseThrow(() -> new RootBuildFileNotFoundException("Could not find BuildFile for root module.")); + // 2nd try (fallback): no explicit root → choose the build file + // whose source path is closest to the project root (smallest depth) + .orElseGet(() -> buildFiles.stream() + .min(Comparator.comparingInt(bf -> pathDepth(bf.getSourcePath()))) + .orElseThrow(() -> new RootBuildFileNotFoundException("Could not find BuildFile for root module."))); + } + + private int pathDepth(Path path) { + // defensive: null check, though OpenRewrite usually always has a path + return (path == null) ? Integer.MAX_VALUE : path.getNameCount(); } -} +} \ No newline at end of file diff --git a/components/sbm-core/src/test/java/org/springframework/sbm/build/api/RootBuildFileFilterTest.java b/components/sbm-core/src/test/java/org/springframework/sbm/build/api/RootBuildFileFilterTest.java new file mode 100644 index 000000000..8307c7f91 --- /dev/null +++ b/components/sbm-core/src/test/java/org/springframework/sbm/build/api/RootBuildFileFilterTest.java @@ -0,0 +1,412 @@ +/* + * Copyright 2021 - 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.sbm.build.api; + +import org.junit.jupiter.api.Test; +import org.openrewrite.*; +import org.openrewrite.internal.lang.Nullable; +import org.openrewrite.maven.tree.Scope; +import org.springframework.sbm.project.resource.ProjectResourceSet; +import org.springframework.sbm.project.resource.RewriteSourceFileHolder; +import org.openrewrite.marker.Markers; + +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; + +import static org.assertj.core.api.Assertions.assertThat; + +class RootBuildFileFilterTest { + + /** + * Fake SourceFile that allows us to control the sourcePath. + */ + static class DummySourceFile implements SourceFile { + private final Path sourcePath; + + DummySourceFile(Path sourcePath) { + this.sourcePath = sourcePath; + } + + @Override + public Path getSourcePath() { + return sourcePath; + } + + @Override + public T withSourcePath(Path path) { + return null; + } + + @Override + public @Nullable Charset getCharset() { + return null; + } + + @Override + public T withCharset(Charset charset) { + return null; + } + + @Override + public boolean isCharsetBomMarked() { + return false; + } + + @Override + public T withCharsetBomMarked(boolean marked) { + return null; + } + + @Override + public @Nullable Checksum getChecksum() { + return null; + } + + @Override + public T withChecksum(@Nullable Checksum checksum) { + return null; + } + + @Override + public @Nullable FileAttributes getFileAttributes() { + return null; + } + + @Override + public T withFileAttributes(@Nullable FileAttributes fileAttributes) { + return null; + } + + @Override + public UUID getId() { + return null; + } + + @Override + public Markers getMarkers() { + return Markers.EMPTY; + } + + @Override + public T withId(UUID id) { + return null; + } + + @Override + public

boolean isAcceptable(TreeVisitor v, P p) { + return false; + } + + @Override + public SourceFile withMarkers(Markers markers) { + return this; + } + } + + /** + * Dummy BuildFile implementation based on RewriteSourceFileHolder. + */ + static class DummyBuildFile extends RewriteSourceFileHolder implements BuildFile { + + DummyBuildFile(Path projectDir, Path sourcePath) { + super(projectDir, new DummySourceFile(sourcePath)); + } + + @Override + public List getDeclaredDependencies(Scope... scopes) { + return List.of(); + } + + @Override + public List getRequestedDependencies() { + return List.of(); + } + + @Override + public Set getEffectiveDependencies(Scope scope) { + return Set.of(); + } + + @Override + public Set getEffectiveDependencies() { + return Set.of(); + } + + @Override + public boolean hasDeclaredDependencyMatchingRegex(String... dependencyPatterns) { + return false; + } + + @Override + public boolean hasEffectiveDependencyMatchingRegex(String... dependencyPatterns) { + return false; + } + + @Override + public boolean hasExactDeclaredDependency(Dependency dependency) { + return false; + } + + @Override + public void addDependency(Dependency dependency) { + + } + + @Override + public void addDependencies(List dependencies) { + + } + + @Override + public void removeDependencies(List dependencies) { + + } + + @Override + public void removeDependenciesMatchingRegex(String... regex) { + + } + + @Override + public void removeDependenciesInner(List dependencies) { + + } + + @Override + public List getEffectiveDependencyManagement() { + return List.of(); + } + + @Override + public List getRequestedDependencyManagement() { + return List.of(); + } + + @Override + public List getRequestedManagedDependencies() { + return List.of(); + } + + @Override + public void addToDependencyManagement(Dependency dependency) { + + } + + @Override + public void addToDependencyManagementInner(Dependency dependency) { + + } + + @Override + public List getResolvedDependenciesPaths() { + return List.of(); + } + + @Override + public boolean hasPlugin(Plugin plugin) { + return false; + } + + @Override + public void addPlugin(Plugin plugin) { + + } + + @Override + public List getClasspath() { + return List.of(); + } + + @Override + public List getSourceFolders() { + return List.of(); + } + + @Override + public List getTestSourceFolders() { + return List.of(); + } + + @Override + public List getResourceFolders() { + return List.of(); + } + + @Override + public List getTestResourceFolders() { + return List.of(); + } + + @Override + public Path getTestResourceFolder() { + return null; + } + + @Override + public Path getMainResourceFolder() { + return null; + } + + @Override + public void setProperty(String key, String value) { + + } + + @Override + public String getProperty(String key) { + return ""; + } + + @Override + public void deleteProperty(String key) { + + } + + @Override + public String getPackaging() { + return ""; + } + + @Override + public void setPackaging(String packaging) { + + } + + @Override + public boolean isRootBuildFile() { + // for this test we force it to return false + // so that RootBuildFileFilter must use the fallback logic. + return false; + } + + @Override + public List getPlugins() { + return List.of(); + } + + @Override + public void removePluginsMatchingRegex(String... regex) { + + } + + @Override + public void removePlugins(String... coordinates) { + + } + + @Override + public String getGroupId() { + return ""; + } + + @Override + public String getArtifactId() { + return ""; + } + + @Override + public String getVersion() { + return ""; + } + + @Override + public String getCoordinates() { + return ""; + } + + @Override + public boolean hasParent() { + return false; + } + + @Override + public void upgradeParentVersion(String version) { + + } + + @Override + public Optional getParentPomDeclaration() { + return Optional.empty(); + } + + @Override + public Optional getName() { + return Optional.empty(); + } + + @Override + public void excludeDependencies(List excludedDependencies) { + + } + + @Override + public void addRepository(RepositoryDefinition repository) { + + } + + @Override + public void addPluginRepository(RepositoryDefinition repository) { + + } + + @Override + public List getRepositories() { + return List.of(); + } + + @Override + public List getPluginRepositories() { + return List.of(); + } + + @Override + public List getDeclaredModules() { + return List.of(); + } + + @Override + public Optional findPlugin(String groupId, String artifactId) { + return Optional.empty(); + } + + // other BuildFile methods can be left unimplemented for now, + // or given simple no-op bodies if the interface requires more. + } + + @Test + void choosesBuildFileWithShortestPathAsRootWhenNoneMarkedAsRoot() { + Path projectDir = Paths.get("C:\\fake-project").toAbsolutePath(); + + // Simulate two pom.xml files: + // - "parent/pom.xml" (depth 2) -> this should be chosen as root + // - "parent/module/pom.xml" (depth 3) + DummyBuildFile parentPom = new DummyBuildFile(projectDir, Path.of("parent/pom.xml")); + DummyBuildFile modulePom = new DummyBuildFile(projectDir, Path.of("parent/module/pom.xml")); + + ProjectResourceSet prs = new ProjectResourceSet(); + prs.add(parentPom); + prs.add(modulePom); + + RootBuildFileFilter filter = new RootBuildFileFilter(); + + BuildFile root = filter.apply(prs); + + assertThat(root.getSourcePath()) + .isEqualTo(Path.of("parent", "pom.xml")); + } +}