Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.openrewrite.xml.RemoveContentVisitor;
import org.openrewrite.xml.tree.Xml;

import java.nio.file.Path;
import java.util.*;

import static java.util.Collections.max;
Expand All @@ -41,7 +42,8 @@

@Value
@EqualsAndHashCode(callSuper = false)
public class ChangeDependencyGroupIdAndArtifactId extends Recipe {
public class ChangeDependencyGroupIdAndArtifactId extends ScanningRecipe<ChangeDependencyGroupIdAndArtifactId.Accumulator> {
@EqualsAndHashCode.Exclude
transient MavenMetadataFailures metadataFailures = new MavenMetadataFailures(this);

@Option(displayName = "Old groupId",
Expand Down Expand Up @@ -142,7 +144,98 @@ public Validated<Object> validate() {
}

@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
public Accumulator getInitialValue(ExecutionContext ctx) {
return new Accumulator();
}

@Override
public TreeVisitor<?, ExecutionContext> getScanner(Accumulator acc) {
return new MavenIsoVisitor<ExecutionContext>() {
final @Nullable VersionComparator versionComparator = newVersion != null ? Semver.validate(newVersion, versionPattern).getValue() : null;
final boolean configuredToChangeManagedDependency = changeManagedDependency == null || changeManagedDependency;

@Override
public Xml.Tag visitTag(Xml.Tag tag, ExecutionContext ctx) {
if (!isDependencyTag(oldGroupId, oldArtifactId) && !isPluginDependencyTag(oldGroupId, oldArtifactId) && !isAnnotationProcessorPathTag(oldGroupId, oldArtifactId)) {
return super.visitTag(tag, ctx);
}
if (newVersion == null || versionComparator == null) {

@steve-aom-elliott steve-aom-elliott Jan 30, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't these two conditions only ever both be true or both be false? Unsure if Semver.validate(..).getValue() can give back null

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it can, so both could be null, unlikely in the case of the. version comperator but could.

return super.visitTag(tag, ctx);
}
String currentVersion = tag.getChildValue("version").orElse(null);
if (currentVersion == null || !isProperty(currentVersion)) {
return super.visitTag(tag, ctx);
}
String propertyName = currentVersion.substring(2, currentVersion.length() - 1);
if (getResolutionResult().getPom().getRequested().getProperties().containsKey(propertyName)) {
return super.visitTag(tag, ctx);
}
// Property is inherited from a parent POM; check if it is safe to change
Set<String> safeProperties = getSafeVersionPlaceholdersToChange(oldGroupId, oldArtifactId, ctx);
if (!safeProperties.contains(currentVersion)) {
return super.visitTag(tag, ctx);
}
try {
String groupId = Optional.ofNullable(newGroupId).orElse(tag.getChildValue("groupId").orElse(oldGroupId));
String artifactId = Optional.ofNullable(newArtifactId).orElse(tag.getChildValue("artifactId").orElse(oldArtifactId));
String resolvedVersion = resolveSemverVersion(ctx, groupId, artifactId, currentVersion);
storeParentPomProperty(getResolutionResult().getParent(), propertyName, resolvedVersion, acc);
} catch (MavenDownloadingException e) {
return e.warn(tag);
}
return super.visitTag(tag, ctx);
}

private Set<String> getSafeVersionPlaceholdersToChange(String groupId, String artifactId, ExecutionContext ctx) {
MavenResolutionResult result = getResolutionResult();
ResolvedPom resolvedPom = result.getPom();
Pom requestedPom = resolvedPom.getRequested();
Set<String> relevantProperties = requestedPom.getDependencies().stream()
.filter(d -> isProperty(d.getVersion()) &&
matchesGlob(resolvedPom.getValue(d.getGroupId()), groupId) &&
matchesGlob(resolvedPom.getValue(d.getArtifactId()), artifactId))
.map(Dependency::getVersion)
.collect(toSet());
relevantProperties = filterPropertiesWithOverlapInDependencies(relevantProperties, groupId, artifactId, requestedPom, resolvedPom, configuredToChangeManagedDependency);
relevantProperties = filterPropertiesWithOverlapInChildren(relevantProperties, groupId, artifactId, result, configuredToChangeManagedDependency);
return filterPropertiesWithOverlapInParents(relevantProperties, groupId, artifactId, result, configuredToChangeManagedDependency, ctx);
}

@SuppressWarnings("ConstantConditions")
private String resolveSemverVersion(ExecutionContext ctx, String groupId, String artifactId, @Nullable String currentVersion) throws MavenDownloadingException {
if (versionComparator == null) {
return newVersion;
}
String finalCurrentVersion = currentVersion != null ? currentVersion : newVersion;
List<String> availableVersions = new ArrayList<>();
MavenMetadata mavenMetadata = metadataFailures.insertRows(ctx, () -> downloadMetadata(groupId, artifactId, ctx));
for (String v : mavenMetadata.getVersioning().getVersions()) {
if (versionComparator.isValid(finalCurrentVersion, v)) {
availableVersions.add(v);
}
}
return availableVersions.isEmpty() ? newVersion : max(availableVersions, versionComparator);
}

private void storeParentPomProperty(@Nullable MavenResolutionResult parent, String propertyName, String newValue, Accumulator acc) {
if (parent == null) {
return;
}
Pom pom = parent.getPom().getRequested();
if (pom.getSourcePath() == null) {
return;
}
if (pom.getProperties().containsKey(propertyName)) {
acc.getPomProperties().add(new PomProperty(pom.getSourcePath(), propertyName, newValue));
return;
}
storeParentPomProperty(parent.getParent(), propertyName, newValue, acc);
}
};
}

@Override
public TreeVisitor<?, ExecutionContext> getVisitor(Accumulator acc) {
return new MavenVisitor<ExecutionContext>() {
@Nullable
final VersionComparator versionComparator = newVersion != null ? Semver.validate(newVersion, versionPattern).getValue() : null;
Expand Down Expand Up @@ -176,6 +269,23 @@ public Xml visitDocument(Xml.Document document, ExecutionContext ctx) {
@Override
public Xml visitTag(Xml.Tag tag, ExecutionContext ctx) {
Xml.Tag t = (Xml.Tag) super.visitTag(tag, ctx);

// Update version properties in parent POMs based on scanner results
if (isPropertyTag()) {
Path pomSourcePath = getResolutionResult().getPom().getRequested().getSourcePath();
for (PomProperty prop : acc.getPomProperties()) {
if (!prop.getPomFilePath().equals(pomSourcePath) || !prop.getPropertyName().equals(tag.getName())) {
continue;
}
Optional<String> value = tag.getValue();
if (!value.isPresent() || !value.get().equals(prop.getNewValue())) {
doAfterVisit(new ChangeTagValueVisitor<>(tag, prop.getNewValue()));
maybeUpdateModel();
}
Comment thread
timtebeek marked this conversation as resolved.
break;
}
}

boolean isOldDependencyTag = isDependencyTag(oldGroupId, oldArtifactId);
if (isOldDependencyTag && isNewDependencyPresent) {
doAfterVisit(new RemoveContentVisitor<>(tag, true, true));
Expand Down Expand Up @@ -321,4 +431,16 @@ private String resolveSemverVersion(ExecutionContext ctx, String groupId, String
}
};
}

@Value
public static class Accumulator {
Set<PomProperty> pomProperties = new HashSet<>();
}

@Value
public static class PomProperty {
Path pomFilePath;
String propertyName;
String newValue;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2320,6 +2320,196 @@ void changeVersionPropertyInParentPom() {
);
}

@Issue("https://github.com/moderneinc/customer-requests/issues/1374")
@Test
void changeVersionPropertyInParentPomSimple() {
rewriteRun(
spec -> spec.recipe(new ChangeDependencyGroupIdAndArtifactId(
"io.swagger",
"swagger-annotations",
"io.swagger.core.v3",
null,
"2.2.x",
null,
null,
null
)),
mavenProject("project",
//language=xml
pomXml(
"""
<project>
<groupId>com.mycompany.app</groupId>
<artifactId>parent-project</artifactId>
<version>1</version>
<properties>
<version.swagger>1.5.16</version.swagger>
</properties>
<modules>
<module>sub-project</module>
</modules>
</project>
""",
"""
<project>
<groupId>com.mycompany.app</groupId>
<artifactId>parent-project</artifactId>
<version>1</version>
<properties>
<version.swagger>2.2.42</version.swagger>
</properties>
<modules>
<module>sub-project</module>
</modules>
</project>
""",
spec -> spec.path("pom.xml")
),
mavenProject("sub-project",
//language=xml
pomXml(
"""
<project>
<groupId>com.mycompany.app</groupId>
<artifactId>sub-project</artifactId>
<version>1</version>
<parent>
<groupId>com.mycompany.app</groupId>
<artifactId>parent-project</artifactId>
<version>1</version>
<relativePath>../pom.xml</relativePath>
</parent>
<dependencies>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>${version.swagger}</version>
</dependency>
</dependencies>
</project>
""",
"""
<project>
<groupId>com.mycompany.app</groupId>
<artifactId>sub-project</artifactId>
<version>1</version>
<parent>
<groupId>com.mycompany.app</groupId>
<artifactId>parent-project</artifactId>
<version>1</version>
<relativePath>../pom.xml</relativePath>
</parent>
<dependencies>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
<version>${version.swagger}</version>
</dependency>
</dependencies>
</project>
""",
spec -> spec.path("sub-project/pom.xml")
)
)
)
);
}

@Issue("https://github.com/openrewrite/rewrite/pull/6630#discussion")
@Test
void sharedPropertyInParentPomLeavesPropertyUnchangedAndInlinesVersion() {
// When a property in the parent POM is shared by multiple dependencies in the child,
// and only one dependency matches the change criteria, the property should NOT be updated
// (as that would break the non-matching dependency). Instead, the version should be inlined.
rewriteRun(
spec -> spec.recipe(new ChangeDependencyGroupIdAndArtifactId(
"io.swagger",
"swagger-annotations",
"io.swagger.core.v3",
null,
"2.2.x",
null,
null,
null
)),
mavenProject("project",
//language=xml
pomXml(
"""
<project>
<groupId>com.mycompany.app</groupId>
<artifactId>parent-project</artifactId>
<version>1</version>
<properties>
<version.swagger>1.5.16</version.swagger>
</properties>
<modules>
<module>sub-project</module>
</modules>
</project>
""",
spec -> spec.path("pom.xml")
),
mavenProject("sub-project",
//language=xml
pomXml(
"""
<project>
<groupId>com.mycompany.app</groupId>
<artifactId>sub-project</artifactId>
<version>1</version>
<parent>
<groupId>com.mycompany.app</groupId>
<artifactId>parent-project</artifactId>
<version>1</version>
<relativePath>../pom.xml</relativePath>
</parent>
<dependencies>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>${version.swagger}</version>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
<version>${version.swagger}</version>
</dependency>
</dependencies>
</project>
""",
"""
<project>
<groupId>com.mycompany.app</groupId>
<artifactId>sub-project</artifactId>
<version>1</version>
<parent>
<groupId>com.mycompany.app</groupId>
<artifactId>parent-project</artifactId>
<version>1</version>
<relativePath>../pom.xml</relativePath>
</parent>
<dependencies>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
<version>2.2.42</version>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
<version>${version.swagger}</version>
</dependency>
</dependencies>
</project>
""",
spec -> spec.path("sub-project/pom.xml")
)
)
)
);
}

@Issue("https://github.com/openrewrite/rewrite/issues/5965")
@Test
void changeDependencyGroupIdAndArtifactIdForEjbType() {
Expand Down