diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/AddParentPom.java b/rewrite-maven/src/main/java/org/openrewrite/maven/AddParentPom.java new file mode 100644 index 00000000000..d495affc70f --- /dev/null +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/AddParentPom.java @@ -0,0 +1,163 @@ +/* + * Copyright 2024 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.maven; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.openrewrite.*; +import org.openrewrite.internal.StringUtils; +import org.openrewrite.internal.lang.Nullable; +import org.openrewrite.marker.SearchResult; +import org.openrewrite.maven.table.MavenMetadataFailures; +import org.openrewrite.maven.tree.MavenMetadata; +import org.openrewrite.maven.tree.MavenRepository; +import org.openrewrite.semver.Semver; +import org.openrewrite.semver.VersionComparator; +import org.openrewrite.xml.AddToTagVisitor; +import org.openrewrite.xml.tree.Xml; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +@Value +@EqualsAndHashCode(callSuper = false) +public class AddParentPom extends Recipe { + transient MavenMetadataFailures metadataFailures = new MavenMetadataFailures(this); + + @Option(displayName = "Group ID", + description = "The group ID of the maven parent pom to be adopted.", + example = "org.springframework.boot") + String groupId; + + @Option(displayName = "Artifact ID", + description = "The artifact ID of the maven parent pom to be adopted.", + example = "spring-boot-starter-parent") + String artifactId; + + @Option(displayName = "Version", + description = "An exact version number or node-style semver selector used to select the version number.", + example = "29.X") + String version; + + @Option(displayName = "Relative path", + description = "New relative path attribute for parent lookup.", + example = "../pom.xml") + @Nullable + String relativePath; + + @Option(displayName = "Version pattern", + description = "Allows version selection to be extended beyond the original Node Semver semantics. So for example," + + "Setting 'version' to \"25-29\" can be paired with a metadata pattern of \"-jre\" to select Guava 29.0-jre", + example = "-jre", + required = false) + @Nullable + String versionPattern; + + @Override + public String getDisplayName() { + return "Add Maven parent"; + } + + @Override + public String getInstanceNameSuffix() { + return String.format("`%s:%s:%s`", groupId, artifactId, version); + } + + @Override + public String getDescription() { + return "Add a parent pom to a Maven pom.xml. Does nothing if a parent pom is already present."; + } + + @Override + public Validated validate() { + Validated validated = super.validate(); + //noinspection ConstantConditions + if (version != null) { + validated = validated.and(Semver.validate(version, versionPattern)); + } + return validated; + } + + @Override + public TreeVisitor getVisitor() { + return Preconditions.check(new MavenVisitor() { + @Override + public Xml visitDocument(Xml.Document document, ExecutionContext ctx) { + Xml.Tag root = document.getRoot(); + if (!root.getChild("parent").isPresent()) { + return SearchResult.found(document); + } + return document; + + } + }, new MavenIsoVisitor() { + @Nullable + private Collection availableVersions; + + @Override + public Xml.Document visitDocument(Xml.Document document, ExecutionContext ctx) { + Xml.Tag root = document.getRoot(); + assert !root.getChild("parent").isPresent(); + + try { + Optional targetVersion = findAcceptableVersion(groupId, artifactId, ctx); + if (targetVersion.isPresent()) { + Xml.Tag parentTag = Xml.Tag.build( + "\n" + + "" + groupId + "\n" + + "" + artifactId + "\n" + + "" + targetVersion.get() + "\n" + + (relativePath == null ? "" : StringUtils.isBlank(relativePath) ? + "" : "" + relativePath + "") + + ""); + + document = (Xml.Document) new AddToTagVisitor<>(root, parentTag, new MavenTagInsertionComparator(root.getChildren())) + .visitNonNull(document, ctx, getCursor().getParentOrThrow()); + + maybeUpdateModel(); + doAfterVisit(new RemoveRedundantDependencyVersions(null, null, + RemoveRedundantDependencyVersions.Comparator.GTE, null).getVisitor()); + } + } catch (MavenDownloadingException e) { + for (Map.Entry repositoryResponse : e.getRepositoryResponses().entrySet()) { + MavenRepository repository = repositoryResponse.getKey(); + metadataFailures.insertRow(ctx, new MavenMetadataFailures.Row(groupId, artifactId, version, + repository.getUri(), repository.getSnapshots(), repository.getReleases(), repositoryResponse.getValue())); + } + return e.warn(document); + } + + return super.visitDocument(document, ctx); + } + + private final VersionComparator versionComparator = Objects.requireNonNull(Semver.validate(version, versionPattern).getValue()); + + private Optional findAcceptableVersion(String groupId, String artifactId, ExecutionContext ctx) + throws MavenDownloadingException { + if (availableVersions == null) { + MavenMetadata mavenMetadata = metadataFailures.insertRows(ctx, () -> downloadMetadata(groupId, artifactId, ctx)); + availableVersions = mavenMetadata.getVersioning().getVersions().stream() + .filter(v -> versionComparator.isValid(null, v)) + .collect(Collectors.toList()); + } + return availableVersions.stream().max(versionComparator); + } + }); + } +} diff --git a/rewrite-maven/src/test/java/org/openrewrite/maven/AddParentPomTest.java b/rewrite-maven/src/test/java/org/openrewrite/maven/AddParentPomTest.java new file mode 100644 index 00000000000..a2575e1e656 --- /dev/null +++ b/rewrite-maven/src/test/java/org/openrewrite/maven/AddParentPomTest.java @@ -0,0 +1,475 @@ +/* + * Copyright 2024 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.maven; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.openrewrite.DocumentExample; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.mavenProject; +import static org.openrewrite.maven.Assertions.pomXml; + +class AddParentPomTest implements RewriteTest { + + @DocumentExample + @Test + void addParent() { + rewriteRun( + spec -> spec.recipe(new AddParentPom( + "org.springframework.boot", + "spring-boot-starter-parent", + "1.5.12.RELEASE", + null, + null + )), + pomXml( + """ + + 4.0.0 + + com.mycompany.app + my-app + 1 + + """, + """ + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 1.5.12.RELEASE + + + com.mycompany.app + my-app + 1 + + """ + ) + ); + } + + @ParameterizedTest + @ValueSource(strings = { + "1.5.99.RELEASE", + "~1.9" + }) + void shoudNotAddInvalidParentPomVersion(String version) { + rewriteRun( + spec -> spec.recipe(new AddParentPom( + "org.springframework.boot", + "spring-boot-starter-parent", + version, + null, + null + )), + pomXml( + """ + + 4.0.0 + + com.mycompany.app + my-app + 1 + + """ + ) + ); + } + + @Test + void addParentWithRelativePath() { + rewriteRun( + spec -> spec.recipe(new AddParentPom( + "org.springframework.boot", + "spring-boot-starter-parent", + "1.5.12.RELEASE", + "../../pom.xml", + null + )), + pomXml( + """ + + 4.0.0 + + com.mycompany.app + my-app + 1 + + """, + """ + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 1.5.12.RELEASE + ../../pom.xml + + + com.mycompany.app + my-app + 1 + + """ + ) + ); + } + + @Test + void addParentWithRelativePathEmptyValue() { + rewriteRun( + spec -> spec.recipe(new AddParentPom( + "org.springframework.boot", + "spring-boot-starter-parent", + "1.5.12.RELEASE", + "", + null + )), + pomXml( + """ + + 4.0.0 + + com.mycompany.app + my-app + 1 + + """, + """ + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 1.5.12.RELEASE + + + + com.mycompany.app + my-app + 1 + + """ + ) + ); + } + + @Test + void multiModuleRelativePath() { + AddParentPom recipe = new AddParentPom("org.springframework.boot", "spring-boot-starter-parent", "1.5.12.RELEASE", "", null); + rewriteRun( + spec -> spec.recipe(recipe), + mavenProject("parent", + pomXml( + """ + + + 4.0.0 + org.sample + sample + 1.0.0 + + + module1 + module2 + + + """, + """ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 1.5.12.RELEASE + + + org.sample + sample + 1.0.0 + + + module1 + module2 + + + """ + ), + mavenProject("module1", + pomXml( + """ + + + 4.0.0 + + org.sample + sample + 1.0.0 + + module1 + + """ + )), + mavenProject("module2", + pomXml( + """ + + + 4.0.0 + + org.sample + sample + 1.0.0 + + module2 + + """ + ) + ) + ) + ); + } + + @Test + void multiModuleShouldOnlyChangeRoot() { + rewriteRun( + spec -> spec.recipe(new AddParentPom( + "org.springframework.boot", + "spring-boot-starter-parent", + "1.5.12.RELEASE", + null, null)), + mavenProject("parent", + pomXml( + """ + + + 4.0.0 + org.sample + sample + 1.0.0 + + + module1 + module2 + + + """, + """ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 1.5.12.RELEASE + + org.sample + sample + 1.0.0 + + + module1 + module2 + + + """ + ), + mavenProject("module1", + pomXml( + """ + + + 4.0.0 + + org.sample + sample + 1.0.0 + + module1 + + """ + )), + mavenProject("module2", + pomXml( + """ + + + 4.0.0 + + org.sample + sample + 1.0.0 + + module2 + + """ + ) + ) + ) + ); + } + + @Test + void shouldResolveWildcardVersion() { + rewriteRun( + spec -> spec.recipe(new AddParentPom( + "org.springframework.boot", + "spring-boot-starter-parent", + "~1.5", + null, + null + )), + pomXml( + """ + + 4.0.0 + + com.mycompany.app + my-app + 1 + + """, + """ + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 1.5.22.RELEASE + + + com.mycompany.app + my-app + 1 + + """ + ) + ); + } + + @Test + void removesRedundantExplicitVersionsMatchingNewParent() { + rewriteRun( + spec -> spec.recipe(new AddParentPom( + "org.junit", + "junit-bom", + "5.9.1", + "", + null + )), + pomXml( + """ + + 4.0.0 + + com.mycompany.app + my-app + 1 + + + + org.junit.jupiter + junit-jupiter-api + 5.9.1 + + + + """, + """ + + 4.0.0 + + org.junit + junit-bom + 5.9.1 + + + + com.mycompany.app + my-app + 1 + + + + org.junit.jupiter + junit-jupiter-api + + + + """ + ) + ); + } + + @Test + void takesNewVersionFromParent() { + rewriteRun( + spec -> spec.recipe(new AddParentPom( + "org.junit", + "junit-bom", + "5.9.1", + "", + null + )), + pomXml( + """ + + 4.0.0 + + com.mycompany.app + my-app + 1 + + + + org.junit.jupiter + junit-jupiter-api + 5.8.0 + + + + """, + """ + + 4.0.0 + + org.junit + junit-bom + 5.9.1 + + + + com.mycompany.app + my-app + 1 + + + + org.junit.jupiter + junit-jupiter-api + + + + """ + ) + ); + } +}