All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.openrewrite.maven.ChangeParentPom Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2021 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.jspecify.annotations.Nullable; import org.openrewrite.*; import org.openrewrite.internal.StringUtils; import org.openrewrite.marker.SearchResult; import org.openrewrite.maven.internal.MavenPomDownloader; import org.openrewrite.maven.table.MavenMetadataFailures; import org.openrewrite.maven.tree.*; import org.openrewrite.semver.Semver; import org.openrewrite.semver.VersionComparator; import org.openrewrite.xml.AddToTagVisitor; import org.openrewrite.xml.ChangeTagValueVisitor; import org.openrewrite.xml.TagNameComparator; import org.openrewrite.xml.tree.Xml; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import static java.util.Collections.emptyList; import static org.openrewrite.internal.StringUtils.matchesGlob; @Value @EqualsAndHashCode(callSuper = false) public class ChangeParentPom extends Recipe { transient MavenMetadataFailures metadataFailures = new MavenMetadataFailures(this); @Option(displayName = "Old group ID", description = "The group ID of the Maven parent pom to be changed away from.", example = "org.springframework.boot") String oldGroupId; @Option(displayName = "New group ID", description = "The group ID of the new maven parent pom to be adopted. If this argument is omitted it defaults to the value of `oldGroupId`.", example = "org.springframework.boot", required = false) @Nullable String newGroupId; @Option(displayName = "Old artifact ID", description = "The artifact ID of the maven parent pom to be changed away from.", example = "spring-boot-starter-parent") String oldArtifactId; @Option(displayName = "New artifact ID", description = "The artifact ID of the new maven parent pom to be adopted. If this argument is omitted it defaults to the value of `oldArtifactId`.", example = "spring-boot-starter-parent", required = false) @Nullable String newArtifactId; @Option(displayName = "New version", description = "An exact version number or node-style semver selector used to select the version number.", example = "29.X") String newVersion; @Option(displayName = "Old relative path", description = "The relativePath of the maven parent pom to be changed away from.", example = "../../pom.xml", required = false) @Nullable String oldRelativePath; @Option(displayName = "New relative path", description = "New relative path attribute for parent lookup.", example = "../pom.xml", required = false) @Nullable String newRelativePath; @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; @Option(displayName = "Allow version downgrades", description = "If the new parent has the same group/artifact, this flag can be used to only upgrade the " + "version if the target version is newer than the current.", required = false) @Nullable Boolean allowVersionDowngrades; @Override public String getDisplayName() { return "Change Maven parent"; } @Override public String getInstanceNameSuffix() { return String.format("`%s:%s:%s`", newGroupId, newArtifactId, newVersion); } @Override public String getDescription() { return "Change the parent pom of a Maven pom.xml. Identifies the parent pom to be changed by its groupId and artifactId."; } @Override public Validated validate() { Validated validated = super.validate(); //noinspection ConstantConditions if (newVersion != null) { validated = validated.and(Semver.validate(newVersion, versionPattern)); } return validated; } @Override public TreeVisitor getVisitor() { VersionComparator versionComparator = Semver.validate(newVersion, versionPattern).getValue(); assert versionComparator != null; return Preconditions.check(new MavenVisitor() { @Override public Xml visitDocument(Xml.Document document, ExecutionContext ctx) { Parent parent = getResolutionResult().getPom().getRequested().getParent(); if (parent != null && matchesGlob(parent.getArtifactId(), oldArtifactId) && matchesGlob(parent.getGroupId(), oldGroupId)) { return SearchResult.found(document); } return document; } }, new MavenIsoVisitor() { @Nullable private Collection availableVersions; @SuppressWarnings("OptionalGetWithoutIsPresent") @Override public Xml.Tag visitTag(Xml.Tag tag, ExecutionContext ctx) { Xml.Tag t = super.visitTag(tag, ctx); if (isParentTag()) { MavenResolutionResult mrr = getResolutionResult(); ResolvedPom resolvedPom = mrr.getPom(); if (matchesGlob(resolvedPom.getValue(tag.getChildValue("groupId").orElse(null)), oldGroupId) && matchesGlob(resolvedPom.getValue(tag.getChildValue("artifactId").orElse(null)), oldArtifactId) && (oldRelativePath == null || matchesGlob(resolvedPom.getValue(tag.getChildValue("relativePath").orElse(null)), oldRelativePath))) { String oldVersion = resolvedPom.getValue(tag.getChildValue("version").orElse(null)); assert oldVersion != null; String currentGroupId = tag.getChildValue("groupId").orElse(oldGroupId); String targetGroupId = newGroupId == null ? currentGroupId : newGroupId; String currentArtifactId = tag.getChildValue("artifactId").orElse(oldArtifactId); String targetArtifactId = newArtifactId == null ? currentArtifactId : newArtifactId; String targetRelativePath = newRelativePath == null ? tag.getChildValue("relativePath").orElse(oldRelativePath) : newRelativePath; try { Optional targetVersion = findAcceptableVersion(targetGroupId, targetArtifactId, oldVersion, ctx); if (!targetVersion.isPresent() || (Objects.equals(targetGroupId, currentGroupId) && Objects.equals(targetArtifactId, currentArtifactId) && Objects.equals(targetVersion.get(), oldVersion) && Objects.equals(targetRelativePath, oldRelativePath))) { return t; } List> changeParentTagVisitors = new ArrayList<>(); if (!currentGroupId.equals(targetGroupId)) { changeParentTagVisitors.add(new ChangeTagValueVisitor<>(t.getChild("groupId").get(), targetGroupId)); } if (!currentArtifactId.equals(targetArtifactId)) { changeParentTagVisitors.add(new ChangeTagValueVisitor<>(t.getChild("artifactId").get(), targetArtifactId)); } if (!oldVersion.equals(targetVersion.get())) { changeParentTagVisitors.add(new ChangeTagValueVisitor<>(t.getChild("version").get(), targetVersion.get())); } // Retain managed versions from the old parent that are not managed in the new parent MavenPomDownloader mpd = new MavenPomDownloader(mrr.getProjectPoms(), ctx, mrr.getMavenSettings(), mrr.getActiveProfiles()); ResolvedPom newParent = mpd.download(new GroupArtifactVersion(targetGroupId, targetArtifactId, targetVersion.get()), null, resolvedPom, resolvedPom.getRepositories()) .resolve(emptyList(), mpd, ctx); List dependenciesWithoutExplicitVersions = getDependenciesUnmanagedByNewParent(mrr, newParent); for (ResolvedManagedDependency dep : dependenciesWithoutExplicitVersions) { changeParentTagVisitors.add(new AddManagedDependencyVisitor( dep.getGav().getGroupId(), dep.getGav().getArtifactId(), dep.getGav().getVersion(), dep.getScope() == null ? null : dep.getScope().toString().toLowerCase(), dep.getType(), dep.getClassifier())); } // Retain properties from the old parent that are not present in the new parent Map propertiesInUse = getPropertiesInUse(getCursor().firstEnclosingOrThrow(Xml.Document.class), ctx); Map newParentProps = newParent.getProperties(); for (Map.Entry propInUse : propertiesInUse.entrySet()) { if(!newParentProps.containsKey(propInUse.getKey())) { changeParentTagVisitors.add(new UnconditionalAddProperty(propInUse.getKey(), propInUse.getValue())); } } // Update or add relativePath if (oldRelativePath != null && !oldRelativePath.equals(targetRelativePath)) { changeParentTagVisitors.add(new ChangeTagValueVisitor<>(t.getChild("relativePath").get(), targetRelativePath)); } else if (mismatches(tag.getChild("relativePath").orElse(null), targetRelativePath)) { final Xml.Tag relativePathTag; if (StringUtils.isBlank(targetRelativePath)) { relativePathTag = Xml.Tag.build(""); } else { relativePathTag = Xml.Tag.build("" + targetRelativePath + ""); } doAfterVisit(new AddToTagVisitor<>(t, relativePathTag, new MavenTagInsertionComparator(t.getChildren()))); maybeUpdateModel(); } if (!changeParentTagVisitors.isEmpty()) { for (TreeVisitor visitor : changeParentTagVisitors) { doAfterVisit(visitor); } 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(targetGroupId, targetArtifactId, newVersion, repository.getUri(), repository.getSnapshots(), repository.getReleases(), repositoryResponse.getValue())); } return e.warn(tag); } } } return t; } private boolean mismatches(Xml.@Nullable Tag relativePath, @Nullable String targetRelativePath) { if (relativePath == null) { return targetRelativePath != null; } String relativePathValue = relativePath.getValue().orElse(null); if (relativePathValue == null) { return !StringUtils.isBlank(targetRelativePath); } return !relativePathValue.equals(targetRelativePath); } private Optional findAcceptableVersion(String groupId, String artifactId, String currentVersion, ExecutionContext ctx) throws MavenDownloadingException { String finalCurrentVersion = !Semver.isVersion(currentVersion) ? "0.0.0" : currentVersion; if (availableVersions == null) { MavenMetadata mavenMetadata = metadataFailures.insertRows(ctx, () -> downloadMetadata(groupId, artifactId, ctx)); //noinspection EqualsWithItself availableVersions = mavenMetadata.getVersioning().getVersions().stream() .filter(v -> versionComparator.isValid(finalCurrentVersion, v)) .filter(v -> Boolean.TRUE.equals(allowVersionDowngrades) || versionComparator.compare(finalCurrentVersion, finalCurrentVersion, v) <= 0) .collect(Collectors.toList()); } if (Boolean.TRUE.equals(allowVersionDowngrades)) { return availableVersions.stream() .max((v1, v2) -> versionComparator.compare(finalCurrentVersion, v1, v2)); } Optional upgradedVersion = versionComparator.upgrade(finalCurrentVersion, availableVersions); if (upgradedVersion.isPresent()) { return upgradedVersion; } return availableVersions.stream().filter(finalCurrentVersion::equals).findFirst(); } }); } private static Pattern PROPERTY_PATTERN = Pattern.compile("\\$\\{([^}]+)}"); private static Map getPropertiesInUse(Xml.Document pomXml, ExecutionContext ctx) { Map properties = new HashMap<>(); new MavenIsoVisitor() { @Nullable ResolvedPom resolvedPom = null; @Override public Xml.Tag visitTag(Xml.Tag tag, ExecutionContext ctx) { Xml.Tag t = super.visitTag(tag, ctx); if(t.getContent() != null && t.getContent().size() == 1 && t.getContent().get(0) instanceof Xml.CharData) { String text = ((Xml.CharData) t.getContent().get(0)).getText().trim(); Matcher m = PROPERTY_PATTERN.matcher(text); while(m.find()) { if(resolvedPom == null) { resolvedPom = getResolutionResult().getPom(); } String propertyName = m.group(1).trim(); if (resolvedPom.getProperties().containsKey(propertyName) && !isGlobalProperty(propertyName)) { properties.put(m.group(1).trim(), resolvedPom.getProperties().get(propertyName)); } } } return t; } private boolean isGlobalProperty(String propertyName) { return propertyName.startsWith("project.") || propertyName.startsWith("env.") || propertyName.startsWith("settings.") || propertyName.equals("basedir"); } }.visit(pomXml, ctx); return properties; } private List getDependenciesUnmanagedByNewParent(MavenResolutionResult mrr, ResolvedPom newParent) { ResolvedPom resolvedPom = mrr.getPom(); // Dependencies managed by the current pom's own dependency management are irrelevant to parent upgrade List locallyManaged = mrr.getPom().getRequested().getDependencyManagement(); Set requestedWithoutExplicitVersion = resolvedPom.getRequested().getDependencies().stream() .filter(dep -> dep.getVersion() == null) // Dependencies explicitly managed by the current pom require no changes .filter(dep -> locallyManaged.stream() .noneMatch(localManagedDep -> localManagedDep.getGroupId().equals(dep.getGroupId()) && localManagedDep.getArtifactId().equals(dep.getArtifactId()))) .map(dep -> new GroupArtifactVersion(dep.getGroupId(), dep.getArtifactId(), null)) .collect(Collectors.toCollection(LinkedHashSet::new)); if(requestedWithoutExplicitVersion.isEmpty()) { return emptyList(); } List depsWithoutExplicitVersion = resolvedPom.getDependencyManagement().stream() .filter(dep -> requestedWithoutExplicitVersion.contains(dep.getGav().withVersion(null))) // Exclude dependencies managed by a bom imported by the current pom .filter(dep -> dep.getBomGav() == null || locallyManaged.stream() .noneMatch(localManagedDep -> localManagedDep.getGroupId().equals(dep.getBomGav().getGroupId()) && localManagedDep.getArtifactId().equals(dep.getBomGav().getArtifactId()))) .collect(Collectors.toList()); if(depsWithoutExplicitVersion.isEmpty()) { return emptyList(); } // Remove from the list any that would still be managed under the new parent Set newParentManagedGa = newParent.getDependencyManagement().stream() .map(dep -> new GroupArtifact(dep.getGav().getGroupId(), dep.getGav().getArtifactId())) .collect(Collectors.toSet()); depsWithoutExplicitVersion = depsWithoutExplicitVersion.stream() .filter(it -> !newParentManagedGa.contains(new GroupArtifact(it.getGav().getGroupId(), it.getGav().getArtifactId()))) .collect(Collectors.toList()); return depsWithoutExplicitVersion; } @Value @EqualsAndHashCode(callSuper = false) private static class UnconditionalAddProperty extends MavenIsoVisitor { String key; String value; @Override public Xml.Document visitDocument(Xml.Document document, ExecutionContext ctx) { Xml.Document d = super.visitDocument(document, ctx); Xml.Tag root = d.getRoot(); Optional properties = root.getChild("properties"); if (!properties.isPresent()) { Xml.Tag propertiesTag = Xml.Tag.build("\n<" + key + ">" + value + "\n"); d = (Xml.Document) new AddToTagVisitor(root, propertiesTag, new MavenTagInsertionComparator(root.getChildren())).visitNonNull(d, ctx); } else if (!properties.get().getChildValue(key).isPresent()) { Xml.Tag propertyTag = Xml.Tag.build("<" + key + ">" + value + ""); d = (Xml.Document) new AddToTagVisitor<>(properties.get(), propertyTag, new TagNameComparator()).visitNonNull(d, ctx); } if (d != document) { maybeUpdateModel(); } return d; } @Override public Xml.Tag visitTag(Xml.Tag tag, ExecutionContext ctx) { Xml.Tag t = super.visitTag(tag, ctx); if (isPropertyTag() && key.equals(tag.getName()) && !value.equals(tag.getValue().orElse(null))) { t = (Xml.Tag) new ChangeTagValueVisitor<>(tag, value).visitNonNull(t, ctx); } return t; } } }