use crate::models::{EntityKind, ParsedEntity};
pub(crate) fn extract_entities_xml(
source: &str,
file_path: &str,
repo_name: &str,
) -> Vec<ParsedEntity> {
let mut entities = Vec::new();
let doc = match roxmltree::Document::parse(source) {
Ok(d) => d,
Err(_) => return entities,
};
let root = doc.root_element();
let group_id = child_text(&root, "groupId");
let artifact_id = child_text(&root, "artifactId");
let version = child_text(&root, "version").unwrap_or_else(|| "unknown".to_string());
if let (Some(gid), Some(aid)) = (&group_id, &artifact_id) {
let fqn = format!("maven:{}:{}", gid, aid);
entities.push(ParsedEntity::new(
format!("{}:{}", gid, aid),
EntityKind::ProjectIdentity,
&fqn,
Some(format!("version: {}, build_system: maven", version)),
Some(format!("Maven project identity: {}:{}", gid, aid)),
"xml",
file_path,
1,
1,
None,
repo_name,
));
}
for deps_node in root
.descendants()
.filter(|n| n.tag_name().name() == "dependencies")
{
for dep_node in deps_node
.children()
.filter(|n| n.tag_name().name() == "dependency")
{
if let Some(dep_entity) = extract_dependency(&dep_node, file_path, repo_name) {
entities.push(dep_entity);
}
}
}
for build_node in root
.descendants()
.filter(|n| n.tag_name().name() == "build")
{
for plugins_node in build_node
.children()
.filter(|n| n.tag_name().name() == "plugins")
{
for plugin_node in plugins_node
.children()
.filter(|n| n.tag_name().name() == "plugin")
{
if let Some(plugin_entity) = extract_plugin(&plugin_node, file_path, repo_name) {
entities.push(plugin_entity);
}
}
}
}
entities
}
fn extract_dependency(
dep_node: &roxmltree::Node,
file_path: &str,
repo_name: &str,
) -> Option<ParsedEntity> {
let group_id = child_text(dep_node, "groupId")?;
let artifact_id = child_text(dep_node, "artifactId")?;
let version = child_text(dep_node, "version").unwrap_or_else(|| "unknown".to_string());
let scope = child_text(dep_node, "scope");
let name = format!("{}:{}:{}", group_id, artifact_id, version);
let signature = scope.map(|s| format!("scope: {}", s));
Some(ParsedEntity::new(
&name,
EntityKind::BuildDependency,
&name,
signature,
Some(format!("Maven dependency: {}", name)),
"xml",
file_path,
1,
1,
None,
repo_name,
))
}
fn extract_plugin(
plugin_node: &roxmltree::Node,
file_path: &str,
repo_name: &str,
) -> Option<ParsedEntity> {
let group_id = child_text(plugin_node, "groupId")?;
let artifact_id = child_text(plugin_node, "artifactId")?;
let version = child_text(plugin_node, "version").unwrap_or_else(|| "unknown".to_string());
let name = format!("{}:{}:{}", group_id, artifact_id, version);
Some(ParsedEntity::new(
&name,
EntityKind::BuildPlugin,
&name,
None,
Some(format!("Maven plugin: {}", name)),
"xml",
file_path,
1,
1,
None,
repo_name,
))
}
fn child_text(parent: &roxmltree::Node, tag: &str) -> Option<String> {
for child in parent.children() {
if child.tag_name().name() == tag {
return child.text().map(|s| s.trim().to_string());
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_single_maven_dependency() {
let source = r#"<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>test-app</artifactId>
<version>1.0.0</version>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.3.29</version>
</dependency>
</dependencies>
</project>"#;
let entities = extract_entities_xml(source, "pom.xml", "test-repo");
assert_eq!(entities.len(), 2);
let proj_ids: Vec<_> = entities
.iter()
.filter(|e| e.kind == EntityKind::ProjectIdentity)
.collect();
assert_eq!(proj_ids.len(), 1);
assert_eq!(proj_ids[0].name, "com.example:test-app");
assert_eq!(proj_ids[0].fqn, "maven:com.example:test-app");
let deps: Vec<_> = entities
.iter()
.filter(|e| e.kind == EntityKind::BuildDependency)
.collect();
assert_eq!(deps.len(), 1);
assert_eq!(deps[0].name, "org.springframework:spring-core:5.3.29");
assert_eq!(deps[0].kind, EntityKind::BuildDependency);
assert_eq!(deps[0].fqn, "org.springframework:spring-core:5.3.29");
assert_eq!(deps[0].repo_name, "test-repo");
assert!(
deps[0]
.docstring
.as_ref()
.unwrap()
.contains("Maven dependency")
);
}
#[test]
fn test_extract_multiple_maven_dependencies() {
let source = r#"<?xml version="1.0" encoding="UTF-8"?>
<project>
<dependencies>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.20.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>"#;
let entities = extract_entities_xml(source, "pom.xml", "test-repo");
assert_eq!(entities.len(), 3);
assert_eq!(entities[0].name, "com.google.code.gson:gson:2.10.1");
assert_eq!(
entities[1].name,
"org.apache.logging.log4j:log4j-core:2.20.0"
);
assert_eq!(entities[2].name, "junit:junit:4.13.2");
assert!(entities[2].signature.as_ref().unwrap().contains("test"));
}
#[test]
fn test_extract_maven_plugins() {
let source = r#"<?xml version="1.0" encoding="UTF-8"?>
<project>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.7.14</version>
</plugin>
</plugins>
</build>
</project>"#;
let entities = extract_entities_xml(source, "pom.xml", "test-repo");
assert_eq!(entities.len(), 2);
assert_eq!(
entities[0].name,
"org.apache.maven.plugins:maven-compiler-plugin:3.11.0"
);
assert_eq!(entities[0].kind, EntityKind::BuildPlugin);
assert!(
entities[0]
.docstring
.as_ref()
.unwrap()
.contains("Maven plugin")
);
assert_eq!(
entities[1].name,
"org.springframework.boot:spring-boot-maven-plugin:2.7.14"
);
}
#[test]
fn test_extract_dependencies_and_plugins_together() {
let source = r#"<?xml version="1.0" encoding="UTF-8"?>
<project>
<dependencies>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
</plugin>
</plugins>
</build>
</project>"#;
let entities = extract_entities_xml(source, "pom.xml", "test-repo");
assert_eq!(entities.len(), 2);
let deps: Vec<_> = entities
.iter()
.filter(|e| e.kind == EntityKind::BuildDependency)
.collect();
assert_eq!(deps.len(), 1);
let plugins: Vec<_> = entities
.iter()
.filter(|e| e.kind == EntityKind::BuildPlugin)
.collect();
assert_eq!(plugins.len(), 1);
}
#[test]
fn test_extract_empty_pom() {
let source = r#"<?xml version="1.0" encoding="UTF-8"?>
<project>
<modelVersion>4.0.0</modelVersion>
</project>"#;
let entities = extract_entities_xml(source, "pom.xml", "test-repo");
assert!(entities.is_empty());
}
#[test]
fn test_extract_dependency_without_version() {
let source = r#"<?xml version="1.0" encoding="UTF-8"?>
<project>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>managed-lib</artifactId>
</dependency>
</dependencies>
</project>"#;
let entities = extract_entities_xml(source, "pom.xml", "test-repo");
assert_eq!(entities.len(), 1);
assert_eq!(entities[0].name, "com.example:managed-lib:unknown");
}
#[test]
fn test_extract_maven_project_identity() {
let source = r#"<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>my-app</artifactId>
<version>2.0.1</version>
</project>"#;
let entities = extract_entities_xml(source, "pom.xml", "test-repo");
let proj_ids: Vec<_> = entities
.iter()
.filter(|e| e.kind == EntityKind::ProjectIdentity)
.collect();
assert_eq!(proj_ids.len(), 1);
assert_eq!(proj_ids[0].name, "com.example:my-app");
assert_eq!(proj_ids[0].fqn, "maven:com.example:my-app");
assert!(
proj_ids[0]
.signature
.as_ref()
.unwrap()
.contains("build_system: maven")
);
}
#[test]
fn test_extract_maven_project_identity_with_dependencies() {
let source = r#"<?xml version="1.0" encoding="UTF-8"?>
<project>
<groupId>com.example</groupId>
<artifactId>test-app</artifactId>
<version>1.0.0</version>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.3.29</version>
</dependency>
</dependencies>
</project>"#;
let entities = extract_entities_xml(source, "pom.xml", "test-repo");
assert_eq!(entities.len(), 2);
let proj_ids: Vec<_> = entities
.iter()
.filter(|e| e.kind == EntityKind::ProjectIdentity)
.collect();
assert_eq!(proj_ids.len(), 1);
assert_eq!(proj_ids[0].name, "com.example:test-app");
let deps: Vec<_> = entities
.iter()
.filter(|e| e.kind == EntityKind::BuildDependency)
.collect();
assert_eq!(deps.len(), 1);
}
}