PomModel.java
package network.ike.plugin.ws;
import network.ike.plugin.PomRewriter;
import org.apache.maven.api.model.Build;
import org.apache.maven.api.model.Dependency;
import org.apache.maven.api.model.DependencyManagement;
import org.apache.maven.api.model.Model;
import org.apache.maven.api.model.Parent;
import org.apache.maven.api.model.Plugin;
import org.apache.maven.api.model.PluginManagement;
import org.apache.maven.model.v4.MavenStaxReader;
import javax.xml.stream.XMLStreamException;
import java.io.IOException;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* Read-only POM model backed by Maven 4's {@code maven-api-model}.
*
* <p>Parses a POM file using {@link MavenStaxReader} with location
* tracking enabled. Provides typed access to dependencies, properties,
* and parent — replacing regex-based extraction throughout the
* workspace plugin.
*
* <p>For writes, static utility methods delegate to {@link PomRewriter},
* which uses OpenRewrite's XML Lossless Semantic Tree to update version
* elements in place — preserving whitespace, comments, and quote style.
* Regex/sed-style substitution on POMs is forbidden; see
* {@code feedback_no_sed_on_poms}.
*/
public final class PomModel {
private final Model model;
private final String content;
private PomModel(Model model, String content) {
this.model = model;
this.content = content;
}
/**
* Parse a POM file into a model with location tracking.
*
* @param pomFile path to pom.xml
* @return parsed model
* @throws IOException if the file cannot be read or parsed
*/
public static PomModel parse(Path pomFile) throws IOException {
String content = Files.readString(pomFile, StandardCharsets.UTF_8);
MavenStaxReader reader = new MavenStaxReader();
reader.setAddLocationInformation(true);
try {
Model model = reader.read(new StringReader(content), true, null);
return new PomModel(model, content);
} catch (XMLStreamException e) {
throw new IOException("Cannot parse " + pomFile + ": "
+ e.getMessage(), e);
}
}
/** The underlying Maven 4 model. */
public Model model() { return model; }
/** Raw POM text for targeted editing. */
public String content() { return content; }
// ── Reading ────────────────────────────────────────────────────
/**
* All dependencies from both {@code <dependencies>} and
* {@code <dependencyManagement>} sections.
*/
public List<Dependency> allDependencies() {
List<Dependency> result = new ArrayList<>(model.getDependencies());
DependencyManagement mgmt = model.getDependencyManagement();
if (mgmt != null) {
result.addAll(mgmt.getDependencies());
}
return Collections.unmodifiableList(result);
}
/**
* All plugins from both {@code <build><plugins>} and
* {@code <build><pluginManagement><plugins>} sections.
*/
public List<Plugin> allPlugins() {
List<Plugin> result = new ArrayList<>();
Build build = model.getBuild();
if (build != null) {
result.addAll(build.getPlugins());
PluginManagement mgmt = build.getPluginManagement();
if (mgmt != null) {
result.addAll(mgmt.getPlugins());
}
}
return Collections.unmodifiableList(result);
}
/** Properties from {@code <properties>}. */
public Map<String, String> properties() {
return model.getProperties();
}
/** Parent info, or null if no parent block. */
public Parent parent() {
return model.getParent();
}
/** The project's own groupId (may be null if inherited). */
String groupId() {
String gid = model.getGroupId();
if (gid != null) return gid;
Parent p = model.getParent();
return p != null ? p.getGroupId() : null;
}
/** The project's own artifactId. */
String artifactId() {
return model.getArtifactId();
}
/** The project's own version (may be null if inherited). */
String version() {
String v = model.getVersion();
if (v != null) return v;
Parent p = model.getParent();
return p != null ? p.getVersion() : null;
}
/** Subprojects (Maven 4.1.0) or modules (Maven 4.0.0). */
@SuppressWarnings("deprecation") // getModules() fallback for POM 4.0.0
List<String> subprojects() {
List<String> subs = model.getSubprojects();
if (subs != null && !subs.isEmpty()) return subs;
return model.getModules();
}
/**
* BOM imports from {@code <dependencyManagement>} — dependencies
* with {@code <type>pom</type>} and {@code <scope>import</scope>}.
*
* <p>Uses the Maven 4 model API for precise detection instead of
* regex parsing (#47). Property references (e.g., {@code ${foo.version}})
* are resolved against the POM's {@code <properties>} block.
*
* @return list of BOM import dependencies (unmodifiable)
*/
List<Dependency> bomImports() {
DependencyManagement mgmt = model.getDependencyManagement();
if (mgmt == null) return List.of();
Map<String, String> props = model.getProperties();
return mgmt.getDependencies().stream()
.filter(d -> "pom".equals(d.getType())
&& "import".equals(d.getScope()))
.map(d -> {
// Resolve ${property} in version
String version = d.getVersion();
if (version != null && version.startsWith("${")
&& version.endsWith("}")) {
String propName = version.substring(2, version.length() - 1);
String resolved = props.get(propName);
if (resolved != null) {
return d.withVersion(resolved);
}
}
return d;
})
.toList();
}
// ── Writing (OpenRewrite XML LST via PomRewriter) ──────────────
/**
* Update the version of a specific dependency identified by
* {@code groupId:artifactId}. Uses OpenRewrite LST for
* element-order-tolerant matching within dependency blocks.
*
* @param pomContent the raw POM text
* @param groupId dependency groupId to match
* @param artifactId dependency artifactId to match
* @param newVersion the version to set
* @return updated POM text, or unchanged if no match
*/
public static String updateDependencyVersion(String pomContent,
String groupId,
String artifactId,
String newVersion) {
return PomRewriter.updateDependencyVersion(
pomContent, groupId, artifactId, newVersion);
}
/**
* Update a version property value in the POM content.
* Uses OpenRewrite LST for precise property matching.
*
* @param pomContent the raw POM text
* @param propertyName the property name (e.g., "tinkar-core.version")
* @param newValue the new property value
* @return updated POM text
*/
public static String updateProperty(String pomContent,
String propertyName,
String newValue) {
return PomRewriter.updateProperty(
pomContent, propertyName, newValue);
}
/**
* Update the parent version in a POM's {@code <parent>} block.
* Uses OpenRewrite LST for element-order-tolerant matching.
*
* <p>Matching requires <strong>both</strong> groupId and
* artifactId to match — see issue #241.
*
* @param pomContent the raw POM text
* @param parentGroupId the parent groupId to match
* @param parentArtifactId the parent artifactId to match
* @param newVersion the new version to set
* @return updated POM text, or unchanged if no match
*/
public static String updateParentVersion(String pomContent,
String parentGroupId,
String parentArtifactId,
String newVersion) {
return PomRewriter.updateParentVersion(
pomContent, parentGroupId, parentArtifactId, newVersion);
}
/**
* Update the version of a specific plugin identified by
* {@code groupId:artifactId}. Uses OpenRewrite LST for
* element-order-tolerant matching within plugin blocks,
* including both {@code <plugins>} and {@code <pluginManagement>}.
*
* @param pomContent the raw POM text
* @param groupId plugin groupId to match
* @param artifactId plugin artifactId to match
* @param newVersion the version to set
* @return updated POM text, or unchanged if no match
*/
public static String updatePluginVersion(String pomContent,
String groupId,
String artifactId,
String newVersion) {
return PomRewriter.updatePluginVersion(
pomContent, groupId, artifactId, newVersion);
}
/**
* Remove the {@code <version>} tag from a dependency matched by
* {@code groupId:artifactId}. Used to eliminate intra-reactor
* version pins where the reactor resolves the version automatically.
*
* @param pomContent the raw POM text
* @param groupId dependency groupId to match
* @param artifactId dependency artifactId to match
* @return updated POM text with version removed, or unchanged if no match
*/
static String removeDependencyVersion(String pomContent,
String groupId,
String artifactId) {
return PomRewriter.removeDependencyVersion(
pomContent, groupId, artifactId);
}
}