diff --git a/spring-rewrite-commons-functional-tests/dependency-resolution-tests/src/test/java/org/springframework/rewrite/MavenTypeResolutionTest.java b/spring-rewrite-commons-functional-tests/dependency-resolution-tests/src/test/java/org/springframework/rewrite/MavenTypeResolutionTest.java deleted file mode 100644 index 562577c..0000000 --- a/spring-rewrite-commons-functional-tests/dependency-resolution-tests/src/test/java/org/springframework/rewrite/MavenTypeResolutionTest.java +++ /dev/null @@ -1,198 +0,0 @@ -/* - * 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.rewrite; - -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.*; -import org.junit.jupiter.api.io.TempDir; -import org.openrewrite.ExecutionContext; -import org.openrewrite.RecipeRun; -import org.openrewrite.SourceFile; -import org.openrewrite.internal.InMemoryLargeSourceSet; -import org.openrewrite.java.JavaIsoVisitor; -import org.openrewrite.java.JavaParser; -import org.openrewrite.java.JavaTemplate; -import org.openrewrite.java.marker.JavaSourceSet; -import org.openrewrite.java.tree.J; -import org.openrewrite.java.tree.JavaType; -import org.springframework.rewrite.parsers.RewriteExecutionContext; -import org.springframework.rewrite.parsers.RewriteProjectParsingResult; -import org.springframework.rewrite.parsers.SpringRewriteProperties; -import org.springframework.rewrite.parsers.maven.ClasspathDependencies; -import org.springframework.rewrite.support.openrewrite.GenericOpenRewriteRecipe; -import org.springframework.rewrite.test.util.ParserExecutionHelper; -import org.springframework.rewrite.test.util.TestProjectHelper; - -import java.nio.file.Path; -import java.util.Comparator; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; - -public class MavenTypeResolutionTest { - - @Nested - @TestMethodOrder(MethodOrderer.OrderAnnotation.class) - class WithSingleModuleMavenProject { - - private static RewriteProjectParsingResult parsingResult; - - @Test - @Order(1) - @DisplayName("parsed project should have correct classpath") - void parsedProjectShouldHaveCorrectClasspath(@TempDir Path baseDir) { - // given - pepareProject(baseDir); - // when - parsingResult = new ParserExecutionHelper().parseWithRewriteProjectParser(baseDir, - new SpringRewriteProperties()); - // then - verifyTypesOnClasspathAndNoTypesInUse(parsingResult); - } - - @Test - @Order(2) - @DisplayName("annotating the class adds the annotation to typesInUse") - void annotatingTheClassAddsTheAnnotationToTypesInUse() { - // Given: parsed project - List sourceFiles = parsingResult.sourceFiles(); - assertThat(sourceFiles).isNotEmpty(); - // When: Add @SpringBootApplication annotation to class - RecipeRun recipeRun = annotateClass(sourceFiles); - // Then: annotation is in typesInUse - verifyAnnotationIsNowInUsedTypes(recipeRun); - } - - private static void verifyAnnotationIsNowInUsedTypes(RecipeRun recipeRun) { - SourceFile after = recipeRun.getChangeset().getAllResults().get(0).getAfter(); - assertThat(after).isInstanceOf(J.CompilationUnit.class); - J.CompilationUnit cu = (J.CompilationUnit) after; - List classpath = cu.getMarkers() - .findFirst(JavaSourceSet.class) - .get() - .getClasspath() - .stream() - .map(JavaType.FullyQualified::getFullyQualifiedName) - .toList(); - assertThat(classpath).contains("org.springframework.boot.autoconfigure.SpringBootApplication", "SomeClass"); - - List typesInUse = cu.getTypesInUse() - .getTypesInUse() - .stream() - .map(JavaType.FullyQualified.class::cast) - .map(JavaType.FullyQualified::getFullyQualifiedName) - .toList(); - assertThat(typesInUse) - .containsExactlyInAnyOrder("org.springframework.boot.autoconfigure.SpringBootApplication"); - } - - @NotNull - private static RecipeRun annotateClass(List sourceFiles) { - RecipeRun recipeRun = new GenericOpenRewriteRecipe<>(() -> new JavaIsoVisitor<>() { - @Override - public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, - ExecutionContext executionContext) { - J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, executionContext); - if (cd.getSimpleName().equals("SomeClass")) { - ClasspathDependencies classpathDependencies = ((J.CompilationUnit) getCursor() - .dropParentUntil(J.CompilationUnit.class::isInstance) - .getValue()).getMarkers().findFirst(ClasspathDependencies.class).get(); - String annotationFqName = "org.springframework.boot.autoconfigure.SpringBootApplication"; - cd = JavaTemplate.builder("@SpringBootApplication") - .imports(annotationFqName) - .javaParser(JavaParser.fromJavaVersion() - .classpath(classpathDependencies.getDependencies()) - .logCompilationWarningsAndErrors(true)) - .build() - .apply(getCursor(), cd.getCoordinates() - .addAnnotation(Comparator.comparing(J.Annotation::getSimpleName))); - maybeAddImport(annotationFqName); - } - return cd; - } - }).run(new InMemoryLargeSourceSet(sourceFiles), new RewriteExecutionContext()); - return recipeRun; - } - - private static void verifyTypesOnClasspathAndNoTypesInUse(RewriteProjectParsingResult parallelParsingResult) { - J.CompilationUnit cuBefore = parallelParsingResult.sourceFiles() - .stream() - .filter(J.CompilationUnit.class::isInstance) - .map(J.CompilationUnit.class::cast) - .findFirst() - .get(); - List typesInUseBefore = cuBefore.getTypesInUse() - .getTypesInUse() - .stream() - .map(JavaType.FullyQualified.class::cast) - .map(JavaType.FullyQualified::getFullyQualifiedName) - .toList(); - assertThat(typesInUseBefore).isEmpty(); - List classpathBefore = cuBefore.getMarkers() - .findFirst(JavaSourceSet.class) - .get() - .getClasspath() - .stream() - .map(JavaType.FullyQualified::getFullyQualifiedName) - .toList(); - assertThat(classpathBefore).contains("org.springframework.boot.autoconfigure.SpringBootApplication", - "SomeClass"); - } - - private static void pepareProject(Path baseDir) { - TestProjectHelper.createTestProject(baseDir) - .addResource("pom.xml", - """ - - - 4.0.0 - org.example - artifact - 0.1.0-SNAPSHOT - - 17 - 17 - - - - org.springframework.boot - spring-boot-autoconfigure - 3.1.3 - - - - """) - - .addResource("src/main/java/SomeClass.java", - // @formatter:off - """ - public class SomeClass {} - """ - // @formatter:on - ) - .writeToFilesystem(); - } - - } - - @TestMethodOrder(MethodOrderer.OrderAnnotation.class) - class WithMultiModuleMavenProject { - - } - -} diff --git a/spring-rewrite-commons-functional-tests/dependency-resolution-tests/src/test/java/org/springframework/rewrite/MavenTypeResolutionTests.java b/spring-rewrite-commons-functional-tests/dependency-resolution-tests/src/test/java/org/springframework/rewrite/MavenTypeResolutionTests.java new file mode 100644 index 0000000..9b8567c --- /dev/null +++ b/spring-rewrite-commons-functional-tests/dependency-resolution-tests/src/test/java/org/springframework/rewrite/MavenTypeResolutionTests.java @@ -0,0 +1,514 @@ +/* + * 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.rewrite; + +import org.intellij.lang.annotations.Language; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.io.TempDir; +import org.openrewrite.ExecutionContext; +import org.openrewrite.RecipeRun; +import org.openrewrite.SourceFile; +import org.openrewrite.internal.InMemoryLargeSourceSet; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.JavaParser; +import org.openrewrite.java.JavaTemplate; +import org.openrewrite.java.marker.JavaSourceSet; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaType; +import org.openrewrite.java.tree.Statement; +import org.springframework.rewrite.parsers.RewriteExecutionContext; +import org.springframework.rewrite.parsers.RewriteProjectParsingResult; +import org.springframework.rewrite.parsers.SpringRewriteProperties; +import org.springframework.rewrite.parsers.maven.ClasspathDependencies; +import org.springframework.rewrite.support.openrewrite.GenericOpenRewriteRecipe; +import org.springframework.rewrite.test.util.ParserExecutionHelper; +import org.springframework.rewrite.test.util.TestProjectHelper; + +import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class MavenTypeResolutionTests { + + @Nested + class ClassesFromMainCanBeResolvedInTest { + + @Test + @DisplayName("TestClass should have MainClass in used types") + void testClassShouldHaveMainClassInUsedTypes(@TempDir Path baseDir) { + + TestProjectHelper.createTestProject(baseDir) + .addResource("pom.xml", + """ + + + 4.0.0 + org.example + artifact + 0.1.0-SNAPSHOT + + 17 + 17 + + + + org.springframework.boot + spring-boot-autoconfigure + 3.1.3 + + + + """) + + .addResource("src/main/java/main/SomeClass.java", """ + package main; + public class SomeClass {} + """) + .addResource("src/test/java/test/SomeTest.java", """ + package test; + import main.SomeClass; + public class SomeTest { + private SomeClass someClass; + } + """) + .writeToFilesystem(); + + RewriteProjectParsingResult parsingResult = new ParserExecutionHelper() + .parseWithRewriteProjectParser(baseDir, new SpringRewriteProperties()); + + SourceFile sourceFile = parsingResult.sourceFiles() + .stream() + .filter(f -> f.getSourcePath().toString().endsWith("SomeTest.java")) + .findFirst() + .get(); + assertThat(sourceFile).isNotNull(); + J.CompilationUnit cu = (J.CompilationUnit) sourceFile; + cu.getClasses().get(0).getBody().getStatements().get(0); + assertThat( + ((JavaType.Class) ((J.VariableDeclarations) cu.getClasses().get(0).getBody().getStatements().get(0)) + .getTypeExpression() + .getType()).getFullyQualifiedName()) + .isEqualTo("main.SomeClass"); + } + + } + + @Nested + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + class WithSingleModuleMavenProject { + + private static RewriteProjectParsingResult parsingResult; + + @Test + @Order(1) + @DisplayName("parsed project should have correct classpath") + void parsedProjectShouldHaveCorrectClasspath(@TempDir Path baseDir) { + // given + pepareProject(baseDir); + // when + parsingResult = new ParserExecutionHelper().parseWithRewriteProjectParser(baseDir, + new SpringRewriteProperties()); + // then + verifyTypesOnClasspathAndNoTypesInUse(parsingResult); + } + + @Test + @Order(2) + @DisplayName("annotating the class adds the annotation to typesInUse") + void annotatingTheClassAddsTheAnnotationToTypesInUse() { + // Given: parsed project + List sourceFiles = parsingResult.sourceFiles(); + assertThat(sourceFiles).isNotEmpty(); + // When: Add @SpringBootApplication annotation to class + RecipeRun recipeRun = annotateClass(sourceFiles); + // Then: annotation is in typesInUse + verifyAnnotationIsNowInUsedTypes(recipeRun); + } + + private static void verifyAnnotationIsNowInUsedTypes(RecipeRun recipeRun) { + SourceFile after = recipeRun.getChangeset().getAllResults().get(0).getAfter(); + assertThat(after).isInstanceOf(J.CompilationUnit.class); + J.CompilationUnit cu = (J.CompilationUnit) after; + List classpath = cu.getMarkers() + .findFirst(JavaSourceSet.class) + .get() + .getClasspath() + .stream() + .map(JavaType.FullyQualified::getFullyQualifiedName) + .toList(); + assertThat(classpath).contains("org.springframework.boot.autoconfigure.SpringBootApplication", "SomeClass"); + + List typesInUse = cu.getTypesInUse() + .getTypesInUse() + .stream() + .map(JavaType.FullyQualified.class::cast) + .map(JavaType.FullyQualified::getFullyQualifiedName) + .toList(); + assertThat(typesInUse) + .containsExactlyInAnyOrder("org.springframework.boot.autoconfigure.SpringBootApplication"); + } + + @NotNull + private static RecipeRun annotateClass(List sourceFiles) { + RecipeRun recipeRun = new GenericOpenRewriteRecipe<>(() -> new JavaIsoVisitor<>() { + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, + ExecutionContext executionContext) { + J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, executionContext); + if (cd.getSimpleName().equals("SomeClass")) { + ClasspathDependencies classpathDependencies = ((J.CompilationUnit) getCursor() + .dropParentUntil(J.CompilationUnit.class::isInstance) + .getValue()).getMarkers().findFirst(ClasspathDependencies.class).get(); + String annotationFqName = "org.springframework.boot.autoconfigure.SpringBootApplication"; + cd = JavaTemplate.builder("@SpringBootApplication") + .imports(annotationFqName) + .javaParser(JavaParser.fromJavaVersion() + .classpath(classpathDependencies.getDependencies()) + .logCompilationWarningsAndErrors(true)) + .build() + .apply(getCursor(), cd.getCoordinates() + .addAnnotation(Comparator.comparing(J.Annotation::getSimpleName))); + maybeAddImport(annotationFqName); + } + return cd; + } + }).run(new InMemoryLargeSourceSet(sourceFiles), new RewriteExecutionContext()); + return recipeRun; + } + + private static void verifyTypesOnClasspathAndNoTypesInUse(RewriteProjectParsingResult parallelParsingResult) { + J.CompilationUnit cuBefore = parallelParsingResult.sourceFiles() + .stream() + .filter(J.CompilationUnit.class::isInstance) + .map(J.CompilationUnit.class::cast) + .findFirst() + .get(); + List typesInUseBefore = cuBefore.getTypesInUse() + .getTypesInUse() + .stream() + .map(JavaType.FullyQualified.class::cast) + .map(JavaType.FullyQualified::getFullyQualifiedName) + .toList(); + assertThat(typesInUseBefore).isEmpty(); + List classpathBefore = cuBefore.getMarkers() + .findFirst(JavaSourceSet.class) + .get() + .getClasspath() + .stream() + .map(JavaType.FullyQualified::getFullyQualifiedName) + .toList(); + assertThat(classpathBefore).contains("org.springframework.boot.autoconfigure.SpringBootApplication", + "SomeClass"); + } + + private static void pepareProject(Path baseDir) { + // formatter:off + TestProjectHelper.createTestProject(baseDir) + .addResource("pom.xml", + """ + + + 4.0.0 + org.example + artifact + 0.1.0-SNAPSHOT + + 17 + 17 + + + + org.springframework.boot + spring-boot-autoconfigure + 3.1.3 + + + + """) + + .addResource("src/main/java/SomeClass.java", """ + public class SomeClass {} + """) + .writeToFilesystem(); + // formatter:on + } + + } + + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + @Nested + class WithMultiModuleMavenProject { + + /** + * formatter:off Multi-module project with two modules module-1 and module-2. + * module-2 has a dependency to spring and a class in main Module2Class and one in + * test Module2Test module-1 has a dependency to module-2 and a class in main + * Module1Class and one in test Module1Test, Module1Class depends on Module2Class + * and MOdule1Test depends on Module2Class and Module2Test. formatter:on + */ + @Test + @DisplayName("Module-1 should have classpath from module-2") + void module1ShouldHaveClasspathFromModule2(@TempDir Path baseDir) { + TestProjectHelper.createTestProject(baseDir) + .addResource("pom.xml", PARENT_POM) + .addResource("module-1/pom.xml", MODULE_1_POM) + .addResource("module-2/pom.xml", MODULE_2_POM) + .addResource("module-1/src/main/java/com/example/module1/main/Module1Class.java", MODULE_1_CLASS) + .addResource("module-1/src/test/java/com/example/module1/test/Module1Test.java", MODULE_1_TEST) + .addResource("module-2/src/main/java/com/example/module2/main/Module2Class.java", MODULE_2_CLASS) + .addResource("module-2/src/test/java/com/example/module2/test/Module2Test.java", MODULE_2_TEST) + .writeToFilesystem(); + + RewriteProjectParsingResult parsingResult = new ParserExecutionHelper() + .parseWithRewriteProjectParser(baseDir, new SpringRewriteProperties()); + + // Module2Class + J.CompilationUnit module2class = getCompilationContainginTypeEndingWith("Module2Class.java", parsingResult); + // classpath + List cpModule2 = getClasspath(module2class); + // typesInUse + List module2classTypesInUse = getTypesInUse(module2class); + + assertThat(module2classTypesInUse).contains("org.springframework.boot.autoconfigure.SpringBootApplication"); + assertThat(cpModule2).contains("org.springframework.boot.autoconfigure.SpringBootApplication"); + // Module2Test + J.CompilationUnit module2test = getCompilationContainginTypeEndingWith("Module2Test.java", parsingResult); + // typesInUse + List module2testTypesInUse = getTypesInUse(module2test); + // classpath + List classpathModule2test = getClasspath(module2test); + + Statement actual = module2test.getClasses().get(0).getBody().getStatements().get(0); + assertThat(classpathModule2test).contains("com.example.module2.main.Module2Class", + "com.example.module2.test.Module2Test", + "org.springframework.boot.autoconfigure.SpringBootApplication", + "org.springframework.boot.test.context.SpringBootTest"); + + // Module1Class + J.CompilationUnit module1class = getCompilationContainginTypeEndingWith("Module1Class.java", parsingResult); + // typesInUse + List module1classTypesInUse = getTypesInUse(module1class); + // classpath + List module1classClasspath = getClasspath(module1class); + + assertThat(module1classTypesInUse).contains("com.example.module2.main.Module2Class"); + assertThat(module1classClasspath).contains("com.example.module1.main.Module1Class", + "com.example.module2.main.Module2Class", + "org.springframework.boot.autoconfigure.SpringBootApplication"); + + assertThat(module1classClasspath).doesNotContain("com.example.module2.test.Module2Test", + "org.springframework.boot.test.context.SpringBootTest"); + + // Module1Test + J.CompilationUnit module1test = getCompilationContainginTypeEndingWith("Module1Test.java", parsingResult); + // typesInUse + List module1testTypesInUse = getTypesInUse(module1test); + // classpath + List classpathModule1test = getClasspath(module1test); + + assertThat(module1testTypesInUse).contains("com.example.module2.main.Module2Class", + "com.example.module2.test.Module2Test", "org.springframework.boot.test.context.SpringBootTest"); + assertThat(classpathModule1test).contains("com.example.module1.main.Module1Class", + "com.example.module1.test.Module1Test", + "org.springframework.boot.autoconfigure.SpringBootApplication", + "org.springframework.boot.test.context.SpringBootTest", "com.example.module2.main.Module2Class", + "com.example.module2.test.Module2Test"); + + } + + // @formatter:off + + @Language("xml") + private static final String PARENT_POM = + """ + + + 4.0.0 + com.example + parent + 0.1.0-SNAPSHOT + pom + + 17 + 17 + + + module-1 + module-2 + + + """; + + @Language("xml") + private static final String MODULE_1_POM = + """ + + + 4.0.0 + + com.example + parent + 0.1.0-SNAPSHOT + ../pom.xml + + module-1 + + 17 + 17 + + + + com.example + module-2 + 0.1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-test + 3.1.2 + test + + + + """; + + private static final String MODULE_1_CLASS = + """ + package com.example.module1.main; + import com.example.module2.main.Module2Class; + + public class Module1Class { + private Module2Class module2Class; + } + """; + + private static final String MODULE_1_TEST = + """ + package com.example.module1.test; + import com.example.module2.main.Module2Class; + import com.example.module2.test.Module2Test; + import org.springframework.boot.test.context.SpringBootTest; + + @SpringBootTest + public class Module1Test extends Module2Test { + private Module1Class module1Class; + private Module2Class module2Class; + } + """; + + @Language("xml") + private static final String MODULE_2_POM = + """ + + + 4.0.0 + + com.example + parent + 0.1.0-SNAPSHOT + ../pom.xml + + module-2 + + 17 + 17 + + + + org.springframework.boot + spring-boot-autoconfigure + 3.1.2 + + + org.springframework.boot + spring-boot-test + 3.1.2 + test + + + + """; + private static final String MODULE_2_CLASS = + """ + package com.example.module2.main; + import org.springframework.boot.autoconfigure.SpringBootApplication; + + @SpringBootApplication + public class Module2Class { + } + """; + + private static final String MODULE_2_TEST = + """ + package com.example.module2.test; + import com.example.module2.main.Module2Class; + import org.springframework.boot.test.context.SpringBootTest; + + @SpringBootTest + public class Module2Test { + Module2Class module2Class; + } + """; + + // @formatter:on + + @NotNull + private static List getClasspath(J.CompilationUnit module2class) { + return module2class.getMarkers() + .findFirst(JavaSourceSet.class) + .get() + .getClasspath() + .stream() + .map(JavaType.FullyQualified::getFullyQualifiedName) + .toList(); + } + + @NotNull + private static List getTypesInUse(J.CompilationUnit module2class) { + return module2class.getTypesInUse() + .getTypesInUse() + .stream() + .map(JavaType.FullyQualified.class::cast) + .map(JavaType.FullyQualified::getFullyQualifiedName) + .toList(); + } + + @NotNull + private static J.CompilationUnit getCompilationContainginTypeEndingWith(String endsWith, + RewriteProjectParsingResult parsingResult) { + return (J.CompilationUnit) parsingResult.sourceFiles() + .stream() + .filter(sf -> sf.getSourcePath().toString().endsWith(endsWith)) + .findFirst() + .get(); + } + + } + +} diff --git a/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/MavenProject.java b/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/MavenProject.java index 80657b2..4af904c 100644 --- a/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/MavenProject.java +++ b/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/MavenProject.java @@ -15,8 +15,9 @@ */ package org.springframework.rewrite.parsers; -import org.apache.maven.model.Model; -import org.apache.maven.model.Plugin; +import org.apache.maven.model.*; +import org.apache.maven.model.io.xpp3.MavenXpp3Reader; +import org.codehaus.plexus.util.xml.pull.XmlPullParserException; import org.jetbrains.annotations.NotNull; import org.openrewrite.maven.tree.MavenResolutionResult; import org.openrewrite.maven.tree.ResolvedDependency; @@ -29,11 +30,9 @@ import org.springframework.rewrite.utils.LinuxWindowsPathUnifier; import org.springframework.rewrite.utils.ResourceUtil; import java.io.File; +import java.io.IOException; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.Properties; +import java.util.*; import java.util.function.Predicate; /** @@ -43,14 +42,24 @@ public class MavenProject { private final Path projectRoot; - private final Resource pomFile; + private final MavenBuildFile buildFile; - // FIXME: 945 temporary method, model should nopt come from Maven - private final Model pomModel; + /** + * All {@link MavenProject}s of this build. + */ + private List reactorProjects = new ArrayList<>(); - private List collectedProjects = new ArrayList<>(); + /** + * List of {@link MavenProject}s that depend on this project. + */ + private final List dependentProjects = new ArrayList<>(); - private Xml.Document sourceFile; + /** + * List of {@link MavenProject}s this project depends on. + */ + private final List dependencyProjects = new ArrayList<>(); + + private final List moduleProjects = new ArrayList<>(); private final MavenArtifactDownloader rewriteMavenArtifactDownloader; @@ -58,35 +67,58 @@ public class MavenProject { private ProjectId projectId; - public MavenProject(Path projectRoot, Resource pomFile, Model pomModel, + public MavenProject(Path baseDir, Resource rootPom, MavenArtifactDownloader rewriteMavenArtifactDownloader, + List resources) { + this(baseDir, rootPom, List.of(), rewriteMavenArtifactDownloader, resources); + } + + public MavenProject(Path baseDir, Resource pomFile, List dependsOnModels, MavenArtifactDownloader rewriteMavenArtifactDownloader, List resources) { - this.projectRoot = projectRoot; - this.pomFile = pomFile; - this.pomModel = pomModel; + this.projectRoot = baseDir; + this.buildFile = new MavenBuildFile(pomFile); + if (dependsOnModels != null) { + this.dependentProjects.addAll(dependsOnModels); + } this.rewriteMavenArtifactDownloader = rewriteMavenArtifactDownloader; this.resources = resources; projectId = new ProjectId(getGroupId(), getArtifactId()); } + public List getDependentProjects() { + return dependentProjects; + } + + public void setDependencyProjects(List dependencyProjects) { + this.dependencyProjects.clear(); + this.dependencyProjects.addAll(dependencyProjects); + } + + public List getDependencyProjects() { + return dependencyProjects; + } + public File getFile() { - return ResourceUtil.getPath(pomFile).toFile(); + return buildFile.getPath().toFile(); + } + + public MavenBuildFile getBuildFile() { + return buildFile; } public Path getBasedir() { // TODO: 945 Check if this is correct - return pomFile == null ? null : ResourceUtil.getPath(pomFile).getParent(); + return buildFile == null ? null : buildFile.getPath().getParent(); } - public void setCollectedProjects(List collected) { - this.collectedProjects = collected; + public void setReactorProjects(List collected) { + this.reactorProjects = collected; } + /** + * @return all {@link MavenProject}s belonging to the same reactor build. + */ public List getCollectedProjects() { - return collectedProjects; - } - - public Resource getResource() { - return pomFile; + return reactorProjects; } /** @@ -103,29 +135,28 @@ public class MavenProject { if (getBasedir() == null) { return null; } - else if ("pom.xml" - .equals(LinuxWindowsPathUnifier.relativize(projectRoot, ResourceUtil.getPath(pomFile)).toString())) { + else if ("pom.xml".equals(LinuxWindowsPathUnifier.relativize(projectRoot, buildFile.getPath()).toString())) { return Path.of(""); } else { - return LinuxWindowsPathUnifier.relativize(projectRoot, ResourceUtil.getPath(pomFile)).getParent(); + return LinuxWindowsPathUnifier.relativize(projectRoot, buildFile.getPath()).getParent(); } } public String getGroupIdAndArtifactId() { - return this.pomModel.getGroupId() + ":" + pomModel.getArtifactId(); + return this.buildFile.getGroupIdAndArtifactId(); } public Path getPomFilePath() { - return ResourceUtil.getPath(pomFile); + return buildFile.getPath(); } public Plugin getPlugin(String s) { - return pomModel.getBuild() == null ? null : pomModel.getBuild().getPluginsAsMap().get(s); + return buildFile.getBuild() == null ? null : buildFile.getBuild().getPluginsAsMap().get(s); } public Properties getProperties() { - return pomModel.getProperties(); + return buildFile.getProperties(); } public MavenRuntimeInformation getMavenRuntimeInformation() { @@ -134,15 +165,15 @@ public class MavenProject { } public String getName() { - return pomModel.getName(); + return buildFile.getName(); } public String getGroupId() { - return pomModel.getGroupId() == null ? pomModel.getParent().getGroupId() : pomModel.getGroupId(); + return buildFile.getGroupId() == null ? buildFile.getParent().getGroupId() : buildFile.getGroupId(); } public String getArtifactId() { - return pomModel.getArtifactId(); + return buildFile.getArtifactId(); } /** @@ -150,30 +181,25 @@ public class MavenProject { * version will be null. */ public String getVersion() { - return pomModel.getVersion() == null ? pomModel.getParent().getVersion() : pomModel.getVersion(); + return buildFile.getVersion() == null ? buildFile.getParent().getVersion() : buildFile.getVersion(); } @Override public String toString() { - String groupId = pomModel.getGroupId() == null ? pomModel.getParent().getGroupId() : pomModel.getGroupId(); - return groupId + ":" + pomModel.getArtifactId(); + String groupId = buildFile.getGroupId() == null ? buildFile.getParent().getGroupId() : buildFile.getGroupId(); + return groupId + ":" + buildFile.getArtifactId(); } public String getBuildDirectory() { - String s = pomModel.getBuild() != null ? pomModel.getBuild().getDirectory() : null; - return s == null - ? ResourceUtil.getPath(pomFile).getParent().resolve("target").toAbsolutePath().normalize().toString() + String s = buildFile.getBuild() != null ? buildFile.getBuild().getDirectory() : null; + return s == null ? buildFile.getPath().getParent().resolve("target").toAbsolutePath().normalize().toString() : s; } public String getSourceDirectory() { - String s = pomModel.getBuild() != null ? pomModel.getBuild().getSourceDirectory() : null; - return s == null ? ResourceUtil.getPath(pomFile) - .getParent() - .resolve("src/main/java") - .toAbsolutePath() - .normalize() - .toString() : s; + String s = buildFile.getBuild() != null ? buildFile.getBuild().getSourceDirectory() : null; + return s == null + ? buildFile.getPath().getParent().resolve("src/main/java").toAbsolutePath().normalize().toString() : s; } public List getCompileClasspathElements() { @@ -187,7 +213,20 @@ public class MavenProject { @NotNull private List getClasspathElements(Scope scope) { - MavenResolutionResult pom = getSourceFile().getMarkers().findFirst(MavenResolutionResult.class).get(); + Xml.Document pomSourceFile = getSourceFile(); + return getClasspathJars(scope, pomSourceFile); + } + + @NotNull + private List getClasspathJars(Scope scope, Xml.Document pomSourceFile) { + MavenArtifactDownloader downloader = rewriteMavenArtifactDownloader; + return getClasspathJars(scope, pomSourceFile, downloader); + } + + @NotNull + public static List getClasspathJars(Scope scope, Xml.Document pomSourceFile, + MavenArtifactDownloader downloader) { + MavenResolutionResult pom = pomSourceFile.getMarkers().findFirst(MavenResolutionResult.class).get(); List resolvedDependencies = pom.getDependencies().get(scope); if (resolvedDependencies != null) { return resolvedDependencies @@ -195,7 +234,7 @@ public class MavenProject { // .stream() .filter(rd -> rd.getRepository() != null) - .map(rd -> rewriteMavenArtifactDownloader.downloadArtifact(rd)) + .map(rd -> downloader.downloadArtifact(rd)) .filter(Objects::nonNull) .distinct() .toList(); @@ -206,17 +245,13 @@ public class MavenProject { } public String getTestSourceDirectory() { - String s = pomModel.getBuild() != null ? pomModel.getBuild().getSourceDirectory() : null; - return s == null ? ResourceUtil.getPath(pomFile) - .getParent() - .resolve("src/test/java") - .toAbsolutePath() - .normalize() - .toString() : s; + String s = buildFile.getBuild() != null ? buildFile.getBuild().getSourceDirectory() : null; + return s == null + ? buildFile.getPath().getParent().resolve("src/test/java").toAbsolutePath().normalize().toString() : s; } public void setSourceFile(Xml.Document sourceFile) { - this.sourceFile = sourceFile; + this.buildFile.setSourceFile(sourceFile); } private static List listJavaSources(List resources, Path sourceDirectory) { @@ -245,6 +280,9 @@ public class MavenProject { return this.resources; } + /** + * @return All {@link Resource}s found under {@code src/main/java} of this project. + */ public List getMainJavaSources() { Path sourceDir = getProjectRoot().resolve(getModuleDir()).resolve("src/main/java"); return listJavaSources(resources, sourceDir); @@ -259,31 +297,316 @@ public class MavenProject { } public Object getProjectEncoding() { - return getPomModel().getProperties().get("project.build.sourceEncoding"); + return buildFile.getProperties().get("project.build.sourceEncoding"); } public Path getProjectRoot() { return projectRoot; } + @Deprecated public Resource getPomFile() { - return pomFile; - } - - public Model getPomModel() { - return pomModel; + return buildFile.getPomFileResource(); } public Xml.Document getSourceFile() { - return sourceFile; + return buildFile.getSourceFile(); } - public MavenArtifactDownloader getRewriteMavenArtifactDownloader() { - return rewriteMavenArtifactDownloader; + public boolean dependsOn(MavenProject model) { + return dependentProjects.stream() + .anyMatch( + m -> m.getGroupId().equals(model.getGroupId()) && m.getArtifactId().equals(model.getArtifactId())); } - public void setProjectId(ProjectId projectId) { - this.projectId = projectId; + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + MavenProject that = (MavenProject) o; + return Objects.equals(buildFile, that.buildFile); + } + + @Override + public int hashCode() { + return Objects.hash(buildFile); + } + + public class MavenBuildFile extends org.apache.maven.model.Model { + + private final Resource pomFileResource; + + // FIXME: 945 temporary method, model should nopt come from Maven + @Deprecated + private final Resource resource; + + private Xml.Document sourceFile; + + private final org.apache.maven.model.Model delegate; + + private static final MavenXpp3Reader XPP_3_READER = new MavenXpp3Reader(); + + public MavenBuildFile(Resource pomFileResource) { + this.pomFileResource = pomFileResource; + assertPomFile(pomFileResource); + this.resource = pomFileResource; + try { + this.delegate = XPP_3_READER.read(ResourceUtil.getInputStream(resource)); + this.delegate.setPomFile(resource.getFile()); + List dependencies = this.delegate.getDependencies(); + dependencies.forEach(d -> { + + }); + } + catch (IOException e) { + throw new RuntimeException(e); + } + catch (XmlPullParserException e) { + throw new RuntimeException(e); + } + } + + private void assertPomFile(Resource resource) { + if (!LinuxWindowsPathUnifier.unifiedPathString(resource).endsWith("pom.xml")) { + throw new IllegalArgumentException( + "Provided resource '%s' is not a pom.xml file.".formatted(ResourceUtil.getPath(resource))); + } + } + + public void setSourceFile(Xml.Document sourceFile) { + this.sourceFile = sourceFile; + } + + public Xml.Document getSourceFile() { + return sourceFile; + } + + public String getContent() { + return ResourceUtil.getContent(pomFileResource); + } + + public Path getPath() { + return ResourceUtil.getPath(pomFileResource); + } + + public Resource getPomFileResource() { + return pomFileResource; + } + + // Model methods + @Override + public String toString() { + return (delegate.getGroupId() == null ? delegate.getParent().getGroupId() : delegate.getGroupId()) + ":" + + delegate.getArtifactId(); + } + + @Override + public String getArtifactId() { + return delegate.getArtifactId(); + } + + @Override + public Build getBuild() { + return delegate.getBuild(); + } + + @Override + public String getChildProjectUrlInheritAppendPath() { + return delegate.getChildProjectUrlInheritAppendPath(); + } + + @Override + public CiManagement getCiManagement() { + return delegate.getCiManagement(); + } + + @Override + public List getContributors() { + return delegate.getContributors(); + } + + @Override + public String getDescription() { + return delegate.getDescription(); + } + + @Override + public List getDevelopers() { + return delegate.getDevelopers(); + } + + @Override + public String getGroupId() { + return delegate.getGroupId(); + } + + @Override + public String getInceptionYear() { + return delegate.getInceptionYear(); + } + + @Override + public IssueManagement getIssueManagement() { + return delegate.getIssueManagement(); + } + + @Override + public List getLicenses() { + return delegate.getLicenses(); + } + + @Override + public List getMailingLists() { + return delegate.getMailingLists(); + } + + @Override + public String getModelEncoding() { + return delegate.getModelEncoding(); + } + + @Override + public String getModelVersion() { + return delegate.getModelVersion(); + } + + @Override + public String getName() { + String name = delegate.getName(); + if (name == null) { + name = delegate.getArtifactId(); + } + return name; + } + + @Override + public Organization getOrganization() { + return delegate.getOrganization(); + } + + @Override + public String getPackaging() { + return delegate.getPackaging(); + } + + @Override + public Parent getParent() { + return delegate.getParent(); + } + + @Override + public Prerequisites getPrerequisites() { + return delegate.getPrerequisites(); + } + + @Override + public List getProfiles() { + return delegate.getProfiles(); + } + + @Override + public Scm getScm() { + return delegate.getScm(); + } + + @Override + public String getUrl() { + return delegate.getUrl(); + } + + @Override + public String getVersion() { + return delegate.getVersion(); + } + + @Override + public File getPomFile() { + return delegate.getPomFile(); + } + + @Override + public File getProjectDirectory() { + return delegate.getPomFile().toPath().getParent().toFile(); + } + + @Override + public String getId() { + return delegate.getId(); + } + + @Override + public List getDependencies() { + return delegate.getDependencies(); + } + + @Override + public DependencyManagement getDependencyManagement() { + return delegate.getDependencyManagement(); + } + + @Override + public DistributionManagement getDistributionManagement() { + return delegate.getDistributionManagement(); + } + + @Override + public InputLocation getLocation(Object key) { + return delegate.getLocation(key); + } + + @Override + public List getModules() { + return delegate.getModules(); + } + + @Override + public List getPluginRepositories() { + return delegate.getPluginRepositories(); + } + + @Override + public Properties getProperties() { + return delegate.getProperties(); + } + + @Override + public Reporting getReporting() { + return delegate.getReporting(); + } + + @Override + public Object getReports() { + return delegate.getReports(); + } + + @Override + public List getRepositories() { + return delegate.getRepositories(); + } + + public String getGroupIdAndArtifactId() { + return getGroupId() + ":" + getArtifactId(); + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + MavenBuildFile that = (MavenBuildFile) o; + Path thisPath = ResourceUtil.getPath(this.pomFileResource); + Path thatPath = ResourceUtil.getPath(that.pomFileResource); + return Objects.equals(thisPath, thatPath); + } + + @Override + public int hashCode() { + return Objects.hash(pomFileResource); + } + } } diff --git a/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/ModuleParsingResult.java b/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/ModuleParsingResult.java new file mode 100644 index 0000000..fb616e8 --- /dev/null +++ b/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/ModuleParsingResult.java @@ -0,0 +1,35 @@ +/* + * 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.rewrite.parsers; + +import org.openrewrite.SourceFile; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Fabian Krüger + */ +public record ModuleParsingResult(MavenProject currentProject, SourceSetParsingResult mainSourcesParsingResult, + SourceSetParsingResult testSourcesParsingResult, List resourceFilesList) { + public List sourceFiles() { + List allSourceFiles = new ArrayList<>(); + allSourceFiles.addAll(mainSourcesParsingResult.sourceFiles()); + allSourceFiles.addAll(testSourcesParsingResult.sourceFiles()); + allSourceFiles.addAll(resourceFilesList); + return allSourceFiles; + } +} diff --git a/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/RewriteParserConfiguration.java b/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/RewriteParserConfiguration.java index 9546aac..c3ca9e6 100644 --- a/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/RewriteParserConfiguration.java +++ b/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/RewriteParserConfiguration.java @@ -102,7 +102,10 @@ public class RewriteParserConfiguration { @Bean MavenProjectAnalyzer mavenProjectAnalyzer(MavenArtifactDownloader artifactDownloader) { - return new MavenProjectAnalyzer(artifactDownloader); + MavenProjectGraph mavenProjectGraph = new MavenProjectGraph(); + MavenProjectSorter mavenProjectSorter = new MavenProjectSorter(mavenProjectGraph); + MavenProjectFactory mavenProjectFactory = new MavenProjectFactory(artifactDownloader); + return new MavenProjectAnalyzer(mavenProjectSorter, mavenProjectFactory); } @Bean diff --git a/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/RewriteProjectParser.java b/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/RewriteProjectParser.java index 8ec7bd7..80628e8 100644 --- a/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/RewriteProjectParser.java +++ b/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/RewriteProjectParser.java @@ -35,7 +35,6 @@ import org.springframework.rewrite.parsers.maven.MavenProjectAnalyzer; import org.springframework.rewrite.parsers.maven.ProvenanceMarkerFactory; import org.springframework.rewrite.recipes.RewriteRecipeDiscovery; import org.springframework.rewrite.scopes.ScanScope; -import org.springframework.rewrite.utils.LinuxWindowsPathUnifier; import org.springframework.util.StringUtils; import java.nio.file.Path; @@ -140,7 +139,8 @@ public class RewriteProjectParser { List styles = List.of(); // Get the ordered otherSourceFiles of projects - ParserContext parserContext = mavenProjectAnalyzer.createParserContext(baseDir, resources); + List sortedProjects = mavenProjectAnalyzer.getBuildProjects(baseDir, resources); + ParserContext parserContext = new ParserContext(baseDir, resources, sortedProjects); // generate provenance Map> provenanceMarkers = provenanceMarkerFactory.generateProvenanceMarkers(baseDir, diff --git a/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/SourceFileParser.java b/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/SourceFileParser.java index 5c0379b..7f4a835 100644 --- a/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/SourceFileParser.java +++ b/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/SourceFileParser.java @@ -46,6 +46,9 @@ public class SourceFileParser { Set parsedSourceFiles = new LinkedHashSet<>(); + // we use the map to look up previous parsing results when building the classpath + // of a module + Map parsingResultsMap = new HashMap<>(); parserContext.getSortedProjects().forEach(currentMavenProject -> { Xml.Document moduleBuildFile = currentMavenProject.getSourceFile(); List markers = provenanceMarkers.get(currentMavenProject.getPomFilePath()); @@ -53,9 +56,30 @@ public class SourceFileParser { LOGGER.warn("Could not find provenance markers for resource '%s'" .formatted(parserContext.getMatchingBuildFileResource(currentMavenProject))); } - List sourceFiles = moduleParser.parseModuleSourceFiles(resources, currentMavenProject, - moduleBuildFile, markers, styles, executionContext, baseDir); - parsedSourceFiles.addAll(sourceFiles); + ModuleParsingResult result = moduleParser.parseModule(baseDir, resources, currentMavenProject, + moduleBuildFile, markers, styles, executionContext, parsingResultsMap); + + parsingResultsMap.put(currentMavenProject, result); + + // Maybe.. + // Return ModuleParsingResult + // mpr.getMainClasspath() + // mpr.getTestClasspath() + // if(currentMavenProject.dependsOn(mpr.getModule()) + // requirements: + // - provide jars that define the classpath + // - provide classes from (transitive) module(s) + + // Retrieve and append the shared classpath from previously parsed modules + Set classpath = new HashSet<>(); + Map> modelClasspathMap = new HashMap<>(); + currentMavenProject.getDependentProjects().forEach(m -> { + Set dependencyPaths = modelClasspathMap.get(m); + classpath.addAll(dependencyPaths); + }); + // TODO: provide the classpath to ModuleParser + + parsedSourceFiles.addAll(result.sourceFiles()); }); return new ArrayList<>(parsedSourceFiles); diff --git a/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/SourceSetParsingResult.java b/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/SourceSetParsingResult.java index 512cd7b..138e72a 100644 --- a/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/SourceSetParsingResult.java +++ b/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/SourceSetParsingResult.java @@ -16,6 +16,7 @@ package org.springframework.rewrite.parsers; import org.openrewrite.SourceFile; +import org.openrewrite.java.internal.JavaTypeCache; import org.openrewrite.java.tree.JavaType; import java.util.List; @@ -23,5 +24,6 @@ import java.util.List; /** * @author Fabian Krüger */ -public record SourceSetParsingResult(List sourceFiles, List classpath) { +public record SourceSetParsingResult(List sourceFiles, List classpath, + JavaTypeCache typeCache) { } diff --git a/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/maven/MavenBuildFileFilter.java b/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/maven/MavenBuildFileFilter.java new file mode 100644 index 0000000..fcb7c86 --- /dev/null +++ b/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/maven/MavenBuildFileFilter.java @@ -0,0 +1,38 @@ +/* + * 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.rewrite.parsers.maven; + +import org.jetbrains.annotations.NotNull; +import org.springframework.core.io.Resource; +import org.springframework.rewrite.utils.ResourceUtil; + +import java.util.List; + +/** + * @author Fabian Krüger + */ +class MavenBuildFileFilter { + + private static final String POM_XML = "pom.xml"; + + @NotNull + static List filterBuildFiles(List resources) { + return resources.stream() + .filter(r -> ResourceUtil.getPath(r).getFileName().toString().equals(POM_XML)) + .toList(); + } + +} diff --git a/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/maven/MavenModuleParser.java b/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/maven/MavenModuleParser.java index 369f498..bcd49e5 100644 --- a/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/maven/MavenModuleParser.java +++ b/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/maven/MavenModuleParser.java @@ -67,9 +67,9 @@ public class MavenModuleParser { this.springRewriteProperties = springRewriteProperties; } - public List parseModuleSourceFiles(List resources, MavenProject currentProject, + public ModuleParsingResult parseModule(Path baseDir, List resources, MavenProject currentProject, Xml.Document moduleBuildFile, List provenanceMarkers, List styles, - ExecutionContext executionContext, Path baseDir) { + ExecutionContext executionContext, Map parsingResultsMap) { List sourceFiles = new ArrayList<>(); // 146:149: get source encoding from maven @@ -85,9 +85,10 @@ public class MavenModuleParser { .setCharset(Charset.forName(mavenSourceEncoding.toString())); } + boolean logCompilationWarningsAndErrors = springRewriteProperties.isLogCompilationWarningsAndErrors(); JavaParser.Builder javaParserBuilder = JavaParser.fromJavaVersion() .styles(styles) - .logCompilationWarningsAndErrors(springRewriteProperties.isLogCompilationWarningsAndErrors()); + .logCompilationWarningsAndErrors(logCompilationWarningsAndErrors); Path buildFilePath = currentProject.getBasedir().resolve(moduleBuildFile.getSourcePath()); LOGGER.info("Parsing module " + buildFilePath); @@ -102,12 +103,12 @@ public class MavenModuleParser { alreadyParsed.add(moduleBuildFilePath); alreadyParsed.addAll(skipResourceScanDirs); - SourceSetParsingResult mainSourcesParsingResult = processMainSources(baseDir, resources, moduleBuildFile, - javaParserBuilder, rp, provenanceMarkers, alreadyParsed, executionContext, currentProject); + SourceSetParsingResult mainSourcesParsingResult = parseMainSourceSet(baseDir, currentProject, javaParserBuilder, + parsingResultsMap, executionContext, alreadyParsed, provenanceMarkers, resources, rp); - SourceSetParsingResult testSourcesParsingResult = processTestSources(baseDir, moduleBuildFile, - javaParserBuilder, rp, provenanceMarkers, alreadyParsed, executionContext, currentProject, resources, - mainSourcesParsingResult.classpath()); + SourceSetParsingResult testSourcesParsingResult = parseTestSourceSet(baseDir, currentProject, javaParserBuilder, + parsingResultsMap, executionContext, alreadyParsed, provenanceMarkers, resources, rp, + mainSourcesParsingResult); // Collect the dirs of modules parsed in previous steps // parse other project resources @@ -122,7 +123,199 @@ public class MavenModuleParser { sourceFiles.addAll(mainAndTestSources); sourceFiles.addAll(resourceFilesList); - return sourceFiles; + ModuleParsingResult moduleParsingResult = new ModuleParsingResult(currentProject, mainSourcesParsingResult, + testSourcesParsingResult, resourceFilesList); + return moduleParsingResult; + } + + /** + * Parse main source set {@code src/main} from current module. The classpath for Java + * sources is created from jars and compilation units of dependency project previously + * parsed. The parsed java sources are collected and provided with the result and can + * be used by subsequent parse to build tha classpath. + */ + SourceSetParsingResult parseMainSourceSet(@Nullable Path baseDir, MavenProject currentProject, + JavaParser.Builder javaParserBuilder, + Map parsingResultsMap, ExecutionContext executionContext, + Set alreadyParsed, List provenanceMarkers, List resources, + RewriteResourceParser rp) { + // collect and prepare all types for classpath and TypeCache + // java sources in current source set + List javaSourcesInSrc = currentProject.getMainJavaSources(); + // jars from dependencies + List classpathJars = currentProject.getCompileClasspathElements(); + + LOGGER.debug("Dependencies on main classpath: %s".formatted(classpathJars)); + javaParserBuilder.classpath(classpathJars); + + // sources from other dependency modules + List sourceFilesFromOtherModules = currentProject.getDependencyProjects() + .stream() + // get their parsing result + .map(project -> parsingResultsMap.get(project)) + .flatMap(result -> result.mainSourcesParsingResult().sourceFiles().stream()) + .toList(); + + String[] dependsOnSources = sourceFilesFromOtherModules.stream() + .map(SourceFile::printAll) + .toArray(String[]::new); + javaParserBuilder.dependsOn(dependsOnSources); + + JavaTypeCache typeCache = getJavaTypeCache(currentProject, parsingResultsMap, sourceFilesFromOtherModules); + javaParserBuilder.typeCache(typeCache); + + Set sourceSetClassesCp = new HashSet<>(); + + // Add test sources from dependency projects to classpath + sourceFilesFromOtherModules.stream() + .filter(J.CompilationUnit.class::isInstance) + .map(J.CompilationUnit.class::cast) + .flatMap(s -> s.getClasses().stream()) + .map(J.ClassDeclaration::getType) + .forEach(sourceSetClassesCp::add); + + return parseSourceSet(baseDir, currentProject, javaSourcesInSrc, javaParserBuilder, sourceSetClassesCp, + executionContext, alreadyParsed, classpathJars, typeCache, provenanceMarkers, "main", resources, rp, + "src/main"); + } + + /** + * Parse test source set {@code src/test} from current module. The classpath for Java + * sources is created from jars and compilation units of dependency project previously + * parsed. The parsed java sources are collected and provided with the result and can + * be used by subsequent parse to build tha classpath. + */ + SourceSetParsingResult parseTestSourceSet(@Nullable Path baseDir, MavenProject currentProject, + JavaParser.Builder javaParserBuilder, + Map parsingResultsMap, ExecutionContext executionContext, + Set alreadyParsed, List provenanceMarkers, List resources, RewriteResourceParser rp, + SourceSetParsingResult mainSourcesParsingResult) { + // collect and prepare all types for classpath and TypeCache + // java sources in current source set + List javaSourcesInSrc = currentProject.getTestJavaSources(); + // jars from dependencies + List classpathJars = currentProject.getTestClasspathElements(); + + LOGGER.debug("Dependencies on main classpath: %s".formatted(classpathJars)); + javaParserBuilder.classpath(classpathJars); + + // sources from other dependency modules + List sourceFilesFromOtherModules = currentProject.getDependencyProjects() + .stream() + // get their parsing result + .map(project -> parsingResultsMap.get(project)) + .flatMap(result -> { + return Stream.concat(result.mainSourcesParsingResult().sourceFiles().stream(), + result.testSourcesParsingResult().sourceFiles().stream()); + }) + .toList(); + + List sourceFilesFromMain = mainSourcesParsingResult.sourceFiles(); + String[] dependsOnSources = Stream.concat(sourceFilesFromMain.stream(), sourceFilesFromOtherModules.stream()) + .map(SourceFile::printAll) + .toArray(String[]::new); + javaParserBuilder.dependsOn(dependsOnSources); + + JavaTypeCache typeCache = getJavaTypeCache(currentProject, parsingResultsMap, sourceFilesFromOtherModules); + javaParserBuilder.typeCache(typeCache); + + Set sourceSetClassesCp = new HashSet<>(); + + // Add test sources from dependency projects to classpath + Stream.concat(sourceFilesFromMain.stream(), sourceFilesFromOtherModules.stream()) + .filter(J.CompilationUnit.class::isInstance) + .map(J.CompilationUnit.class::cast) + .flatMap(s -> s.getClasses().stream()) + .map(J.ClassDeclaration::getType) + .forEach(sourceSetClassesCp::add); + + return parseSourceSet(baseDir, currentProject, javaSourcesInSrc, javaParserBuilder, sourceSetClassesCp, + executionContext, alreadyParsed, classpathJars, typeCache, provenanceMarkers, "test", resources, rp, + "src/test"); + } + + SourceSetParsingResult parseSourceSet(@Nullable Path baseDir, MavenProject currentProject, + List javaSourcesInSrc, JavaParser.Builder javaParserBuilder, + Set localClassesCp, ExecutionContext executionContext, Set alreadyParsed, + List classpathJars, JavaTypeCache typeCache, List provenanceMarkers, String sourceSetName, + List resources, RewriteResourceParser rp, String sourceDir) { + // collect source files from module src dir + List javaSources = new ArrayList<>(); + List javaSourcesInTarget = currentProject.getJavaSourcesInTarget(); + javaSources.addAll(javaSourcesInTarget); + javaSources.addAll(javaSourcesInSrc); + + Iterable inputs = javaSources.stream().map(r -> { + FileAttributes fileAttributes = null; + Path path = ResourceUtil.getPath(r); + boolean isSynthetic = Files.exists(path); + Supplier inputStreamSupplier = () -> ResourceUtil.getInputStream(r); + Parser.Input input = new Parser.Input(path, fileAttributes, inputStreamSupplier, isSynthetic); + return input; + }).toList(); + + // collecting parsed compilation units to the classpath (localClassesCp). + List cus = javaParserBuilder.build() + .parseInputs(inputs, baseDir, executionContext) + .peek(s -> { + ((J.CompilationUnit) s).getClasses() + .stream() + .map(J.ClassDeclaration::getType) + .forEach(localClassesCp::add); + + alreadyParsed.add(baseDir.resolve(s.getSourcePath())); + }) + .toList(); + + JavaSourceSet javaSourceSet = sourceSet(sourceSetName, classpathJars, typeCache); + List markers = new ArrayList<>(provenanceMarkers); + + javaSourceSet = appendToClasspath(localClassesCp, javaSourceSet); + ClasspathDependencies classpathDependencies = new ClasspathDependencies(classpathJars); + + markers.add(javaSourceSet); + markers.add(classpathDependencies); + + List parsedJavaPaths = javaSourcesInTarget.stream().map(ResourceUtil::getPath).toList(); + Stream parsedJavaSources = cus.stream().map(addProvenance(baseDir, markers, parsedJavaPaths)); + LOGGER.debug("[%s] Scanned %d java source files in main scope.".formatted(currentProject, javaSources.size())); + + // Filter out any generated source files from the returned list, as we do not want + // to apply the recipe to the + // generated files. + Path buildDirectory = LinuxWindowsPathUnifier.unifiedPath(Paths.get(currentProject.getBuildDirectory())); + List filteredJavaSources = filterOutResourcesInDir(parsedJavaSources, buildDirectory); + + int sourcesParsedBefore = alreadyParsed.size(); + alreadyParsed.addAll(parsedJavaPaths); + + List resourcesLeft = resources.stream() + .filter(r -> alreadyParsed.stream().noneMatch(path -> LinuxWindowsPathUnifier.pathStartsWith(r, path))) + .toList(); + + LOGGER.info("Parsing test resources"); + Path searchDir = currentProject.getModulePath().resolve(sourceDir).resolve("resources"); + List parsedResourceFiles = rp + .parseSourceFiles(searchDir, resourcesLeft, alreadyParsed, executionContext) + .map(addProvenance(baseDir, markers, null)) + .toList(); + + LOGGER.info("Parsed %d main resources".formatted(parsedResourceFiles.size())); + + // TODO: Remove + // List parsedResourceFiles = rp + // .parse(currentProject.getModulePath().resolve("src/main/resources"), resources, + // alreadyParsed) + // .map(addProvenance(baseDir, mainProjectProvenance, null)) + // .toList(); + + LOGGER.debug("[%s] Scanned %d resource files in main scope.".formatted(currentProject, + (alreadyParsed.size() - sourcesParsedBefore))); + // Any resources parsed from "main/resources" should also have the main source set + // added to them. + filteredJavaSources.addAll(parsedResourceFiles); + return new SourceSetParsingResult(filteredJavaSources, javaSourceSet.getClasspath(), typeCache); + } /** @@ -163,134 +356,26 @@ public class MavenModuleParser { private Set pathsToOtherMavenProjects(MavenProject mavenProject, Path moduleBuildFile) { return mavenProject.getCollectedProjects() .stream() - .filter(p -> !p.getFile().toPath().toString().equals(moduleBuildFile.toString())) + .filter(p -> !LinuxWindowsPathUnifier.pathEquals(p.getBuildFile().getPath(), moduleBuildFile)) .map(p -> p.getFile().toPath().getParent()) .collect(Collectors.toSet()); } - /** - * Parse Java sources and resources under {@code src/main} of current module. - */ - public SourceSetParsingResult processMainSources(Path baseDir, List resources, - Xml.Document moduleBuildFile, JavaParser.Builder javaParserBuilder, - RewriteResourceParser rp, List provenanceMarkers, Set alreadyParsed, - ExecutionContext executionContext, MavenProject currentProject) { - LOGGER.info("Processing main sources in module '%s'".formatted(currentProject.getProjectId())); - // FIXME: 945 - // Some annotation processors output generated sources to the /target directory. - // These are added for parsing but - // should be filtered out of the final SourceFile list. - - List mainJavaSources = new ArrayList<>(); - List javaSourcesInTarget = currentProject.getJavaSourcesInTarget(); // listJavaSources(resources, - // currentProject.getBasedir().resolve(currentProject.getBuildDirectory())); - List javaSourcesInMain = currentProject.getMainJavaSources(); // listJavaSources(resources, - // currentProject.getBasedir().resolve(currentProject.getSourceDirectory())); - mainJavaSources.addAll(javaSourcesInTarget); - mainJavaSources.addAll(javaSourcesInMain); - - LOGGER.info("[%s] Parsing main source files".formatted(currentProject)); - - // FIXME 945 classpath - // - Resolve dependencies to non-reactor projects from Maven repository - // - Resolve dependencies to reactor projects by providing the sources - // javaParserBuilder.classpath(byte[]) - - // we're processing a module here. The classpath of the module consists of all - // declared dependencies and their transitive dependencies too. - // For dependencies to projects that belong to the current rector... - // They'd either need to be built with Maven before to guarantee that the jars are - // installed to local Maven repo. - // Or, the classpath must be created from the sources of the project. - - List dependencies = currentProject.getCompileClasspathElements(); - - javaParserBuilder.classpath(dependencies); - - LOGGER.info("Dependencies on main classpath: %s".formatted(dependencies)); - - JavaTypeCache typeCache = new JavaTypeCache(); - javaParserBuilder.typeCache(typeCache); - - Iterable inputs = mainJavaSources.stream().map(r -> { - FileAttributes fileAttributes = null; - Path path = ResourceUtil.getPath(r); - boolean isSynthetic = Files.exists(path); - Supplier inputStreamSupplier = () -> ResourceUtil.getInputStream(r); - Parser.Input input = new Parser.Input(path, fileAttributes, inputStreamSupplier, isSynthetic); - return input; - }).toList(); - - LOGGER.info("Parsing main Java sources."); - - Set localClassesCp = new HashSet<>(); - List cus = javaParserBuilder.build() - .parseInputs(inputs, baseDir, executionContext) - .peek(s -> { - ((J.CompilationUnit) s).getClasses() - .stream() - .map(J.ClassDeclaration::getType) - .forEach(localClassesCp::add); - - alreadyParsed.add(baseDir.resolve(s.getSourcePath())); - }) - .toList(); - - LOGGER.info("Parsed %d main Java source files.".formatted(cus.size())); - - // TODO: This is a hack: - // Parsed java sources are not themselves on the classpath (here). - // The actual parsing happens when the stream is terminated (toList), - // therefore the toList() must be called before the parsed compilation units can - // be added to the classpath - JavaSourceSet javaSourceSet = sourceSet("main", dependencies, typeCache); - List mainProjectProvenance = new ArrayList<>(provenanceMarkers); - javaSourceSet = appendToClasspath(localClassesCp, javaSourceSet); - mainProjectProvenance.add(javaSourceSet); - ClasspathDependencies classpathDependencies = new ClasspathDependencies(dependencies); - mainProjectProvenance.add(classpathDependencies); - - List parsedJavaPaths = javaSourcesInTarget.stream().map(ResourceUtil::getPath).toList(); - Stream parsedMainJava = cus.stream() - .map(addProvenance(baseDir, mainProjectProvenance, parsedJavaPaths)); - LOGGER.debug( - "[%s] Scanned %d java source files in main scope.".formatted(currentProject, mainJavaSources.size())); - - // Filter out any generated source files from the returned list, as we do not want - // to apply the recipe to the - // generated files. - Path buildDirectory = LinuxWindowsPathUnifier.unifiedPath(Paths.get(currentProject.getBuildDirectory())); - List filteredMainJava = filterOutResourcesInDir(parsedMainJava, buildDirectory); - - int sourcesParsedBefore = alreadyParsed.size(); - alreadyParsed.addAll(parsedJavaPaths); - - List resourcesLeft = resources.stream() - .filter(r -> alreadyParsed.stream().noneMatch(path -> LinuxWindowsPathUnifier.pathStartsWith(r, path))) - .toList(); - - LOGGER.info("Parsing main resources"); - List parsedResourceFiles = rp - .parseSourceFiles(currentProject.getModulePath().resolve("src/main/resources"), resourcesLeft, - alreadyParsed, executionContext) - .map(addProvenance(baseDir, mainProjectProvenance, null)) - .toList(); - - LOGGER.info("Parsed %d main resources".formatted(parsedResourceFiles.size())); - - // TODO: Remove - // List parsedResourceFiles = rp - // .parse(currentProject.getModulePath().resolve("src/main/resources"), resources, - // alreadyParsed) - // .map(addProvenance(baseDir, mainProjectProvenance, null)) - // .toList(); - - LOGGER.debug("[%s] Scanned %d resource files in main scope.".formatted(currentProject, - (alreadyParsed.size() - sourcesParsedBefore))); - // Any resources parsed from "main/resources" should also have the main source set - // added to them. - filteredMainJava.addAll(parsedResourceFiles); - return new SourceSetParsingResult(filteredMainJava, javaSourceSet.getClasspath()); + private static JavaTypeCache getJavaTypeCache(MavenProject currentProject, + Map parsingResultsMap, List sourceFilesFromOtherModules) { + JavaTypeCache typeCache; + if (!sourceFilesFromOtherModules.isEmpty()) { + Optional optJavaTypeCache = currentProject.getDependencyProjects() + .stream() + .map(mp -> parsingResultsMap.get(mp).mainSourcesParsingResult().typeCache()) + .max(Comparator.comparing(JavaTypeCache::size)); + typeCache = optJavaTypeCache.orElseThrow(() -> new IllegalStateException( + "No TypeCahche from previous build found for project " + currentProject.getProjectId())); + } + else { + typeCache = new JavaTypeCache(); + } + return typeCache; } @NotNull @@ -321,84 +406,4 @@ public class MavenModuleParser { return JavaSourceSet.build(name, dependencies, typeCache, false); } - /** - * Parse Java sources and resource files under {@code src/test}. - */ - public SourceSetParsingResult processTestSources(Path baseDir, Xml.Document moduleBuildFile, - JavaParser.Builder javaParserBuilder, RewriteResourceParser rp, - List provenanceMarkers, Set alreadyParsed, ExecutionContext executionContext, - MavenProject currentProject, List resources, List classpath) { - LOGGER.info("Processing test sources in module '%s'".formatted(currentProject.getProjectId())); - - List testDependencies = currentProject.getTestClasspathElements(); - - javaParserBuilder.classpath(testDependencies); - JavaTypeCache typeCache = new JavaTypeCache(); - javaParserBuilder.typeCache(typeCache); - - List testJavaSources = currentProject.getTestJavaSources(); - // listJavaSources(resources, - // currentProject.getBasedir().resolve(currentProject.getTestSourceDirectory())); - // alreadyParsed.addAll(testJavaSources.stream().map(ResourceUtil::getPath).toList()); - - Iterable inputs = testJavaSources.stream() - .map(r -> new Parser.Input(ResourceUtil.getPath(r), () -> ResourceUtil.getInputStream(r))) - .toList(); - - final List localClassesCp = new ArrayList<>(); - List cus = javaParserBuilder.build() - .parseInputs(inputs, baseDir, executionContext) - .peek(s -> { - ((J.CompilationUnit) s).getClasses() - .stream() - .map(J.ClassDeclaration::getType) - .forEach(localClassesCp::add); - alreadyParsed.add(baseDir.resolve(s.getSourcePath())); - }) - .toList(); - - List markers = new ArrayList<>(provenanceMarkers); - - JavaSourceSet javaSourceSet = sourceSet("test", testDependencies, typeCache); - Set curClasspath = Stream.concat(classpath.stream(), localClassesCp.stream()) - .collect(Collectors.toSet()); - javaSourceSet = appendToClasspath(curClasspath, javaSourceSet); - markers.add(javaSourceSet); - Stream parsedJava = cus.stream().map(addProvenance(baseDir, markers, null)); - - LOGGER.debug( - "[%s] Scanned %d java source files in test scope.".formatted(currentProject, testJavaSources.size())); - Stream sourceFiles = parsedJava; - - // Any resources parsed from "test/resources" should also have the test source set - // added to them. - int sourcesParsedBefore = alreadyParsed.size(); - Stream parsedResourceFiles = rp - .parse(currentProject.getBasedir().resolve("src/test/resources"), resources, alreadyParsed) - .map(addProvenance(baseDir, markers, null)); - LOGGER.debug("[%s] Scanned %d resource files in test scope.".formatted(currentProject, - (alreadyParsed.size() - sourcesParsedBefore))); - sourceFiles = Stream.concat(sourceFiles, parsedResourceFiles); - List result = sourceFiles.toList(); - return new SourceSetParsingResult(result, javaSourceSet.getClasspath()); - } - - // FIXME: 945 take Java sources from resources - private static List listJavaSources(List resources, Path sourceDirectory) { - return resources.stream() - .filter(whenIn(LinuxWindowsPathUnifier.unifiedPath(sourceDirectory))) - .filter(whenFileNameEndsWithJava()) - .toList(); - } - - @NotNull - private static Predicate whenFileNameEndsWithJava() { - return p -> ResourceUtil.getPath(p).getFileName().toString().endsWith(".java"); - } - - @NotNull - private static Predicate whenIn(Path sourceDirectory) { - return r -> ResourceUtil.getPath(r).toString().startsWith(sourceDirectory.toString()); - } - } diff --git a/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/maven/MavenProjectAnalyzer.java b/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/maven/MavenProjectAnalyzer.java index e30066b..c500a50 100644 --- a/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/maven/MavenProjectAnalyzer.java +++ b/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/maven/MavenProjectAnalyzer.java @@ -15,23 +15,12 @@ */ package org.springframework.rewrite.parsers.maven; -import org.apache.maven.model.*; -import org.apache.maven.model.io.xpp3.MavenXpp3Reader; -import org.codehaus.plexus.util.xml.pull.XmlPullParserException; -import org.jetbrains.annotations.NotNull; -import org.openrewrite.maven.utilities.MavenArtifactDownloader; import org.springframework.core.io.Resource; import org.springframework.rewrite.parsers.MavenProject; -import org.springframework.rewrite.parsers.ParserContext; import org.springframework.rewrite.utils.LinuxWindowsPathUnifier; -import org.springframework.rewrite.utils.ResourceUtil; -import java.io.File; -import java.io.IOException; import java.nio.file.Path; import java.util.*; -import java.util.function.Predicate; -import java.util.stream.Collectors; /** * Implements the ordering of Maven (reactor) build projects. See getSortedProjects(Path baseDir, List resources) { - - List allPomFiles = resources.stream() - .filter(r -> ResourceUtil.getPath(r).getFileName().toString().equals(POM_XML)) - .toList(); - - if (allPomFiles.isEmpty()) { - throw new IllegalArgumentException("The provided resources did not contain any 'pom.xml' file."); - } - - Resource rootPom = allPomFiles.stream() - .filter(r -> LinuxWindowsPathUnifier.pathEquals(r, baseDir.resolve(POM_XML))) - .findFirst() - .orElseThrow( - () -> new IllegalArgumentException("The provided resources do not contain a root 'pom.xml' file.")); - Model rootPomModel = new Model(rootPom); - - if (isSingleModuleProject(rootPomModel)) { - return List.of(new MavenProject(baseDir, rootPom, rootPomModel, rewriteMavenArtifactDownloader, resources)); - } - List reactorModels = new ArrayList<>(); - recursivelyFindReactorModules(baseDir, null, reactorModels, allPomFiles, rootPomModel); - List sortedModels = sortModels(reactorModels); - return map(baseDir, resources, sortedModels); + public List getBuildProjects(Path baseDir, List resources) { + List allMavenProjects = mavenProjectFactory.create(baseDir, resources); + List mavenProjects = mavenProjectSorter.sort(baseDir, allMavenProjects); + return map(baseDir, resources, mavenProjects); } - private List map(Path baseDir, List resources, List sortedModels) { + private List map(Path baseDir, List resources, List sortedModels) { + List mavenProjects = new ArrayList<>(); - sortedModels.stream().filter(Objects::nonNull).forEach(m -> { + sortedModels.stream().filter(Objects::nonNull).forEach(mavenProject -> { String projectDir = LinuxWindowsPathUnifier - .unifiedPathString(baseDir.resolve(m.getProjectDirectory().toPath()).normalize()); + .unifiedPathString(baseDir.resolve(mavenProject.getModuleDir()).normalize()); List filteredResources = resources.stream() .filter(r -> LinuxWindowsPathUnifier.unifiedPathString(r).startsWith(projectDir)) .toList(); - MavenProject mavenProject = new MavenProject(baseDir, m.getResource(), m, rewriteMavenArtifactDownloader, - filteredResources); mavenProjects.add(mavenProject); }); - // set all non parent poms as collected projects for root parent pom + // set all non parent poms as collected projects for root parent p List collected = new ArrayList<>(mavenProjects); collected.remove(0); - mavenProjects.get(0).setCollectedProjects(collected); + mavenProjects.get(0).setReactorProjects(collected); return mavenProjects; } - private List recursivelyFindReactorModules(Path baseDir, String path, List reactorModels, - List allPomFiles, Model pomModel) { - // TODO: verify given module is root pom - if (pomModel != null) { - reactorModels.add(pomModel); - } - else { - throw new IllegalStateException("PomModel was null."); - } - - List moduleNames = pomModel.getModules(); - - moduleNames.stream().forEach(moduleName -> { - - String modulePathSegment = path == null ? moduleName : path + "/" + moduleName; - - allPomFiles.stream() - .filter(getResourcePredicate(baseDir, modulePathSegment)) - .map(Model::new) - .forEach(m -> recursivelyFindReactorModules(baseDir, modulePathSegment, reactorModels, allPomFiles, m) - .stream()); - }); - return reactorModels; - } - - @NotNull - private static Predicate getResourcePredicate(Path baseDir, String modulePathSegment) { - return resource -> { - Path pomPath = baseDir.resolve(modulePathSegment).resolve(POM_XML).toAbsolutePath().normalize(); - String modulePath = LinuxWindowsPathUnifier.unifiedPathString(pomPath); - Path resourcePath = ResourceUtil.getPath(resource).toAbsolutePath().normalize(); - return LinuxWindowsPathUnifier.pathEquals(resourcePath, modulePath); - }; - } - - public List sortModels(List reactorModels) { - List sortedModels = new ArrayList<>(); - Map gaToModelMap = reactorModels.stream().collect(Collectors.toMap(m -> { - return (m.getGroupId() == null ? (m.getParent() == null ? null : m.getParent().getGroupId()) - : m.getGroupId()) + ":" + m.getArtifactId(); - }, m -> m)); - - Map> dependsOn = new HashMap<>(); - - for (Model m : reactorModels) { - addToDependants(m, dependsOn, null); - if (hasParent(m)) { - String parentGa = m.getParent().getGroupId() + ":" + m.getParent().getArtifactId(); - Model parentModel = gaToModelMap.get(parentGa); - addToDependants(m, dependsOn, parentModel); - } - for (Dependency d : m.getDependencies()) { - String dependencyGa = getGav(d); - if (gaToModelMap.containsKey(dependencyGa)) { - Model dependencyModel = gaToModelMap.get(dependencyGa); - addToDependants(m, dependsOn, dependencyModel); - } - } - if (m.getBuild() != null && m.getBuild().getPluginsAsMap() != null - && !m.getBuild().getPluginsAsMap().isEmpty()) { - for (String pluginName : m.getBuild().getPluginsAsMap().keySet()) { - // TODO: find plugin dependencies - } - } - } - - ArrayList>> entries = new ArrayList<>(dependsOn.entrySet()); - - // sort entries by number of values - entries.stream().sorted((e1, e2) -> { - int compare = Integer.compare(e1.getValue().size(), e2.getValue().size()); - if (compare != 0) { - return compare; - } - else { - // with same number of dependencies order by dependant - if (e1.getValue().contains(e2.getKey())) { - return 1; - } - else if (e2.getValue().contains(e1.getKey())) { - return -1; - } - else { - // default is order as given by reactorModels - return Integer.compare(reactorModels.indexOf(e1.getKey()), reactorModels.indexOf(e2.getKey())); - } - } - }).forEach(e -> sortedModels.add(e.getKey())); - return sortedModels; - } - - private void addToDependants(Model model, Map> dependsOn, Model dependantModel) { - if (!dependsOn.containsKey(model)) { - dependsOn.put(model, new ArrayList<>()); - } - if (dependantModel != null) { - dependsOn.get(model).add(dependantModel); - } - } - - private boolean hasParent(Model m) { - return m.getParent() != null; - } - - private String getGav(Dependency d) { - return d.getGroupId() + ":" + d.getArtifactId(); - } - - private static boolean isSingleModuleProject(Model rootPomModel) { - return !rootPomModel.getPackaging().equals("pom"); - } - - public ParserContext createParserContext(Path baseDir, List resources) { - List sortedProjectsList = getSortedProjects(baseDir, resources); - ParserContext parserContext = new ParserContext(baseDir, resources, sortedProjectsList); - return parserContext; - } - - static class Model extends org.apache.maven.model.Model { - - private final Resource resource; - - private final org.apache.maven.model.Model delegate; - - public Model(Resource resource) { - this.resource = resource; - try { - this.delegate = XPP_3_READER.read(ResourceUtil.getInputStream(resource)); - this.delegate.setPomFile(resource.getFile()); - } - catch (IOException e) { - throw new RuntimeException(e); - } - catch (XmlPullParserException e) { - throw new RuntimeException(e); - } - } - - @Override - public String toString() { - return (delegate.getGroupId() == null ? delegate.getParent().getGroupId() : delegate.getGroupId()) + ":" - + delegate.getArtifactId(); - } - - public Resource getResource() { - return resource; - } - - @Override - public String getArtifactId() { - return delegate.getArtifactId(); - } - - @Override - public Build getBuild() { - return delegate.getBuild(); - } - - @Override - public String getChildProjectUrlInheritAppendPath() { - return delegate.getChildProjectUrlInheritAppendPath(); - } - - @Override - public CiManagement getCiManagement() { - return delegate.getCiManagement(); - } - - @Override - public List getContributors() { - return delegate.getContributors(); - } - - @Override - public String getDescription() { - return delegate.getDescription(); - } - - @Override - public List getDevelopers() { - return delegate.getDevelopers(); - } - - @Override - public String getGroupId() { - return delegate.getGroupId(); - } - - @Override - public String getInceptionYear() { - return delegate.getInceptionYear(); - } - - @Override - public IssueManagement getIssueManagement() { - return delegate.getIssueManagement(); - } - - @Override - public List getLicenses() { - return delegate.getLicenses(); - } - - @Override - public List getMailingLists() { - return delegate.getMailingLists(); - } - - @Override - public String getModelEncoding() { - return delegate.getModelEncoding(); - } - - @Override - public String getModelVersion() { - return delegate.getModelVersion(); - } - - @Override - public String getName() { - String name = delegate.getName(); - if (name == null) { - name = delegate.getArtifactId(); - } - return name; - } - - @Override - public Organization getOrganization() { - return delegate.getOrganization(); - } - - @Override - public String getPackaging() { - return delegate.getPackaging(); - } - - @Override - public Parent getParent() { - return delegate.getParent(); - } - - @Override - public Prerequisites getPrerequisites() { - return delegate.getPrerequisites(); - } - - @Override - public List getProfiles() { - return delegate.getProfiles(); - } - - @Override - public Scm getScm() { - return delegate.getScm(); - } - - @Override - public String getUrl() { - return delegate.getUrl(); - } - - @Override - public String getVersion() { - return delegate.getVersion(); - } - - @Override - public File getPomFile() { - return delegate.getPomFile(); - } - - @Override - public File getProjectDirectory() { - return delegate.getPomFile().toPath().getParent().toFile(); - } - - @Override - public String getId() { - return delegate.getId(); - } - - @Override - public List getDependencies() { - return delegate.getDependencies(); - } - - @Override - public DependencyManagement getDependencyManagement() { - return delegate.getDependencyManagement(); - } - - @Override - public DistributionManagement getDistributionManagement() { - return delegate.getDistributionManagement(); - } - - @Override - public InputLocation getLocation(Object key) { - return delegate.getLocation(key); - } - - @Override - public List getModules() { - return delegate.getModules(); - } - - @Override - public List getPluginRepositories() { - return delegate.getPluginRepositories(); - } - - @Override - public Properties getProperties() { - return delegate.getProperties(); - } - - @Override - public Reporting getReporting() { - return delegate.getReporting(); - } - - @Override - public Object getReports() { - return delegate.getReports(); - } - - @Override - public List getRepositories() { - return delegate.getRepositories(); - } - - } - } diff --git a/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/maven/MavenProjectFactory.java b/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/maven/MavenProjectFactory.java new file mode 100644 index 0000000..948a16b --- /dev/null +++ b/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/maven/MavenProjectFactory.java @@ -0,0 +1,50 @@ +/* + * 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.rewrite.parsers.maven; + +import org.jetbrains.annotations.NotNull; +import org.openrewrite.maven.utilities.MavenArtifactDownloader; +import org.springframework.core.io.Resource; +import org.springframework.rewrite.parsers.MavenProject; + +import java.nio.file.Path; +import java.util.List; + +/** + * @author Fabian Krüger + */ +public class MavenProjectFactory { + + private final MavenArtifactDownloader artifactDownloader; + + public MavenProjectFactory(MavenArtifactDownloader artifactDownloader) { + this.artifactDownloader = artifactDownloader; + } + + public List create(Path baseDir, List projectResources) { + List allPomFiles = MavenBuildFileFilter.filterBuildFiles(projectResources); + if (allPomFiles.isEmpty()) { + throw new IllegalArgumentException("The provided resources did not contain any 'pom.xml' file."); + } + return allPomFiles.stream().map(pf -> create(baseDir, pf, projectResources)).toList(); + } + + @NotNull + public MavenProject create(Path baseDir, Resource pomFile, List projectResources) { + return new MavenProject(baseDir, pomFile, artifactDownloader, projectResources); + } + +} diff --git a/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/maven/MavenProjectGraph.java b/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/maven/MavenProjectGraph.java new file mode 100644 index 0000000..33036ba --- /dev/null +++ b/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/maven/MavenProjectGraph.java @@ -0,0 +1,137 @@ +/* + * 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.rewrite.parsers.maven; + +import org.springframework.rewrite.parsers.MavenProject; +import org.springframework.rewrite.parsers.ProjectId; +import org.springframework.rewrite.utils.LinuxWindowsPathUnifier; + +import java.nio.file.Path; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Create directed acyclic graph from MavenProjects. + * + * @author Fabian Krüger + */ +public class MavenProjectGraph { + + private static final String POM_XML = "pom.xml"; + + private Map gaToMavenProjectMap = new HashMap<>(); + + /** + * Create a Maven module dependency graph starting from pom.xml in {@code baseDir}. + * The dependency and dependants for all {@MavenModule}s are added when they are part + * of the build. + */ + public Map> from(Path baseDir, List allMavenProjects) { + // build map of projectId -> MavenProject + // this is used to decide if dependencies come from the project + initGaToMavenProjectMap(allMavenProjects); + MavenProject rootProject = findRootProject(baseDir, allMavenProjects); + // the dag with child/parent dependencies + Map> dag = new HashMap<>(); + buildDependencyGraph(rootProject, allMavenProjects, dag); + enrichGraphWithDependencies(dag); + return dag; + } + + private void enrichGraphWithDependencies(Map> dag) { + dag.keySet().stream().forEach(curProject -> { + List dependencyProjects = new ArrayList<>(); + curProject.getBuildFile().getDependencies().forEach(d -> { + ProjectId projectId = new ProjectId(d.getGroupId(), d.getArtifactId()); + if (gaToMavenProjectMap.containsKey(projectId)) { + MavenProject dependencyMavenProject = gaToMavenProjectMap.get(projectId); + dependencyProjects.add(dependencyMavenProject); + } + }); + curProject.setDependencyProjects(dependencyProjects); + }); + + dag.keySet().stream().forEach(curProject -> { + Set dependentProjects = dag.get(curProject); + curProject.getBuildFile().getDependencies().stream().forEach(dependency -> { + ProjectId projectId = new ProjectId(dependency.getGroupId(), dependency.getArtifactId()); + if (gaToMavenProjectMap.containsKey(projectId)) { + MavenProject dependantProject = gaToMavenProjectMap.get(projectId); + if (dag.containsKey(dependantProject)) { + dependentProjects.add(dependantProject); + } + } + }); + }); + } + + private void initGaToMavenProjectMap(List allPomFiles) { + gaToMavenProjectMap = allPomFiles.stream() + .collect(Collectors.toMap(mp -> new ProjectId(mp.getGroupId(), mp.getArtifactId()), mp -> mp)); + } + + private void buildDependencyGraph(MavenProject currentProject, List reactorProjects, + Map> dag) { + if (isSingleModuleProject(reactorProjects)) { + logDependentProject(currentProject, dag, null); + return; + } + + if (isMultiModuleProject(currentProject)) { + currentProject.getBuildFile().getModules().stream().map(moduleName -> { + Path modulePath = currentProject.getModulePath().resolve(moduleName).normalize(); + MavenProject mavenProject = reactorProjects.stream() + .filter(p -> LinuxWindowsPathUnifier.pathEquals(p.getModulePath(), modulePath)) + .findFirst() + .get(); + return mavenProject; + }).forEach(childProject -> { + // add dependent project + logDependentProject(childProject, dag, currentProject); + buildDependencyGraph(childProject, reactorProjects, dag); + }); + } + dag.computeIfAbsent(currentProject, __ -> new HashSet<>()); + } + + private static boolean isMultiModuleProject(MavenProject currentProject) { + return hasPomPackaging(currentProject); + } + + private static boolean isSingleModuleProject(List reactorProjects) { + return reactorProjects.size() == 1; + } + + private static void logDependentProject(MavenProject dependingProject, Map> dag, + MavenProject dependantProject) { + Set mavenProjects = dag.computeIfAbsent(dependingProject, __ -> new HashSet<>()); + if (dependantProject != null) { + mavenProjects.add(dependantProject); + } + } + + private MavenProject findRootProject(Path baseDir, List reactorProjects) { + return reactorProjects.stream() + .filter(p -> LinuxWindowsPathUnifier.pathEquals(p.getBuildFile().getPath(), baseDir.resolve(POM_XML))) + .findFirst() + .get(); + } + + private static boolean hasPomPackaging(MavenProject curMavenProject) { + return "pom".equals(curMavenProject.getBuildFile().getPackaging()); + } + +} diff --git a/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/maven/MavenProjectSorter.java b/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/maven/MavenProjectSorter.java new file mode 100644 index 0000000..2c09c96 --- /dev/null +++ b/spring-rewrite-commons-launcher/src/main/java/org/springframework/rewrite/parsers/maven/MavenProjectSorter.java @@ -0,0 +1,91 @@ +/* + * 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.rewrite.parsers.maven; + +import org.springframework.rewrite.parsers.MavenProject; + +import java.nio.file.Path; +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author Fabian Krüger + */ +public class MavenProjectSorter { + + private static final String POM_XML = "pom.xml"; + + private final MavenProjectGraph mavenProjectGraph; + + public MavenProjectSorter(MavenProjectGraph mavenProjectGraph) { + this.mavenProjectGraph = mavenProjectGraph; + } + + /** + * Filter {@link MavenProject}s that belong to a reactor build. Create ADG of the + * relevant projects and their dependencies to other {@link MavenProject}s. Sort the + * ADG and return the ordered list of {@link MavenProject}s. + */ + public List sort(Path baseDir, List allPomFiles) { + Map> graph = mavenProjectGraph.from(baseDir, allPomFiles); + List buildOrder = new ArrayList<>(); + + Map> dependingProjects = new HashMap<>(); + graph.keySet().forEach(mavenProject -> { + graph.entrySet() + .stream() + .peek(e -> dependingProjects.computeIfAbsent(mavenProject, __ -> new HashSet<>())) + .filter(e -> e.getValue().contains(mavenProject)) + .forEach(e -> { + dependingProjects.get(mavenProject).add(e.getKey()); + }); + }); + + Map inDegree = dependingProjects.keySet() + .stream() + .collect(Collectors.toMap(k -> k, __ -> 0)); + dependingProjects.entrySet() + .stream() + .flatMap(e -> e.getValue().stream()) + .forEach(d -> inDegree.put(d, inDegree.get(d) + 1)); + + Queue sources = new PriorityQueue<>(Comparator.comparing(p -> p.getBuildFile().getArtifactId())); + for (Map.Entry entry : inDegree.entrySet()) { + if (entry.getValue() == 0) { + sources.add(entry.getKey()); + } + } + + while (!sources.isEmpty()) { + MavenProject project = sources.poll(); + buildOrder.add(project); + List mavenProjects1 = new ArrayList<>(dependingProjects.get(project)); + for (MavenProject child : mavenProjects1) { + inDegree.put(child, inDegree.get(child) - 1); + if (inDegree.get(child) == 0) { + sources.add(child); + } + } + } + + if (buildOrder.size() != inDegree.size()) { + throw new RuntimeException("Cycle detected Maven projects"); + } + + return buildOrder; + } + +} diff --git a/spring-rewrite-commons-launcher/src/test/java/org/openrewrite/JavaParserTest.java b/spring-rewrite-commons-launcher/src/test/java/org/openrewrite/JavaParserTest.java deleted file mode 100644 index fb70f85..0000000 --- a/spring-rewrite-commons-launcher/src/test/java/org/openrewrite/JavaParserTest.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * 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.openrewrite; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.openrewrite.java.JavaParser; -import org.openrewrite.java.internal.JavaTypeCache; -import org.openrewrite.java.internal.TypesInUse; -import org.openrewrite.java.marker.JavaSourceSet; -import org.openrewrite.java.tree.J; -import org.openrewrite.java.tree.JavaType; - -import java.nio.file.Path; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Fabian Krüger - */ -public class JavaParserTest { - - @Test - @DisplayName("shouldHaveTypeInUse") - @Disabled("Examination the JavaParser") - void shouldHaveTypeInUse() { - - String localM2Repo = Path.of(System.getProperty("user.home")).resolve(".m2/repository").toString(); - List classpath = List.of( - Path.of(localM2Repo - + "/org/springframework/boot/spring-boot-starter/3.1.1/spring-boot-starter-3.1.1.jar"), - Path.of(localM2Repo + "/org/springframework/boot/spring-boot/3.1.1/spring-boot-3.1.1.jar"), - Path.of(localM2Repo + "/org/springframework/spring-context/6.0.10/spring-context-6.0.10.jar"), - Path.of(localM2Repo + "/org/springframework/spring-aop/6.0.10/spring-aop-6.0.10.jar"), - Path.of(localM2Repo + "/org/springframework/spring-beans/6.0.10/spring-beans-6.0.10.jar"), - Path.of(localM2Repo + "/org/springframework/spring-expression/6.0.10/spring-expression-6.0.10.jar"), - Path.of(localM2Repo - + "/org/springframework/boot/spring-boot-autoconfigure/3.1.1/spring-boot-autoconfigure-3.1.1.jar"), - Path.of(localM2Repo - + "/org/springframework/boot/spring-boot-starter-logging/3.1.1/spring-boot-starter-logging-3.1.1.jar"), - Path.of(localM2Repo + "/ch/qos/logback/logback-classic/1.4.8/logback-classic-1.4.8.jar"), - Path.of(localM2Repo + "/ch/qos/logback/logback-core/1.4.8/logback-core-1.4.8.jar"), - Path.of(localM2Repo + "/org/slf4j/slf4j-api/2.0.7/slf4j-api-2.0.7.jar"), - Path.of(localM2Repo + "/org/apache/logging/log4j/log4j-to-slf4j/2.20.0/log4j-to-slf4j-2.20.0.jar"), - Path.of(localM2Repo + "/org/apache/logging/log4j/log4j-api/2.20.0/log4j-api-2.20.0.jar"), - Path.of(localM2Repo + "/org/slf4j/jul-to-slf4j/2.0.7/jul-to-slf4j-2.0.7.jar"), - Path.of(localM2Repo - + "/jakarta/annotation/jakarta.annotation-api/2.1.1/jakarta.annotation-api-2.1.1.jar"), - Path.of(localM2Repo + "/org/springframework/spring-core/6.0.10/spring-core-6.0.10.jar"), - Path.of(localM2Repo + "/org/springframework/spring-jcl/6.0.10/spring-jcl-6.0.10.jar"), - Path.of(localM2Repo + "/org/yaml/snakeyaml/1.33/snakeyaml-1.33.jar")); - JavaTypeCache javaTypeCache = new JavaTypeCache(); - SourceFile sourceFile = JavaParser.fromJavaVersion() - .classpath(classpath) - .typeCache(javaTypeCache) - .build() - .parse(""" - package com.example; - import org.springframework.boot.SpringApplication; - import org.springframework.boot.autoconfigure.SpringBootApplication; - - @SpringBootApplication - public class MyMain { - public static void main(String[] args){ - SpringApplication.run(MyMain.class, args); - } - } - """) - .toList() - .get(0); - - J.CompilationUnit compilationUnit = (J.CompilationUnit) sourceFile; - List typesInUse = compilationUnit.getTypesInUse() - .getTypesInUse() - .stream() - .map(s -> s.toString()) - .toList(); - assertThat(typesInUse).contains("org.springframework.boot.SpringApplication", - "org.springframework.boot.SpringApplication", "com.example.MyMain"); - JavaSourceSet main = JavaSourceSet.build("main", classpath, javaTypeCache, true); - List typesOnClasspath = main.getClasspath() - .stream() - .map(JavaType.FullyQualified::getFullyQualifiedName) - .toList(); - assertThat(typesOnClasspath).doesNotContain("com.example.MyMain"); // By design - - javaTypeCache.put("com.example.MyMain", sourceFile); - - main = JavaSourceSet.build("main", classpath, javaTypeCache, true); - List mainCp = main.getClasspath(); - TypesInUse typesInUseBefore = compilationUnit.getTypesInUse(); - - typesInUse = typesInUseBefore.getTypesInUse().stream().map(s -> s.toString()).toList(); - assertThat(typesInUse).contains("org.springframework.boot.SpringApplication", - "org.springframework.boot.SpringApplication", "com.example.MyMain"); - compilationUnit.getClasses().stream().map(c -> c.getType()).forEach(mainCp::add); - - main = main.withClasspath(mainCp); - assertThat(main.getClasspath().stream().map(JavaType.FullyQualified::getFullyQualifiedName).toList()) - .contains("org.springframework.boot.SpringApplication"); - assertThat(main.getClasspath().stream().map(JavaType.FullyQualified::getFullyQualifiedName).toList()) - .contains("com.example.MyMain"); - } - -} diff --git a/spring-rewrite-commons-launcher/src/test/java/org/openrewrite/maven/CalculateClasspathTest.java b/spring-rewrite-commons-launcher/src/test/java/org/openrewrite/maven/CalculateClasspathTest.java index 4ac0f2a..fe066f8 100644 --- a/spring-rewrite-commons-launcher/src/test/java/org/openrewrite/maven/CalculateClasspathTest.java +++ b/spring-rewrite-commons-launcher/src/test/java/org/openrewrite/maven/CalculateClasspathTest.java @@ -101,7 +101,7 @@ public class CalculateClasspathTest { } """; - Path baseDir = LinuxWindowsPathUnifier.unifiedPath(tmpDir.resolve("/example-1").toAbsolutePath().normalize()); + Path baseDir = LinuxWindowsPathUnifier.unifiedPath(tmpDir.resolve("example-1").toAbsolutePath().normalize()); List resources = List.of(new DummyResource(baseDir.resolve("pom.xml"), pom), new DummyResource(baseDir.resolve("src/main/java/com/example/MainClass.java"), mainClass), new DummyResource(baseDir.resolve("src/test/java/com/example/TestClass.java"), testClass)); diff --git a/spring-rewrite-commons-launcher/src/test/java/org/springframework/rewrite/parsers/RewriteProjectParserTest.java b/spring-rewrite-commons-launcher/src/test/java/org/springframework/rewrite/parsers/RewriteProjectParserTest.java index 51f44c0..680e5a9 100644 --- a/spring-rewrite-commons-launcher/src/test/java/org/springframework/rewrite/parsers/RewriteProjectParserTest.java +++ b/spring-rewrite-commons-launcher/src/test/java/org/springframework/rewrite/parsers/RewriteProjectParserTest.java @@ -25,7 +25,6 @@ import org.openrewrite.Parser; import org.openrewrite.SourceFile; import org.openrewrite.tree.ParsingEventListener; import org.openrewrite.tree.ParsingExecutionContextView; -import org.sonatype.plexus.components.cipher.PlexusCipherException; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.context.ApplicationEventPublisher; import org.springframework.core.io.DefaultResourceLoader; @@ -88,7 +87,7 @@ class RewriteProjectParserTest { @Test @DisplayName("Parse simple Maven project") - void parseSimpleMavenProject(@TempDir Path tempDir) throws PlexusCipherException { + void parseSimpleMavenProject(@TempDir Path tempDir) { Path basePath = tempDir; SpringRewriteProperties springRewriteProperties = new SpringRewriteProperties(); ExecutionContext executionContext = new InMemoryExecutionContext(t -> { @@ -98,13 +97,16 @@ class RewriteProjectParserTest { ProjectMetadata projectMetadata = new ProjectMetadata(); MavenSettingsInitializer mavenSettingsInitializer = new MavenSettingsInitializer(executionContext, projectMetadata); + RewriteMavenArtifactDownloader artifactDownloader = mock(RewriteMavenArtifactDownloader.class); + MavenProjectGraph projectCollector = new MavenProjectGraph(); + MavenProjectFactory mavenProjectFactory = new MavenProjectFactory(artifactDownloader); RewriteProjectParser projectParser = new RewriteProjectParser( new ProvenanceMarkerFactory(new MavenProvenanceMarkerFactory()), new BuildFileParser(mavenSettingsInitializer), new SourceFileParser(mavenModuleParser), new StyleDetector(), springRewriteProperties, mock(ParsingEventListener.class), mock(ApplicationEventPublisher.class), new ScanScope(), mock(ConfigurableListableBeanFactory.class), new ProjectScanner(new DefaultResourceLoader(), springRewriteProperties), executionContext, - new MavenProjectAnalyzer(mock(RewriteMavenArtifactDownloader.class))); + new MavenProjectAnalyzer(new MavenProjectSorter(projectCollector), mavenProjectFactory)); List parsedFiles = new ArrayList<>(); ParsingExecutionContextView.view(executionContext).setParsingListener(new ParsingEventListener() { diff --git a/spring-rewrite-commons-launcher/src/test/java/org/springframework/rewrite/parsers/maven/MavenProjectGraphTest.java b/spring-rewrite-commons-launcher/src/test/java/org/springframework/rewrite/parsers/maven/MavenProjectGraphTest.java new file mode 100644 index 0000000..c128aa2 --- /dev/null +++ b/spring-rewrite-commons-launcher/src/test/java/org/springframework/rewrite/parsers/maven/MavenProjectGraphTest.java @@ -0,0 +1,346 @@ +/* + * 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.rewrite.parsers.maven; + +import com.google.common.base.Supplier; +import org.assertj.core.api.AbstractAssert; +import org.assertj.core.api.SoftAssertions; +import org.intellij.lang.annotations.Language; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; +import org.openrewrite.maven.cache.LocalMavenArtifactCache; +import org.openrewrite.maven.utilities.MavenArtifactDownloader; +import org.springframework.core.io.Resource; +import org.springframework.rewrite.parsers.MavenProject; +import org.springframework.rewrite.test.util.DummyResource; + +import java.nio.file.Path; +import java.util.*; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.rewrite.parsers.maven.MavenProjectGraphTest.MavenProjectBuilder.none; + +/** + * @author Fabian Krüger + */ +class MavenProjectGraphTest { + + private MavenProjectGraph sut = new MavenProjectGraph(); + + private static MavenArtifactDownloader artifactDownloader = new RewriteMavenArtifactDownloader( + new LocalMavenArtifactCache(Path.of(System.getProperty("user.home")).resolve(".m2/repository")), null, + e -> { + throw new RuntimeException(e); + }); + + private static MavenProjectFactory projectFactory = new MavenProjectFactory(artifactDownloader); + + @Test + @DisplayName("single module") + void singleModule() { + @Language("xml") + String pomCode = """ + + + 4.0.0 + com.acme + 0.1.0-SNAPSHOT + example + + """; + Path baseDir = Path.of("./target").toAbsolutePath().normalize(); + Resource pomResource = new DummyResource(baseDir.resolve("pom.xml"), pomCode); + MavenProject mavenProject = projectFactory.create(baseDir, pomResource, List.of(pomResource)); + List allMavenProjects = List.of(mavenProject); + + Map> mavenProjectSetMap = sut.from(baseDir, allMavenProjects); + + assertThat(mavenProjectSetMap).hasSize(1); + + } + + @Test + @DisplayName("Dangling pom should will be collected") + void danglingPomShouldBeIgnored() { + + @Language("xml") + String parentPom = """ + + + 4.0.0 + com.acme + parent + 0.1.0-SNAPSHOT + pom + + example + + + """; + + @Language("xml") + String modulePom = """ + + + 4.0.0 + + com.acme + parent + 0.1.0-SNAPSHOT + + example + + + com.acme + dangling + 0.1.0-SNAPSHOT + + + + """; + + @Language("xml") + String danglingPom = """ + + + 4.0.0 + com.acme + dangling + 0.1.0-SNAPSHOT + + """; + + Path baseDir = Path.of(".").toAbsolutePath(); + MavenProjectBuilder.buildMavenProject(baseDir) + .withResource("pom.xml", parentPom) + .withResource("dangling/pom.xml", danglingPom) + .withResource("example/pom.xml", modulePom) + .afterSort() + .assertDependencies(parentPom, none()) + .assertDependencies(modulePom, parentPom) + .assertRemoved(danglingPom) + .verify(); + } + + @Test + @DisplayName("Multi module with dependant projects") + void multiModuleWithDependantProjects() { + // Modules declared in order a,b,c + @Language("xml") + String parentPom = """ + + + 4.0.0 + com.acme + parent + 0.1.0-SNAPSHOT + pom + + module-a + module-b + module-c + + + """; + + // Module A depends on C, so C must be built first effectively changing the order + // in + @Language("xml") + String moduleAPom = """ + + + 4.0.0 + + com.acme + parent + 0.1.0-SNAPSHOT + + module-a + + + com.acme + module-c + 0.1.0-SNAPSHOT + + + + """; + + @Language("xml") + String moduleBPom = """ + + + 4.0.0 + + com.acme + parent + 0.1.0-SNAPSHOT + + module-b + + """; + + // C depends on B + @Language("xml") + String moduleCPom = """ + + + 4.0.0 + + com.acme + parent + 0.1.0-SNAPSHOT + + module-c + + + com.acme + module-b + 0.1.0-SNAPSHOT + + + + """; + + Path baseDir = Path.of(".").toAbsolutePath(); + MavenProjectBuilder.buildMavenProject(baseDir) + .withResource("pom.xml", parentPom) + .withResource("module-a/pom.xml", moduleAPom) + .withResource("module-b/pom.xml", moduleBPom) + .withResource("module-c/pom.xml", moduleCPom) + .afterSort() + .assertDependencies(parentPom, none()) + .assertDependencies(moduleAPom, parentPom, moduleCPom) + .assertDependencies(moduleBPom, parentPom) + .assertDependencies(moduleCPom, parentPom, moduleBPom) + .verify(); + } + + static class MavenProjectBuilder { + + private final MavenProjectGraph sut = new MavenProjectGraph(); + + private static MavenArtifactDownloader artifactDownloader = new RewriteMavenArtifactDownloader( + new LocalMavenArtifactCache(Path.of(System.getProperty("user.home")).resolve(".m2/repository")), null, + e -> { + throw new RuntimeException(e); + }); + + ; + private static MavenProjectFactory projectFactory = new MavenProjectFactory(artifactDownloader); + + ; + + private final Path baseDir; + + private List resources = new ArrayList<>(); + + private List mavenProjects = new ArrayList<>(); + + private List assertions = new ArrayList<>(); + + private Map> dependencyGraph; + + private List> listAssertSupplier; + + private SoftAssertions softAssertions = new SoftAssertions(); + + private Set assertedProjects = new HashSet<>(); + + private Set removedProjects = new HashSet<>(); + + private Set selectedProjects = new HashSet<>(); + + public MavenProjectBuilder(Path baseDir) { + this.baseDir = baseDir; + } + + public static MavenProjectBuilder buildMavenProject(Path baseDir) { + return new MavenProjectBuilder(baseDir); + } + + public static List none() { + return List.of(); + } + + public MavenProjectBuilder withResource(String path, String content) { + Resource r = new DummyResource(baseDir.resolve(path), content); + this.resources.add(r); + return this; + } + + public MavenProjectBuilder afterSort() { + mavenProjects = MavenProjectGraphTest.projectFactory.create(baseDir, resources); + dependencyGraph = sut.from(baseDir, mavenProjects); + return this; + } + + public MavenProjectBuilder assertDependencies(String pomContent, String... dependantPomContents) { + MavenProject moduleProject = findMavenProject(pomContent); + this.assertedProjects.add(moduleProject); + List dependantProjects = getDependantProjects(dependantPomContents); + // softAssertions. + assertThat(dependencyGraph).containsKey(moduleProject); + MavenProject[] dependantProjectsArray = dependantProjects.toArray(MavenProject[]::new); + Set actualDependantProjects = dependencyGraph.get(moduleProject); + // softAssertions. + assertThat(actualDependantProjects).containsExactlyInAnyOrder(dependantProjectsArray); + return this; + } + + private List getDependantProjects(String... dependantPomContents) { + List contents = Arrays.asList(dependantPomContents); + return mavenProjects.stream().filter(p -> contents.contains(p.getBuildFile().getContent())).toList(); + } + + private MavenProject findMavenProject(String pomContent) { + return mavenProjects.stream() + .filter(p -> pomContent.equals(p.getBuildFile().getContent())) + .findFirst() + .get(); + } + + public MavenProjectBuilder assertDependencies(String parentPom, List empty) { + assertDependencies(parentPom); + return this; + } + + public void verify() { + softAssertions.assertThat(assertedProjects) + .containsExactlyInAnyOrder(dependencyGraph.keySet().toArray(MavenProject[]::new)); + softAssertions.assertAll(); + } + + public MavenProjectBuilder assertRemoved(String danglingPom) { + MavenProject mavenProject = findMavenProject(danglingPom); + this.removedProjects.add(mavenProject); + this.selectedProjects.remove(mavenProject); + softAssertions.assertThat(selectedProjects).doesNotContain(mavenProject); + return this; + } + + } + +} \ No newline at end of file diff --git a/spring-rewrite-commons-launcher/src/test/java/org/springframework/rewrite/parsers/maven/MavenProjectAnalyzerTest.java b/spring-rewrite-commons-launcher/src/test/java/org/springframework/rewrite/parsers/maven/MavenProjectSorterTest.java similarity index 89% rename from spring-rewrite-commons-launcher/src/test/java/org/springframework/rewrite/parsers/maven/MavenProjectAnalyzerTest.java rename to spring-rewrite-commons-launcher/src/test/java/org/springframework/rewrite/parsers/maven/MavenProjectSorterTest.java index c1e5182..3104292 100644 --- a/spring-rewrite-commons-launcher/src/test/java/org/springframework/rewrite/parsers/maven/MavenProjectAnalyzerTest.java +++ b/spring-rewrite-commons-launcher/src/test/java/org/springframework/rewrite/parsers/maven/MavenProjectSorterTest.java @@ -42,18 +42,552 @@ import static org.assertj.core.api.Assertions.assertThat; /** * @author Fabian Krüger */ -class MavenProjectAnalyzerTest { +class MavenProjectSorterTest { - private MavenProjectAnalyzer sut; + private MavenProjectSorter sut; + + private MavenProjectFactory mavenProjectFactory; @BeforeEach void beforeEach() { - MavenArtifactDownloader rewriteMavenArtifactDownloader = Mockito.mock(RewriteMavenArtifactDownloader.class); - sut = new MavenProjectAnalyzer(rewriteMavenArtifactDownloader); + createMavenProjectSorter(); } + /** + * The simplest possible Maven project. + */ + @Test + @DisplayName("projectWithSinglePom") + void projectWithSinglePom() { + @Language("xml") + String singlePom = """ + + + 4.0.0 + com.acme + 0.1.0-SNAPSHOT + example + + """; + + List resources = List.of(new DummyResource(Path.of("pom.xml"), singlePom)); + Path baseDir = Path.of(".").toAbsolutePath().normalize(); + List mavenProjects = mavenProjectFactory.create(baseDir, resources); + List sortedProjects = sut.sort(baseDir, mavenProjects); + assertThat(sortedProjects).hasSize(1); + } + + /** + * A simple reactor build with one parent and one module pom. + */ + @Test + @DisplayName("reactorBuild") + void getSortedProjectsWithMultiModule() { + @Language("xml") + String parentPom = """ + + + 4.0.0 + com.acme + parent + 0.1.0-SNAPSHOT + pom + + example + + + """; + + @Language("xml") + String modulePom = """ + + + 4.0.0 + + com.acme + parent + 0.1.0-SNAPSHOT + + example + + """; + + Path baseDir = Path.of(".").toAbsolutePath().normalize(); + Path exampleModuleDir = baseDir.resolve("example"); + + // @formatter:off + List resources = List.of( + new DummyResource(baseDir.resolve("pom.xml"), parentPom), + new DummyResource(exampleModuleDir.resolve("pom.xml"), modulePom) + ); + // @formatter:on + List mavenProjects = mavenProjectFactory.create(baseDir, resources); + List sortedProjects = sut.sort(baseDir, mavenProjects); + + assertThat(sortedProjects).hasSize(2); + + MavenProject parentProject = sortedProjects.get(0); + MavenProject exampleProject = sortedProjects.get(1); + + assertThat(LinuxWindowsPathUnifier.pathEquals(baseDir.normalize(), parentProject.getBasedir())).isTrue(); + assertThat(LinuxWindowsPathUnifier.pathEquals(exampleProject.getBasedir(), + Path.of(".").resolve("example").toAbsolutePath().normalize())); + } + + private void createMavenProjectSorter() { + MavenArtifactDownloader rewriteMavenArtifactDownloader = Mockito.mock(RewriteMavenArtifactDownloader.class); + MavenProjectGraph mavenRecatorProjectCollector = new MavenProjectGraph(); + mavenProjectFactory = new MavenProjectFactory(rewriteMavenArtifactDownloader); + sut = new MavenProjectSorter(mavenRecatorProjectCollector); + } + + /** + * Two pom files building a rector build should be returned. The dangling pom not + * belonging to the reactor build defined through parent pom will be ignored. + */ + @Test + @DisplayName("reactorBuildWithDanglingPom") + void reactorBuildWithDanglingPom() { + @Language("xml") + String parentPom = """ + + + 4.0.0 + com.acme + parent + 0.1.0-SNAPSHOT + pom + + example + + + """; + + @Language("xml") + String modulePom = """ + + + 4.0.0 + + com.acme + parent + 0.1.0-SNAPSHOT + + example + + """; + + @Language("xml") + String danglingPom = """ + + + 4.0.0 + com.acme + dangling + 0.1.0-SNAPSHOT + + """; + + Path baseDir = Path.of(".").toAbsolutePath(); + List resources = List.of(new DummyResource(Path.of("pom.xml"), parentPom), + new DummyResource(Path.of("example/pom.xml"), modulePom), + new DummyResource(Path.of("dangling/pom.xml"), danglingPom)); + + List mavenProjects = mavenProjectFactory.create(baseDir, resources); + List sortedProjects = sut.sort(baseDir, mavenProjects); + + assertThat(sortedProjects).hasSize(2); + + String parentPomPath = baseDir.normalize().toString(); + assertThat(LinuxWindowsPathUnifier.pathEquals(sortedProjects.get(0).getBasedir(), parentPomPath)); + + String modulePomPath = Path.of(".").resolve("example").toAbsolutePath().normalize().toString(); + assertThat(LinuxWindowsPathUnifier.pathEquals(sortedProjects.get(1).getBasedir(), modulePomPath)); + } + + /** + * A project with three Maven pom files. Two of them build a reactor. The third is not + * part of the reactor but a dependency of the child module in the reactor build. + */ + @Test + @DisplayName("reactorBuildWithDanglingPomWhichAReactorModuleDependsOn") + void reactorBuildWithDanglingPomWhichAReactorModuleDependsOn() { + @Language("xml") + String parentPom = """ + + + 4.0.0 + com.acme + parent + 0.1.0-SNAPSHOT + pom + + example + + + """; + + @Language("xml") + String modulePom = """ + + + 4.0.0 + + com.acme + parent + 0.1.0-SNAPSHOT + + example + + + com.acme + dangling + 0.1.0-SNAPSHOT + + + + """; + + @Language("xml") + String danglingPom = """ + + + 4.0.0 + com.acme + dangling + 0.1.0-SNAPSHOT + + """; + + List resources = List.of(new DummyResource(Path.of("pom.xml"), parentPom), + new DummyResource(Path.of("example/pom.xml"), modulePom), + new DummyResource(Path.of("dangling/pom.xml"), danglingPom)); + + Path baseDir = Path.of(".").toAbsolutePath(); + List mavenProjects = mavenProjectFactory.create(baseDir, resources); + List sortedProjects = sut.sort(baseDir, mavenProjects); + + assertThat(sortedProjects).hasSize(2); + + String parentPomPath = LinuxWindowsPathUnifier.unifiedPathString(Path.of(".").toAbsolutePath().normalize()); + assertThat(LinuxWindowsPathUnifier.pathEquals(sortedProjects.get(0).getBasedir(), parentPomPath)).isTrue(); + + String modulePomPath = LinuxWindowsPathUnifier + .unifiedPathString(Path.of(".").resolve("example").toAbsolutePath().normalize()); + assertThat(LinuxWindowsPathUnifier.pathEquals(sortedProjects.get(1).getBasedir(), modulePomPath)).isTrue(); + } + + /** + * A reactor project with four modules provided in "wrong" order. The returned order + * differs and reflects the order of the modules in reactor build. + */ + @Test + @DisplayName("theReactorBuildOrderIsReturned") + void theReactorBuildOrderIsReturned() { + @Language("xml") + String parentPom = """ + + + 4.0.0 + com.acme + parent + pom + 0.1.0-SNAPSHOT + + module-a + module-b + module-c + + + """; + + @Language("xml") + String moduleAPom = """ + + + 4.0.0 + + com.acme + parent + 0.1.0-SNAPSHOT + + module-a + + """; + + @Language("xml") + String moduleBPom = """ + + + 4.0.0 + + com.acme + parent + 0.1.0-SNAPSHOT + + module-b + + """; + + @Language("xml") + String moduleCPom = """ + + + 4.0.0 + + com.acme + parent + 0.1.0-SNAPSHOT + + module-c + + """; + + // Provided unordered + List resources = List.of(new DummyResource(Path.of("module-b/pom.xml"), moduleBPom), + new DummyResource(Path.of("module-a/pom.xml"), moduleAPom), + new DummyResource(Path.of("module-c/pom.xml"), moduleCPom), + new DummyResource(Path.of("pom.xml"), parentPom)); + + Path baseDir = Path.of(".").toAbsolutePath(); + List mavenProjects = mavenProjectFactory.create(baseDir, resources); + List sortedProjects = sut.sort(baseDir, mavenProjects); + + // Returned ordered + assertThat(sortedProjects).hasSize(4); + assertThat(sortedProjects.get(0).getModuleDir().toString()).isEqualTo(""); + assertThat(sortedProjects.get(1).getModuleDir().toString()).isEqualTo("module-a"); + assertThat(sortedProjects.get(2).getModuleDir().toString()).isEqualTo("module-b"); + assertThat(sortedProjects.get(3).getModuleDir().toString()).isEqualTo("module-c"); + } + + /** + * Provided unordered list of resources Order in modules is not correct Order is + * defined by dependencies + */ + @Test + @DisplayName("moreComplex") + void moreComplex() { + + // Modules declared in order a,b,c + @Language("xml") + String parentPom = """ + + + 4.0.0 + com.acme + parent + 0.1.0-SNAPSHOT + pom + + module-a + module-b + module-c + + + """; + + // Module A depends on C, so C must be built first effectively changing the order + // in + @Language("xml") + String moduleAPom = """ + + + 4.0.0 + + com.acme + parent + 0.1.0-SNAPSHOT + + module-a + + + com.acme + module-c + 0.1.0-SNAPSHOT + + + + """; + + @Language("xml") + String moduleBPom = """ + + + 4.0.0 + + com.acme + parent + 0.1.0-SNAPSHOT + + module-b + + """; + + // C depends on B + @Language("xml") + String moduleCPom = """ + + + 4.0.0 + + com.acme + parent + 0.1.0-SNAPSHOT + + module-c + + + com.acme + module-b + 0.1.0-SNAPSHOT + + + + """; + + Path baseDir = Path.of(".").toAbsolutePath(); + List resources = List.of(new DummyResource(baseDir.resolve("module-b/pom.xml"), moduleBPom), + new DummyResource(baseDir.resolve("module-a/pom.xml"), moduleAPom), + new DummyResource(baseDir.resolve("module-c/pom.xml"), moduleCPom), + new DummyResource(baseDir.resolve("pom.xml"), parentPom)); + + // Provided unordered + List mavenProjects = mavenProjectFactory.create(baseDir, resources); + List sortedProjects = sut.sort(baseDir, mavenProjects); + + // Expected order is parent, module-b, module-c, module-a + assertThat(sortedProjects).hasSize(4); + + assertThat(sortedProjects.get(0).getModuleDir().toString()).isEqualTo(""); + assertThat(sortedProjects.get(1).getModuleDir().toString()).isEqualTo("module-b"); + assertThat(sortedProjects.get(2).getModuleDir().toString()).isEqualTo("module-c"); + assertThat(sortedProjects.get(3).getModuleDir().toString()).isEqualTo("module-a"); + } + + @Test + @DisplayName("sortModels") + void sortModels() { + + // Modules declared in order a,b,c + @Language("xml") + String parentPom = """ + + + 4.0.0 + com.acme + parent + 0.1.0-SNAPSHOT + pom + + module-a + module-b + module-c + + + """; + + // Module A depends on C, so C must be built first effectively changing the order + // in + @Language("xml") + String moduleAPom = """ + + + 4.0.0 + + com.acme + parent + 0.1.0-SNAPSHOT + + module-a + + + com.acme + module-c + 0.1.0-SNAPSHOT + + + + """; + + @Language("xml") + String moduleBPom = """ + + + 4.0.0 + + com.acme + parent + 0.1.0-SNAPSHOT + + module-b + + """; + + // C depends on B + @Language("xml") + String moduleCPom = """ + + + 4.0.0 + + com.acme + parent + 0.1.0-SNAPSHOT + + module-c + + + com.acme + module-b + 0.1.0-SNAPSHOT + + + + """; + + // Provided unordered + List resources = List.of(new DummyResource(Path.of("module-b/pom.xml"), moduleBPom), + new DummyResource(Path.of("module-a/pom.xml"), moduleAPom), + new DummyResource(Path.of("module-c/pom.xml"), moduleCPom), + new DummyResource(Path.of("pom.xml"), parentPom)); + + Path baseDir = Path.of(".").toAbsolutePath(); + List mavenProjects = mavenProjectFactory.create(baseDir, resources); + List sorted = sut.sort(baseDir, mavenProjects); + + // Expected order is parent, module-b, module-c, module-a + assertThat(sorted.get(0).getArtifactId()).isEqualTo("parent"); + assertThat(sorted.get(1).getArtifactId()).isEqualTo("module-b"); + assertThat(sorted.get(2).getArtifactId()).isEqualTo("module-c"); + assertThat(sorted.get(3).getArtifactId()).isEqualTo("module-a"); + } + + // TODO: Test with parent pom that has boot-starter as parent + @Nested - class CompareWithMaven { + class CompareResultWithMavenTest { @Test @DisplayName("compare MavenProject.getCollectedProjects()") @@ -160,31 +694,28 @@ class MavenProjectAnalyzerTest { List mavenSorted = mavenSession.getProjectDependencyGraph() .getSortedProjects(); - List sbmSorted = sut.getSortedProjects(baseDir, resources); + + List mavenProjects = mavenProjectFactory.create(baseDir, resources); + List sbmSorted = sut.sort(baseDir, mavenProjects); assertThat(mavenSorted).hasSize(5); assertThat(mavenSorted.size()).isEqualTo(sbmSorted.size()); - assertThat(mavenSorted.get(0).getGroupId()).isEqualTo("com.acme"); - assertThat(mavenSorted.get(0).getGroupId()).isEqualTo(sbmSorted.get(0).getGroupId()); + assertThat(sbmSorted.get(0).getGroupId()).isEqualTo("com.acme"); + assertThat(sbmSorted.get(0).getArtifactId()).isEqualTo("parent"); - assertThat(mavenSorted.get(0).getArtifactId()).isEqualTo("parent"); - assertThat(mavenSorted.get(0).getArtifactId()).isEqualTo(sbmSorted.get(0).getArtifactId()); + assertThat(sbmSorted.get(1).getArtifactId()).isEqualTo("module-a"); + assertThat(mavenSorted.get(1).getGroupId()).isEqualTo(sbmSorted.get(1).getGroupId()); - assertThat(mavenSorted.get(0).getCollectedProjects()).hasSize(4); - assertThat(mavenSorted.get(0).getCollectedProjects().size()) - .isEqualTo(sbmSorted.get(0).getCollectedProjects().size()); + assertThat(sbmSorted.get(2).getArtifactId()).isEqualTo("module-b"); + assertThat(mavenSorted.get(2).getGroupId()).isEqualTo(sbmSorted.get(2).getGroupId()); - List projectsCollectedByMaven = mavenSorted.get(0) - .getCollectedProjects() - .stream() - .map(p -> p.getArtifactId()) - .toList(); - assertThat(projectsCollectedByMaven).containsExactlyInAnyOrder("module-a", "module-b", "module-1", - "parent-b"); + assertThat(sbmSorted.get(3).getArtifactId()).isEqualTo("parent-b"); + assertThat(mavenSorted.get(3).getGroupId()).isEqualTo(sbmSorted.get(3).getGroupId()); + + assertThat(sbmSorted.get(4).getArtifactId()).isEqualTo("module-1"); + assertThat(mavenSorted.get(4).getGroupId()).isEqualTo(sbmSorted.get(4).getGroupId()); - assertThat(sbmSorted.get(0).getCollectedProjects().stream().map(p -> p.getArtifactId()).toList()) - .hasSameElementsAs(projectsCollectedByMaven); } private void writeToDisk(Path baseDir, List resources) { @@ -211,539 +742,4 @@ class MavenProjectAnalyzerTest { } - /** - * The simplest possible Maven project. - */ - @Test - @DisplayName("projectWithSinglePom") - void projectWithSinglePom() { - @Language("xml") - String singlePom = """ - - - 4.0.0 - com.acme - 0.1.0-SNAPSHOT - example - - """; - - List resources = List.of(new DummyResource(Path.of("pom.xml"), singlePom)); - - Path baseDir = Path.of(".").toAbsolutePath().normalize(); - List sortedProjects = sut.getSortedProjects(baseDir, resources); - assertThat(sortedProjects).hasSize(1); - } - - /** - * A simple reactor build with one parent and one module pom. - */ - @Test - @DisplayName("reactorBuild") - void getSortedProjectsWithMultiModule() { - @Language("xml") - String parentPom = """ - - - 4.0.0 - com.acme - parent - 0.1.0-SNAPSHOT - pom - - example - - - """; - - @Language("xml") - String modulePom = """ - - - 4.0.0 - - com.acme - parent - 0.1.0-SNAPSHOT - - example - - """; - - String javaSource = """ - package com.example; - public class MainSource { - - } - """; - - Path baseDir = Path.of(".").toAbsolutePath().normalize(); - Path exampleModuleDir = baseDir.resolve("example"); - - // @formatter:off - List resources = List.of( - new DummyResource(baseDir.resolve("pom.xml"), parentPom), - new DummyResource(exampleModuleDir.resolve("pom.xml"), modulePom), - new DummyResource(exampleModuleDir.resolve("src/main/java/com/acme/MainSource.java"), javaSource), - new DummyResource(exampleModuleDir.resolve("src/test/java/com/acme/MainSourceTest.java"), javaSource) - ); - // @formatter:on - - List sortedProjects = sut.getSortedProjects(baseDir, resources); - - assertThat(sortedProjects).hasSize(2); - - MavenProject parentProject = sortedProjects.get(0); - MavenProject exampleProject = sortedProjects.get(1); - - assertThat(LinuxWindowsPathUnifier.pathEquals(baseDir.normalize(), parentProject.getBasedir())).isTrue(); - assertThat(LinuxWindowsPathUnifier.pathEquals(exampleProject.getBasedir(), - Path.of(".").resolve("example").toAbsolutePath().normalize())); - - assertThat(exampleProject.getMainJavaSources()).hasSize(1); - assertThat(LinuxWindowsPathUnifier - .unifiedPathString(ResourceUtil.getPath(exampleProject.getMainJavaSources().get(0))) - .endsWith("src/main/java/com/acme/MainSource.java")); - assertThat(exampleProject.getTestJavaSources()).hasSize(1); - assertThat(LinuxWindowsPathUnifier - .unifiedPathString(ResourceUtil.getPath(exampleProject.getMainJavaSources().get(0))) - .endsWith("src/test/java/com/acme/MainSourceTest.java")); - - } - - /** - * Two pom files building a rector build should be returned. The dangling pom not - * belonging to the reactor build defined through parent pom will be ignored. - */ - @Test - @DisplayName("reactorBuildWithDanglingPom") - void reactorBuildWithDanglingPom() { - @Language("xml") - String parentPom = """ - - - 4.0.0 - com.acme - parent - 0.1.0-SNAPSHOT - pom - - example - - - """; - - @Language("xml") - String modulePom = """ - - - 4.0.0 - - com.acme - parent - 0.1.0-SNAPSHOT - - example - - """; - - @Language("xml") - String danglingPom = """ - - - 4.0.0 - com.acme - dangling - 0.1.0-SNAPSHOT - - """; - - List resources = List.of(new DummyResource(Path.of("pom.xml"), parentPom), - new DummyResource(Path.of("example/pom.xml"), modulePom), - new DummyResource(Path.of("dangling/pom.xml"), danglingPom)); - - List sortedProjects = sut.getSortedProjects(Path.of(".").toAbsolutePath(), resources); - - assertThat(sortedProjects).hasSize(2); - - String parentPomPath = Path.of(".").toAbsolutePath().normalize().toString(); - assertThat(LinuxWindowsPathUnifier.pathEquals(sortedProjects.get(0).getBasedir(), parentPomPath)); - - String modulePomPath = Path.of(".").resolve("example").toAbsolutePath().normalize().toString(); - assertThat(LinuxWindowsPathUnifier.pathEquals(sortedProjects.get(1).getBasedir(), modulePomPath)); - } - - /** - * A project with three Maven pom files. Two of them build a reactor. The third is not - * part of the reactor but a dependency of the child module in the reactor build. - */ - @Test - @DisplayName("reactorBuildWithDanglingPomWhichAReactorModuleDependsOn") - void reactorBuildWithDanglingPomWhichAReactorModuleDependsOn() { - @Language("xml") - String parentPom = """ - - - 4.0.0 - com.acme - parent - 0.1.0-SNAPSHOT - pom - - example - - - """; - - @Language("xml") - String modulePom = """ - - - 4.0.0 - - com.acme - parent - 0.1.0-SNAPSHOT - - example - - - com.acme - dangling - 0.1.0-SNAPSHOT - - - - """; - - @Language("xml") - String danglingPom = """ - - - 4.0.0 - com.acme - dangling - 0.1.0-SNAPSHOT - - """; - - List resources = List.of(new DummyResource(Path.of("pom.xml"), parentPom), - new DummyResource(Path.of("example/pom.xml"), modulePom), - new DummyResource(Path.of("dangling/pom.xml"), danglingPom)); - - List sortedProjects = sut.getSortedProjects(Path.of(".").toAbsolutePath(), resources); - - assertThat(sortedProjects).hasSize(2); - - String parentPomPath = LinuxWindowsPathUnifier.unifiedPathString(Path.of(".").toAbsolutePath().normalize()); - assertThat(LinuxWindowsPathUnifier.pathEquals(sortedProjects.get(0).getBasedir(), parentPomPath)).isTrue(); - - String modulePomPath = LinuxWindowsPathUnifier - .unifiedPathString(Path.of(".").resolve("example").toAbsolutePath().normalize()); - assertThat(LinuxWindowsPathUnifier.pathEquals(sortedProjects.get(1).getBasedir(), modulePomPath)).isTrue(); - } - - /** - * A reactor project with four modules provided in "wrong" order. The returned order - * differs and reflects the order of the modules in reactor build. - */ - @Test - @DisplayName("theReactorBuildOrderIsReturned") - void theReactorBuildOrderIsReturned() { - @Language("xml") - String parentPom = """ - - - 4.0.0 - com.acme - parent - pom - 0.1.0-SNAPSHOT - - module-a - module-b - module-c - - - """; - - @Language("xml") - String moduleAPom = """ - - - 4.0.0 - - com.acme - parent - 0.1.0-SNAPSHOT - - module-a - - """; - - @Language("xml") - String moduleBPom = """ - - - 4.0.0 - - com.acme - parent - 0.1.0-SNAPSHOT - - module-b - - """; - - @Language("xml") - String moduleCPom = """ - - - 4.0.0 - - com.acme - parent - 0.1.0-SNAPSHOT - - module-c - - """; - - // Provided unordered - List resources = List.of(new DummyResource(Path.of("module-b/pom.xml"), moduleBPom), - new DummyResource(Path.of("module-a/pom.xml"), moduleAPom), - new DummyResource(Path.of("module-c/pom.xml"), moduleCPom), - new DummyResource(Path.of("pom.xml"), parentPom)); - - List sortedProjects = sut.getSortedProjects(Path.of(".").toAbsolutePath(), resources); - - // Returned ordered - assertThat(sortedProjects).hasSize(4); - assertThat(sortedProjects.get(0).getModuleDir().toString()).isEqualTo(""); - assertThat(sortedProjects.get(1).getModuleDir().toString()).isEqualTo("module-a"); - assertThat(sortedProjects.get(2).getModuleDir().toString()).isEqualTo("module-b"); - assertThat(sortedProjects.get(3).getModuleDir().toString()).isEqualTo("module-c"); - } - - /** - * Provided unordered list of resources Order in modules is not correct Order is - * defined by dependencies - */ - @Test - @DisplayName("moreComplex") - void moreComplex() { - - // Modules declared in order a,b,c - @Language("xml") - String parentPom = """ - - - 4.0.0 - com.acme - parent - 0.1.0-SNAPSHOT - pom - - module-a - module-b - module-c - - - """; - - // Module A depends on C, so C must be built first effectively changing the order - // in - @Language("xml") - String moduleAPom = """ - - - 4.0.0 - - com.acme - parent - 0.1.0-SNAPSHOT - - module-a - - - com.acme - module-c - 0.1.0-SNAPSHOT - - - - """; - - @Language("xml") - String moduleBPom = """ - - - 4.0.0 - - com.acme - parent - 0.1.0-SNAPSHOT - - module-b - - """; - - // C depends on B - @Language("xml") - String moduleCPom = """ - - - 4.0.0 - - com.acme - parent - 0.1.0-SNAPSHOT - - module-c - - - com.acme - module-b - 0.1.0-SNAPSHOT - - - - """; - - List resources = List.of(new DummyResource(Path.of("module-b/pom.xml"), moduleBPom), - new DummyResource(Path.of("module-a/pom.xml"), moduleAPom), - new DummyResource(Path.of("module-c/pom.xml"), moduleCPom), - new DummyResource(Path.of("pom.xml"), parentPom)); - - // Provided unordered - List sortedProjects = sut.getSortedProjects(Path.of(".").toAbsolutePath(), resources); - - // Expected order is parent, module-b, module-c, module-a - assertThat(sortedProjects).hasSize(4); - - assertThat(sortedProjects.get(0).getModuleDir().toString()).isEqualTo(""); - assertThat(sortedProjects.get(1).getModuleDir().toString()).isEqualTo("module-b"); - assertThat(sortedProjects.get(2).getModuleDir().toString()).isEqualTo("module-c"); - assertThat(sortedProjects.get(3).getModuleDir().toString()).isEqualTo("module-a"); - } - - @Test - @DisplayName("sortModels") - void sortModels() { - - // Modules declared in order a,b,c - @Language("xml") - String parentPom = """ - - - 4.0.0 - com.acme - parent - 0.1.0-SNAPSHOT - pom - - module-a - module-b - module-c - - - """; - - // Module A depends on C, so C must be built first effectively changing the order - // in - @Language("xml") - String moduleAPom = """ - - - 4.0.0 - - com.acme - parent - 0.1.0-SNAPSHOT - - module-a - - - com.acme - module-c - 0.1.0-SNAPSHOT - - - - """; - - @Language("xml") - String moduleBPom = """ - - - 4.0.0 - - com.acme - parent - 0.1.0-SNAPSHOT - - module-b - - """; - - // C depends on B - @Language("xml") - String moduleCPom = """ - - - 4.0.0 - - com.acme - parent - 0.1.0-SNAPSHOT - - module-c - - - com.acme - module-b - 0.1.0-SNAPSHOT - - - - """; - - // Provided unordered - List models = List.of( - new MavenProjectAnalyzer.Model(new DummyResource(Path.of("module-b/pom.xml"), moduleBPom)), - new MavenProjectAnalyzer.Model(new DummyResource(Path.of("module-a/pom.xml"), moduleAPom)), - new MavenProjectAnalyzer.Model(new DummyResource(Path.of("module-c/pom.xml"), moduleCPom)), - new MavenProjectAnalyzer.Model(new DummyResource(Path.of("pom.xml"), parentPom))); - - // Expected order is parent, module-b, module-c, module-a - List sorted = sut.sortModels(models); - assertThat(sorted.get(0).getArtifactId()).isEqualTo("parent"); - assertThat(sorted.get(1).getArtifactId()).isEqualTo("module-b"); - assertThat(sorted.get(2).getArtifactId()).isEqualTo("module-c"); - assertThat(sorted.get(3).getArtifactId()).isEqualTo("module-a"); - } - - // TODO: Test with parent pom that has boot-starter as parent - } \ No newline at end of file diff --git a/spring-rewrite-commons-launcher/src/test/java/org/springframework/rewrite/test/util/ParserParityTestHelper.java b/spring-rewrite-commons-launcher/src/test/java/org/springframework/rewrite/test/util/ParserParityTestHelper.java index dfd2673..25e9929 100644 --- a/spring-rewrite-commons-launcher/src/test/java/org/springframework/rewrite/test/util/ParserParityTestHelper.java +++ b/spring-rewrite-commons-launcher/src/test/java/org/springframework/rewrite/test/util/ParserParityTestHelper.java @@ -33,6 +33,7 @@ import org.openrewrite.maven.tree.Scope; import org.openrewrite.style.Style; import org.springframework.rewrite.parsers.SpringRewriteProperties; import org.springframework.rewrite.parsers.RewriteProjectParsingResult; +import org.springframework.rewrite.parsers.maven.ClasspathDependencies; import java.io.File; import java.net.URI; @@ -236,7 +237,9 @@ public class ParserParityTestHelper { static void verifyEqualSourceFileMarkers(SourceFile curExpectedSourceFile, SourceFile curGivenSourceFile) { Markers expectedMarkers = curExpectedSourceFile.getMarkers(); List expectedMarkersList = expectedMarkers.getMarkers(); - Markers givenMarkers = curGivenSourceFile.getMarkers(); + + // Remove custom marker that only exists here + Markers givenMarkers = curGivenSourceFile.getMarkers().removeByType(ClasspathDependencies.class); List actualMarkersList = givenMarkers.getMarkers(); assertThat(actualMarkersList.stream().map(m -> m.getClass().getSimpleName()).toList())