Compare commits

...

10 commits

Author SHA1 Message Date
Hautvast, S. (Sander)
bb5517470e wip lookup in configured repositories 2025-09-18 21:39:14 +02:00
Hautvast, S. (Sander)
3f30afa689 typo 2025-09-16 21:46:30 +02:00
Hautvast, S. (Sander)
ab651b9b97 prepare better repository resolution 2025-09-16 21:45:28 +02:00
Shautvast
935bfb4139 add test, fix bug with missing xml header 2025-09-11 21:17:01 +02:00
Hautvast, S. (Sander)
f6e39067c8 added settings.xml 2025-09-11 20:38:08 +02:00
Shautvast
2deae73132 downloading from Maven central 2025-08-29 11:53:35 +02:00
Shautvast
e7dee59605 downloading from Maven central 2025-08-01 18:25:34 +02:00
Shautvast
25a5bde49c rudimentary reporting 2025-07-31 17:04:01 +02:00
Shautvast
098cb5e0bc added a start of the html overview 2025-07-25 20:46:16 +02:00
Shautvast
856961b8f5 search parent dependency management 2025-07-25 19:51:52 +02:00
22 changed files with 3123 additions and 95 deletions

1996
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -7,3 +7,6 @@ edition = "2024"
log = "0.4"
env_logger = "0.11"
regex="1.11"
maud = "*"
zip = "4.3"
reqwest = { version = "0.12", features = ["blocking", "rustls-tls"] }

View file

@ -4,7 +4,13 @@ COPY . .
RUN cargo install --path .
FROM debian:bullseye-slim
RUN apt-get update && rm -rf /var/lib/apt/lists/*
RUN apt-get update && \
apt-get install -y --no-install-recommends \
pkg-config \
libssl-dev \
build-essential \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /usr/local/cargo/bin/undeepend /usr/local/bin/undeepend
CMD ["undeepend"]

View file

@ -1,15 +1,48 @@
currently implementing in rust:
**currently implementing in rust:**
* V a sax parser to read xml files (and existing xml binding in rust has trouble reading maven properties)
* V a dom parser to get a generic xml representation
* V a pom reader to get a maven specific representation
* V to find out what dependencies you have
* try default localRepository ~/.m2/repository
* V try default localRepository ~/.m2/repository
* load settings.xml
* search dependency in localRepository
* download dependency from remote repo's
* V search dependency in localRepository
* V download dependency from remote repo's
Why rust and not a maven plugin?
* faster
* more challenges
* run it in docker as a separate step
* report in html
* list dependencies in descending 'should-I-use-it-score' order (below)
* drill down to code usage in project
**gradle**
* probably easiest to run gradle itself to get the dependency list
* maybe should've done that with maven as well...
* but currently it's working rather well (as a POC, it's still missing essential features)
**elaborating**
* deciding if you should ditch a dependency, likely involves other factors:
* (dependency) project quality, as defined by:
* date of last commit
* date of highest version on mavencentral
* java version in bytecode (pre/post java11, I would say)
* nr of collaborators
* nr of issues (ratio open/solved vs total)
* nr of superseded transitive dependencies
* reported vulnerabilities
* in some weighted sum(s), yielding a 'should-I-use-it score'
* and replaceability score: how much work to replace it
* how many occurrences of usage?c
* lib or framework?
* this is going to be a large database,
* incrementally populated with data
* what stack?
**Another idea**
* compute amount of (dependency) code that is reachable from the application
* count references (traverse all)
* what to do with dynamically loaded code?

24
index.html Normal file
View file

@ -0,0 +1,24 @@
<!DOCTYPE html><html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Project Dependencies</title>
<style>
body {
font-family: sans-serif;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
th {
background-color: #f2f2f2;
}
</style>
</head>
<body><h1>Project Dependencies</h1><table><thead><tr><th>Group ID</th><th>Artifact ID</th><th>Version</th></tr></thead><tbody></tbody></table></body></html>

17
main.rs
View file

@ -1,17 +0,0 @@
fn main() {
println!("Hello, Rust!");
// Example: Simple calculation
let x = 5;
let y = 10;
let sum = x + y;
println!("The sum of {} and {} is {}", x, y, sum);
// Example: Vector operations
let numbers = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect();
println!("Original: {:?}", numbers);
println!("Doubled: {:?}", doubled);
}

View file

@ -1,2 +1,3 @@
pub mod maven;
pub mod xml;
mod report;

View file

@ -1,12 +1,24 @@
use std::env;
use std::path::PathBuf;
use std::{env, fs};
use undeepend::maven::project::parse_project;
use undeepend::maven::reporter::report;
use undeepend::maven::settings::get_settings;
fn main() {
let args = std::env::args().collect::<Vec<String>>();
let dir = if args.len() ==1 {
let dir = if args.len() == 1 {
env::current_dir().expect("Could not access current directory")
} else { PathBuf::from(&args[1]) };
} else {
PathBuf::from(&args[1])
};
let project = parse_project(&dir).unwrap();
println!("{:?}", project.get_dependencies(&project.root));
fs::write(
PathBuf::from("index.html"),
project.generate_dependency_html(),
)
.unwrap();
report(&project);
}

79
src/maven/common_model.rs Normal file
View file

@ -0,0 +1,79 @@
use crate::xml::dom_parser::Node;
#[derive(Debug, Clone)]
pub struct Repository {
pub releases: Option<RepositoryPolicy>,
pub snapshots: Option<RepositoryPolicy>,
pub id: Option<String>,
pub name: Option<String>,
pub url: Option<String>,
pub layout: String,
}
#[derive(Debug, Clone)]
pub struct RepositoryPolicy {
pub enabled: bool,
pub update_policy: Option<String>,
pub checksum_policy: Option<String>,
}
pub fn get_repositories(element: Node) -> Vec<Repository> {
let mut repositories = vec![];
for child in element.children {
match child.name.as_str() {
"repository" => repositories.push(get_repository(child)),
_ => {}
}
}
repositories
}
fn get_repository(element: Node) -> Repository {
let mut releases = None;
let mut snapshots = None;
let mut id = None;
let mut name = None;
let mut url = None;
let mut layout = "default".to_owned();
for child in element.children {
match child.name.as_str() {
"releases" => releases = Some(get_update_policy(child)),
"snapshots" => snapshots = Some(get_update_policy(child)),
"id" => id = child.text,
"name" => name = child.text,
"url" => url = child.text,
"layout" => layout = child.text.unwrap_or("default".to_owned()),
_ => {}
}
}
Repository {
releases,
snapshots,
id,
name,
url,
layout,
}
}
fn get_update_policy(element: Node) -> RepositoryPolicy {
let mut enabled = true;
let mut update_policy = None;
let mut checksum_policy = None;
for child in element.children {
match child.name.as_str() {
"enabled" => enabled = child.text.map(|b| b == "true").unwrap_or(true),
"update_policy" => update_policy = child.text,
"checksum_policy" => checksum_policy = child.text,
_ => {}
}
}
RepositoryPolicy {
enabled,
update_policy,
checksum_policy,
}
}

View file

@ -1,4 +1,15 @@
use std::{env, sync::LazyLock};
pub mod common_model;
pub mod metadata;
pub mod pom;
pub mod pom_parser;
pub mod project;
pub mod reporter;
pub mod settings;
pub const HOME: LazyLock<String> = LazyLock::new(|| env::var("HOME").unwrap());
pub const MAVEN_HOME: LazyLock<String> =
LazyLock::new(|| env::var("MAVEN_HOME").unwrap_or("".to_string()));
pub const CUSTOM_SETTINGS_LOCATION: LazyLock<String> =
LazyLock::new(|| env::var("SETTINGS_PATH").unwrap_or("".to_string()));

View file

@ -1,9 +1,10 @@
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::fmt::Display;
use std::path::PathBuf;
/// the maven object model
#[derive(PartialEq, Debug)]
#[derive(Debug)]
pub struct Pom {
pub parent: Option<Parent>,
pub group_id: Option<String>,
@ -18,11 +19,10 @@ pub struct Pom {
pub module_names: Vec<String>,
pub modules: Vec<Pom>,
pub directory: PathBuf,
pub repositories: Vec<Repository>,
}
impl Pom {
}
impl Pom {}
#[derive(PartialEq, Debug)]
pub struct License {
@ -49,4 +49,55 @@ pub struct Dependency {
pub group_id: String,
pub artifact_id: String,
pub version: Option<String>,
// pub scope: Option<String>, // TODO need this?
}
impl Dependency {
/// returns a relative path to the dependency location
pub fn to_jar_path(&self) -> PathBuf {
let mut path = PathBuf::new();
path.push(self.group_id.replace(".", "/"));
path.push(&self.artifact_id);
let version = self.version.clone().unwrap_or_else(|| "latest".to_string());
path.push(&version);
path.push(format!("{}-{}.jar", &self.artifact_id, &version));
path
// why is the version (in the filename) wrong when I use PathBuf::set_extension("jar") ???
}
/// returns an absolute path based on the default maven localRepository location
// useful?
pub fn to_absolute_jar_path(&self) -> PathBuf {
let mut absolute_path = PathBuf::from(HOME.as_str());
absolute_path.push(".m2/repository");
absolute_path.push(self.to_jar_path());
absolute_path
}
pub fn is_snapshot(&self) -> bool {
self.version
.as_ref()
.map(|v| v.ends_with("SNAPSHOT"))
.unwrap_or(false)
}
}
use std::fmt;
use crate::maven::HOME;
use crate::maven::common_model::Repository;
impl Display for Dependency {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let version = self.version.clone().unwrap_or_else(|| "latest".to_string());
write!(
f,
"{}/{}/{}/{}-{}",
self.group_id.replace(".", "/"),
self.artifact_id,
version,
self.artifact_id,
version
)
}
}

View file

@ -1,3 +1,4 @@
use crate::maven::common_model::get_repositories;
use crate::maven::pom::{Dependency, Developer, Parent, Pom};
use crate::xml::SaxError;
use crate::xml::dom_parser::{Node, get_document};
@ -5,9 +6,9 @@ use std::collections::HashMap;
use std::path::PathBuf;
/// parse the pom.xml into a Pom object (struct)
pub fn get_pom(xml: impl Into<String>) -> Result<Pom, SaxError> {
pub fn get_pom(home_dir: PathBuf, xml: impl Into<String>) -> Result<Pom, SaxError> {
let mut group_id = None;
let mut artefact_id = None;
let mut artifact_id = None;
let mut parent = None;
let mut version = None;
let mut name = None;
@ -17,11 +18,12 @@ pub fn get_pom(xml: impl Into<String>) -> Result<Pom, SaxError> {
let mut dependency_management = vec![];
let mut properties = HashMap::new(); // useless assignments...
let mut module_names = vec![]; // not useless assignment...
let mut repositories = vec![]; // not useless assignment...
for child in get_document(xml.into().as_str())?.root.children {
match child.name.as_str() {
"groupId" => group_id = child.text,
"artifactId" => artefact_id = child.text,
"artifactId" => artifact_id = child.text,
"parent" => parent = Some(get_parent(&child)),
"version" => version = child.text,
"name" => name = child.text,
@ -31,13 +33,20 @@ pub fn get_pom(xml: impl Into<String>) -> Result<Pom, SaxError> {
"dependencyManagement" => dependency_management = get_dependency_mgmt(child),
"properties" => properties = get_properties(child),
"modules" => add_modules(child, &mut module_names),
"repositories" => repositories = get_repositories(child),
_ => {}
}
}
// TODO before returning, calculate all
// * dependency versions
// * repositories
// maybe put that in a separate model struct
Ok(Pom {
parent,
group_id,
artifact_id: artefact_id.unwrap(),
artifact_id: artifact_id.unwrap(),
version,
name,
packaging,
@ -47,11 +56,12 @@ pub fn get_pom(xml: impl Into<String>) -> Result<Pom, SaxError> {
properties,
module_names,
modules: vec![],
directory: PathBuf::new(), // resolved later, make optional?
directory: home_dir,
repositories,
})
}
fn add_modules(element: Node, modules: &mut Vec<String>){
fn add_modules(element: Node, modules: &mut Vec<String>) {
for module in element.children {
modules.push(module.text.expect("Cannot read module name"));
}
@ -91,19 +101,19 @@ fn get_dependencies(element: Node) -> Vec<Dependency> {
fn get_dependency(element: Node) -> Dependency {
let mut grouo_id = None;
let mut artefact_id = None;
let mut artifact_id = None;
let mut version = None;
for node in element.children {
match node.name.as_str() {
"groupId" => grouo_id = node.text,
"artifactId" => artefact_id = node.text,
"artifactId" => artifact_id = node.text,
"version" => version = node.text,
_ => {}
}
}
Dependency {
group_id: grouo_id.unwrap(),
artifact_id: artefact_id.unwrap(),
artifact_id: artifact_id.unwrap(),
version,
}
}
@ -136,19 +146,19 @@ fn get_developer(element: Node) -> Developer {
fn get_parent(element: &Node) -> Parent {
let mut group_id = None;
let mut artefact_id = None;
let mut artifact_id = None;
let mut version = None;
for child in &element.children {
match child.name.as_str() {
"groupId" => group_id = child.text.clone(),
"artefactId" => artefact_id = child.text.clone(),
"artifactId" => artifact_id = child.text.clone(),
"version" => version = child.text.clone(),
_ => {}
}
}
Parent {
group_id: group_id.unwrap(),
artifact_id: artefact_id.unwrap(),
artifact_id: artifact_id.unwrap(),
version: version.unwrap(),
}
}

View file

@ -1,10 +1,17 @@
use crate::maven::common_model::Repository;
use crate::maven::pom::{Dependency, Pom};
use crate::maven::pom_parser::get_pom;
use crate::maven::settings::{Settings, get_settings};
use regex::Regex;
use reqwest::blocking::Client;
use std::fs;
use std::path::Path;
use std::fs::File;
use std::io::{BufWriter, Write};
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
const MAVEN_CENTRAL: &str = "https://repo1.maven.org/maven2/";
static PROPERTY_EXPR: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\$\{(.+)}").unwrap());
/// Loads all poms from a given project directory.
@ -18,15 +25,41 @@ pub fn parse_project(project_dir: &Path) -> Result<Project, String> {
let mut pom_file = project_dir.to_path_buf();
pom_file.push(Path::new("pom.xml"));
if !pom_file.exists(){
return Err(format!("Directory {} does not contain pom.xml", project_dir.to_str().unwrap()));
if !pom_file.exists() {
return Err(format!(
"Directory {} does not contain pom.xml",
project_dir.to_str().unwrap()
));
}
let pom_file = fs::read_to_string(pom_file).map_err(|e| e.to_string())?;
let mut root = get_pom(pom_file).map_err(|e| e.to_string())?;
let mut root = get_pom(project_dir.to_path_buf(), pom_file).map_err(|e| e.to_string())?;
resolve_modules(project_dir, &mut root);
Ok(Project { root })
let project_home = project_dir.to_str().unwrap_or_else(|| "?").to_string();
let settings = get_settings()?;
let mut project = Project {
settings,
project_home,
root,
repositories: vec![],
};
let repositories = project.get_repositories();
project.repositories = repositories; // well this is convoluted
for pom in &project.root.modules {
for dep in &project.get_dependencies(pom) {
let path = PathBuf::from(dep.to_absolute_jar_path());
if !path.exists() {
project
.download(dep)
.expect(&format!("Can't download jar file {}", dep));
}
}
}
Ok(project)
}
// examines modules in pom and loads them
@ -51,21 +84,23 @@ fn read_module_pom(project_dir: &Path, module: &String) -> Pom {
let module_pom =
fs::read_to_string(module_file).expect(format!("Cannot read file {}", module).as_str());
let mut pom =
get_pom(module_pom).expect(format!("Cannot create module pom {}", module).as_str());
pom.directory = module_dir;
pom
get_pom(module_dir, module_pom).expect(format!("Cannot create module pom {}", module).as_str())
}
//main entry to project
//the (root) pom holds the child references to modules
#[derive(Debug)]
pub struct Project {
pub settings: Settings,
pub project_home: String,
pub root: Pom,
pub repositories: Vec<Repository>,
}
impl Project {
/// get a list of dependencies for a pom in the project
///
/// Note to self: maybe calculating the versions should be done earlier
pub fn get_dependencies(&self, pom: &Pom) -> Vec<Dependency> {
pom.dependencies
.iter()
@ -92,52 +127,127 @@ impl Project {
.find(|d| d.group_id == group_id && d.artifact_id == artifact_id)
// extract the version
.and_then(|d| d.version.clone())
// is it a property?
.and_then(|version| {
if PROPERTY_EXPR.is_match(&version) {
let property_name = &PROPERTY_EXPR.captures(&version).unwrap()[1];
// search property in project hierarchy
self.get_property(pom, property_name)
} else {
Some(version)
}
})
.or_else(|| {
// version not set, try dependencyManagement
// TODO also search super poms
pom.dependency_management
self.collect_managed_dependencies(pom, group_id, artifact_id)
.iter()
.find(|d| d.group_id == group_id && d.artifact_id == artifact_id)
.find(|d| d.version.is_some())
.and_then(|d| d.version.clone())
.and_then(|version| {
if PROPERTY_EXPR.is_match(&version) {
let property_name = &PROPERTY_EXPR.captures(&version).unwrap()[1];
self.get_property(pom, property_name)
} else {
Some(version)
}
})
.and_then(|v| {
if PROPERTY_EXPR.is_match(v.as_str()) {
let property_name = &PROPERTY_EXPR.captures(&v).unwrap()[1];
// search property in project hierarchy
self.get_property(pom, property_name).ok()
} else {
Some(v)
}
})
}
fn download(&self, dep: &Dependency) -> Result<(), String> {
// self.repositories has all repos,
// but mirrors are not yet taken into account
let url = format!("{}{}.jar", MAVEN_CENTRAL, dep);
let client = Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.map_err(|e| e.to_string())?;
println!("Downloading {}", &url);
let response = client
.get(&url)
.header("User-Agent", "Maven/1.0")
.send()
.map_err(|e| e.to_string())?;
if response.status().is_success() {
let bytes = response.bytes().map_err(|e| e.to_string())?;
let mut buf_writer = BufWriter::new(
File::create(dep.to_absolute_jar_path()).map_err(|e| e.to_string())?,
);
buf_writer.write_all(&bytes).map_err(|e| e.to_string())?;
buf_writer.flush().map_err(|e| e.to_string())?;
println!("Downloaded {}", &url);
}
Ok(())
}
pub fn get_repositories(&self) -> Vec<Repository> {
let mut repositories = vec![];
for pom in &self.root.modules {
repositories.append(&mut pom.repositories.to_vec());
}
self.add_repositories(&self.root, &mut repositories);
repositories.append(&mut self.settings.get_repositories().to_vec());
repositories
}
fn add_repositories(&self, pom: &Pom, repositories: &mut Vec<Repository>) {
repositories.append(&mut pom.repositories.to_vec());
if let Some(parent) = &pom.parent {
if let Some(parent_pom) = self.get_pom(&parent.group_id, &parent.artifact_id) {
self.add_repositories(parent_pom, repositories);
}
}
}
// searches in managed_dependencies for dependencies
fn collect_managed_dependencies<'a>(
&self,
pom: &'a Pom,
group_id: &str,
artifact_id: &str,
) -> Vec<Dependency> {
fn collect<'a>(
project: &'a Project,
pom: &'a Pom,
deps: &mut Vec<Dependency>,
group_id: &str,
artifact_id: &str,
) {
deps.append(
&mut pom
.dependency_management
.iter()
.filter(|d| d.group_id == group_id && d.artifact_id == artifact_id)
.map(|d| d.clone())
.collect::<Vec<Dependency>>(),
);
if let Some(parent) = &pom.parent {
if let Some(parent_pom) = project.get_pom(&parent.group_id, &parent.artifact_id) {
collect(project, parent_pom, deps, group_id, artifact_id);
}
}
}
let mut dependencies = Vec::new();
collect(self, pom, &mut dependencies, group_id, artifact_id);
dependencies
}
// recursively searches a property going up the chain towards parents
fn get_property(&self, pom: &Pom, name: &str) -> Option<String> {
fn get_property(&self, pom: &Pom, name: &str) -> Result<String, String> {
if pom.properties.contains_key(name) {
pom.properties.get(name).cloned()
pom.properties
.get(name)
.cloned()
.ok_or(format!("Unknown property {}", name))
} else if let Some(parent) = &pom.parent {
if let Some(parent_pom) = self.get_pom(&parent.group_id, &parent.artifact_id) {
self.get_property(parent_pom, name)
} else {
None
Err(format!("Unknown property {}", name))
}
} else {
None
Err(format!("Unknown property {}", name))
}
}
// look up a pom in the project
fn get_pom<'a>(&'a self, group_id: &str, artifact_id: &str) -> Option<&'a Pom> {
// inner function to match poms (by artifactId and groupId)
// (extract if needed elsewhere)
fn is_same(pom: &Pom, group_id: &str, artifact_id: &str) -> bool {
@ -166,5 +276,36 @@ impl Project {
get_project_pom(&self.root, group_id, artifact_id)
}
pub fn iter<'a>(&'a self) -> PomIterator<'a> {
PomIterator {
project: self,
idx: 0,
}
}
}
pub struct PomIterator<'a> {
project: &'a Project,
idx: usize,
}
impl<'a> PomIterator<'a> {
pub fn new(project: &'a Project) -> Self {
PomIterator { project, idx: 0 }
}
}
impl<'a> Iterator for PomIterator<'a> {
type Item = &'a Pom;
fn next(&mut self) -> Option<Self::Item> {
if self.idx < self.project.root.modules.len() {
let module = &self.project.root.modules[self.idx];
self.idx += 1;
Some(module)
} else {
None
}
}
}

71
src/maven/reporter.rs Normal file
View file

@ -0,0 +1,71 @@
use crate::maven::pom::{Dependency, Pom};
use crate::maven::project::Project;
use regex::Regex;
use std::collections::HashSet;
use std::fs::File;
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use zip::ZipArchive;
static CLASS_EXPR: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(.+)/.+\.class").unwrap());
// TODO should not be downloading dependencies
pub fn report(project: &Project) {
let pom = &project.root; // TODO other modules
for dep in &project.get_dependencies(pom) {
let jar_file = File::open(dep.to_absolute_jar_path()).expect("Can't open jar file");
let mut archive = ZipArchive::new(jar_file).expect("Can't read jar file");
let mut packages = HashSet::new();
for i in 0..archive.len() {
let file = archive.by_index(i).expect("Can't read file");
let name = file.name();
if CLASS_EXPR.is_match(name) {
let package = &CLASS_EXPR.captures(name).unwrap()[1];
packages.insert(package.replace("/", ".").to_string());
}
}
analyse_source(&packages, &new_path(&pom.directory, "src/main/java"));
analyse_source(&packages, &new_path(&pom.directory, "src/test/java")); //TODO other src dirs, generated src
}
}
fn new_path(dir: &PathBuf, child: &str) -> PathBuf {
let mut new_dir = dir.clone();
new_dir.push(child);
new_dir
}
fn analyse_source(packages: &HashSet<String>, dir: &Path) {
if dir.exists() {
for entry in dir.read_dir().unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if path.is_dir() {
analyse_source(packages, &path);
} else {
if path.extension().unwrap() == "java" {
analyse(packages, &path);
}
}
}
}
}
// TODO deal with import wildcards
fn analyse(packages: &HashSet<String>, path: &Path) {
let content = std::fs::read_to_string(path).unwrap();
let lines = content.lines();
for line in lines {
if line.contains("import") {
for package in packages {
if line.contains(package) {
println!("{:?}: {}", path, line);
}
}
}
}
}

475
src/maven/settings.rs Normal file
View file

@ -0,0 +1,475 @@
use std::{fs, path::PathBuf, str::FromStr};
use crate::{
maven::{
CUSTOM_SETTINGS_LOCATION, HOME, MAVEN_HOME,
common_model::{Repository, get_repositories},
},
xml::dom_parser::{Node, get_document},
};
pub fn get_settings() -> Result<Settings, String> {
let settings_path = get_settings_path().map_err(|e| e.to_string())?;
get_settings_from_path(settings_path)
}
pub fn get_settings_from_path(settings_path: PathBuf) -> Result<Settings, String> {
let settings = fs::read_to_string(settings_path).map_err(|e| e.to_string())?;
get_settings_from_string(settings)
}
pub fn get_settings_from_string(settings: String) -> Result<Settings, String> {
let mut local_repository = None;
let mut interactive_mode = true;
let mut use_plugin_registry = false;
let mut offline = false;
let mut proxies = vec![];
let mut servers = vec![];
let mut mirrors = vec![];
let mut profiles = vec![];
let mut active_profiles = vec![];
let mut plugin_groups = vec![];
let root = get_document(settings).map_err(|err| err.to_string())?.root;
for child in root.children {
match child.name.as_str() {
"localRepository" => local_repository = child.text,
"interactiveMode" => interactive_mode = child.text.map(|b| b == "true").unwrap_or(true),
"usePluginRegistry" => {
use_plugin_registry = child.text.map(|b| b == "true").unwrap_or(false)
}
"offline" => offline = child.text.map(|b| b == "true").unwrap_or(false),
"proxies" => proxies = get_proxies(child),
"servers" => servers = get_servers(child),
"mirrors" => mirrors = get_mirrors(child),
"profiles" => profiles = get_profiles(child),
"activeProfiles" => active_profiles = get_active_profiles(child),
"pluginGroups" => plugin_groups = get_plugin_groups(child),
_ => {}
};
}
Ok(Settings {
local_repository,
interactive_mode,
use_plugin_registry,
offline,
proxies,
servers,
mirrors,
profiles,
active_profiles,
plugin_groups,
})
}
fn get_proxies(element: Node) -> Vec<Proxy> {
let mut proxies = vec![];
for child in element.children {
proxies.push(get_proxy(child));
}
proxies
}
fn get_active_profiles(element: Node) -> Vec<String> {
let mut active_profiles = vec![];
for child in element.children {
if let Some(active_profile) = child.text {
active_profiles.push(active_profile);
}
}
active_profiles
}
fn get_plugin_groups(element: Node) -> Vec<String> {
let mut plugin_groups = vec![];
for child in element.children {
if let Some(plugin_group) = child.text {
plugin_groups.push(plugin_group);
}
}
plugin_groups
}
fn get_servers(servers_element: Node) -> Vec<Server> {
let mut servers = vec![];
for server_element in servers_element.children {
servers.push(get_server(server_element));
}
servers
}
fn get_mirrors(mirrors_element: Node) -> Vec<Mirror> {
let mut mirrors = vec![];
for mirror_element in mirrors_element.children {
mirrors.push(get_mirror(mirror_element));
}
mirrors
}
fn get_profiles(profiles_element: Node) -> Vec<Profile> {
let mut profiles = vec![];
for mirror_element in profiles_element.children {
profiles.push(get_profile(mirror_element));
}
profiles
}
fn get_server(server_element: Node) -> Server {
let mut id = None;
let mut username = None;
let mut password = None;
let mut private_key = None;
let mut passphrase = None;
let mut file_permissions = None;
let mut directory_permissions = None;
let mut configuration = None;
for child in server_element.children {
match child.name.as_str() {
"id" => id = child.text,
"username" => username = child.text,
"password" => password = child.text,
"private_key" => private_key = child.text,
"passphrase" => passphrase = child.text,
"filePermissions" => file_permissions = child.text,
"directoryPermissions" => directory_permissions = child.text,
"configuration" => configuration = Some(child),
_ => {}
}
}
Server {
id,
username,
password,
private_key,
passphrase,
file_permissions,
directory_permissions,
configuration,
}
}
fn get_proxy(element: Node) -> Proxy {
let mut active = false;
let mut protocol = "http".to_owned();
let mut username = None;
let mut password = None;
let mut port: usize = 8080;
let mut host = None;
let mut non_proxy_hosts = None;
let mut id = None;
for child in element.children {
match child.name.as_str() {
"active" => active = child.text.map(|b| b == "true").unwrap_or(false),
"protocol" => protocol = child.text.unwrap_or("http".to_owned()),
"username" => username = child.text,
"password" => password = child.text,
"port" => {
port = child
.text
.map(|i| {
usize::from_str(&i).expect(&format!("Illegal value for port: '{}'", i))
})
.unwrap_or(8080)
}
"host" => host = child.text,
"non_proxy_hosts" => non_proxy_hosts = child.text,
"id" => id = child.text,
_ => {}
}
}
Proxy {
active,
protocol,
username,
password,
port,
host,
non_proxy_hosts,
id,
}
}
fn get_mirror(mirror_element: Node) -> Mirror {
let mut id = None;
let mut mirror_of = None;
let mut url = None;
let mut name = None;
for child in mirror_element.children {
match child.name.as_str() {
"id" => id = child.text,
"mirror_of" => mirror_of = child.text,
"url" => url = child.text,
"name" => name = child.text,
_ => {}
}
}
Mirror {
id,
mirror_of,
url,
name,
}
}
fn get_profile(profile_element: Node) -> Profile {
let mut id = None;
let mut activation = None;
let mut properties = vec![];
let mut repositories = vec![];
let mut plugin_repositories = vec![];
for child in profile_element.children {
match child.name.as_str() {
"id" => id = child.text,
"activation" => activation = Some(get_activation(child)),
"properties" => properties.append(&mut get_properties(child)),
"repositories" => repositories = get_repositories(child),
"pluginRepositories" => plugin_repositories = get_repositories(child),
_ => {}
}
}
Profile {
id,
activation,
properties,
repositories,
plugin_repositories,
}
}
fn get_activation(activation_element: Node) -> Activation {
let mut active_by_default = false;
let mut jdk = None;
let mut os = None;
let mut property = None;
let mut file = None;
for child in activation_element.children {
match child.name.as_str() {
"activeByDefault" => {
active_by_default = child.text.map(|b| b == "true").unwrap_or(false)
}
"jdk" => jdk = child.text,
"os" => os = Some(get_activation_os(child)),
"property" => property = Some(get_activation_property(child)),
"file" => file = Some(get_activation_file(child)),
_ => {}
}
}
Activation {
active_by_default,
jdk,
os,
property,
file,
}
}
fn get_properties(element: Node) -> Vec<Property> {
let mut properties = vec![];
for child in element.children {
properties.push(Property {
name: child.name,
value: child.text,
});
}
properties
}
fn get_activation_os(element: Node) -> ActivationOs {
let mut name = None;
let mut family = None;
let mut arch = None;
let mut version = None;
for child in element.children {
match child.name.as_str() {
"name" => name = child.text,
"family" => family = child.text,
"arch" => arch = child.text,
"version" => version = child.text,
_ => {}
}
}
ActivationOs {
name,
family,
arch,
version,
}
}
fn get_activation_property(element: Node) -> ActivationProperty {
let mut name = None;
let mut value = None;
for child in element.children {
match child.name.as_str() {
"name" => name = child.text,
"value" => value = child.text,
_ => {}
}
}
ActivationProperty { name, value }
}
fn get_activation_file(element: Node) -> ActivationFile {
let mut missing = None;
let mut exists = None;
for child in element.children {
match child.name.as_str() {
"missing" => missing = child.text,
"exists" => exists = child.text,
_ => {}
}
}
ActivationFile { missing, exists }
}
fn get_settings_path() -> Result<PathBuf, String> {
let mut settings = PathBuf::from_str(HOME.as_str()).map_err(|e| e.to_string())?;
settings.push(".m2/settings.xml");
if !settings.exists() {
settings = PathBuf::from_str(MAVEN_HOME.as_str()).map_err(|e| e.to_string())?;
settings.push("conf/settings.xml");
}
if !settings.exists() {
settings =
PathBuf::from_str(CUSTOM_SETTINGS_LOCATION.as_str()).map_err(|e| e.to_string())?;
if settings.is_dir() {
settings.push("settings.xml");
}
}
Ok(settings)
}
impl Settings {
pub fn get_active_profiles(&self) -> Vec<&Profile> {
self.profiles
.iter()
.filter(|p| {
if let Some(activation) = &p.activation {
activation.active_by_default //TODO other activation types are possible
} else if let Some(id) = &p.id {
self.active_profiles.contains(id)
} else {
false
}
})
.collect()
}
pub fn get_repositories(&self) -> Vec<Repository> {
self.get_active_profiles()
.iter()
.map(|p| &p.repositories)
.flatten()
.cloned()
.collect()
}
pub fn get_plugin_repositories(&self) -> Vec<Repository> {
self.get_active_profiles()
.iter()
.map(|p| &p.plugin_repositories)
.flatten()
.cloned()
.collect()
}
}
#[derive(Debug)]
pub struct Settings {
pub local_repository: Option<String>,
pub interactive_mode: bool,
pub use_plugin_registry: bool,
pub offline: bool,
pub proxies: Vec<Proxy>,
pub servers: Vec<Server>,
pub mirrors: Vec<Mirror>,
pub profiles: Vec<Profile>,
pub active_profiles: Vec<String>,
pub plugin_groups: Vec<String>,
}
#[derive(Debug)]
pub struct Server {
pub id: Option<String>,
pub username: Option<String>,
pub password: Option<String>,
pub private_key: Option<String>,
pub passphrase: Option<String>,
pub file_permissions: Option<String>,
pub directory_permissions: Option<String>,
pub configuration: Option<Node>, //xsd:any
}
#[derive(Debug)]
pub struct Mirror {
pub id: Option<String>,
pub mirror_of: Option<String>,
pub name: Option<String>,
pub url: Option<String>,
}
#[derive(Debug)]
pub struct Proxy {
pub active: bool,
pub protocol: String,
pub username: Option<String>,
pub password: Option<String>,
pub port: usize,
pub host: Option<String>,
pub non_proxy_hosts: Option<String>,
pub id: Option<String>,
}
#[derive(Debug)]
pub struct Profile {
pub id: Option<String>,
pub activation: Option<Activation>,
pub properties: Vec<Property>,
pub repositories: Vec<Repository>,
pub plugin_repositories: Vec<Repository>,
}
#[derive(Debug)]
pub struct Activation {
pub active_by_default: bool,
pub jdk: Option<String>,
pub os: Option<ActivationOs>,
pub property: Option<ActivationProperty>,
pub file: Option<ActivationFile>,
}
#[derive(Debug)]
pub struct ActivationOs {
pub name: Option<String>,
pub family: Option<String>,
pub arch: Option<String>,
pub version: Option<String>,
}
#[derive(Debug)]
pub struct ActivationProperty {
pub name: Option<String>,
pub value: Option<String>,
}
#[derive(Debug)]
pub struct ActivationFile {
pub missing: Option<String>,
pub exists: Option<String>,
}
#[derive(Debug)]
pub struct Property {
pub name: String,
pub value: Option<String>,
}

58
src/report.rs Normal file
View file

@ -0,0 +1,58 @@
use crate::maven::project::Project;
use maud::{DOCTYPE, PreEscaped, html};
impl Project {
pub fn generate_dependency_html(&self) -> String {
let html = html! {
(DOCTYPE)
html {
(PreEscaped(r#"
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Project Dependencies</title>
<style>
body {
font-family: sans-serif;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
th {
background-color: #f2f2f2;
}
</style>
</head>
"#))
body{
h1{"Project Dependencies"}
table{
thead{
tr {
th{"Group ID"}
th{"Artifact ID"}
th{"Version"}
}
}
tbody{
@for dependency in &self.get_dependencies(&self.root) {
tr {
td { (dependency.group_id) }
td { (dependency.artifact_id) }
td { (dependency.version.clone().unwrap()) }
}
}
}
}
}
}
};
html.into_string()
}
}

View file

@ -2,9 +2,9 @@ use crate::xml::sax_parser::parse_string;
use crate::xml::{Attribute, SaxError, SaxHandler};
/// get a generic XML object (Document) from the xml contents. This is called DOM parsing
pub fn get_document(xml: &str) -> Result<Document, SaxError> {
pub fn get_document(xml: impl Into<String>) -> Result<Document, SaxError> {
let mut dom_hax_handler = DomSaxHandler::new();
parse_string(xml, Box::new(&mut dom_hax_handler))?;
parse_string(&xml.into(), Box::new(&mut dom_hax_handler))?;
Ok(dom_hax_handler.into_doc())
}

View file

@ -22,6 +22,7 @@ struct SAXParser<'a> {
xml: Vec<char>,
handler: Box<&'a mut dyn SaxHandler>,
position: usize,
current_line: usize,
current: char,
char_buffer: Vec<char>,
namespace_stack: Vec<(String, isize)>,
@ -35,6 +36,7 @@ impl<'a> SAXParser<'a> {
xml: xml.chars().collect(),
handler,
position: 0,
current_line: 0,
current: '\0',
char_buffer: Vec::new(),
namespace_stack: Vec::new(),
@ -44,10 +46,10 @@ impl<'a> SAXParser<'a> {
fn parse(&mut self) -> Result<(), SaxError> {
self.advance()?;
self.expect(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
"Content is not allowed in prolog.",
)?;
// self.expect(
// "<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
// "Content is not allowed in prolog.",
// ).unwrap_or_default(); // not fatal TODO
self.skip_whitespace()?;
self.handler.start_document();
self.parse_elements()
@ -61,6 +63,12 @@ impl<'a> SAXParser<'a> {
self.char_buffer.clear();
}
self.advance()?;
if self.current == '?' {
self.expect(
"?xml version=\"1.0\" encoding=\"UTF-8\"?>",
"Content is not allowed in prolog.",
)?;
}
if self.current == '!' {
self.skip_comment()?;
} else if self.current != '/' {
@ -111,6 +119,9 @@ impl<'a> SAXParser<'a> {
while c.is_whitespace() {
self.skip_whitespace()?;
if self.current == '/' {
break;
}
atts.push(self.parse_attribute()?);
c = self.advance()?;
}
@ -158,15 +169,21 @@ impl<'a> SAXParser<'a> {
let att_name = self.read_until("=")?;
self.skip_whitespace()?;
self.expect("=", "Expected =")?;
self.expect("\"", "Expected start of attribute value")?;
self.skip_whitespace()?;
self.expect(
r#"""#,
&format!(
"Expected start of attribute value at line {}. Instead found [{}]",
self.current_line, self.current
),
)?;
let att_value = self.read_until("\"")?;
if att_name.starts_with("xmlns:") {
let prefix = att_name[6..].to_string();
self.prefix_mapping
.insert(prefix.clone(), att_value.to_string());
self.handler
.start_prefix_mapping(&prefix, &att_value);
self.handler.start_prefix_mapping(&prefix, &att_value);
}
let namespace = if att_name == "xmlns" {
@ -245,6 +262,10 @@ impl<'a> SAXParser<'a> {
} else {
'\0'
};
// print!("{}", self.current);
if self.current == '\n' {
self.current_line += 1;
}
Ok(self.current)
}

View file

@ -1,2 +1,3 @@
mod pom_parser_test;
mod project_parser_test;
mod settings_test;

View file

@ -1,9 +1,10 @@
use std::path::PathBuf;
use undeepend::maven::pom_parser::get_pom;
#[test]
fn test_pom_parser_is_correct() {
let test_xml = include_str!("../maven/resources/pom.xml");
let pom = get_pom(test_xml).expect("failed to get document");
let pom = get_pom(PathBuf::from("../maven/resources"), test_xml).expect("failed to get document");
assert_eq!(Some("Mockito".to_string()),pom.name);
assert_eq!(Some("org.mockito".to_string()),pom.group_id);
assert_eq!("mockito-core",pom.artifact_id);
@ -27,7 +28,7 @@ fn test_pom_parser_is_correct() {
assert_eq!("objenesis", objenesis.artifact_id);
assert_eq!(Some("1.0".to_string()), objenesis.version);
assert_eq!(2, pom.modules.len());
assert_eq!(2, pom.module_names.len());
assert_eq!("a", pom.module_names[0]);
assert_eq!("b", pom.module_names[1]);

View file

@ -0,0 +1,47 @@
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
http://maven.apache.org/xsd/settings-1.0.0.xsd">
<profiles>
<profile>
<id>github</id>
<repositories>
<repository>
<id>central</id>
<url>https://repo1.maven.org/maven2</url>
</repository>
<repository>
<id>github</id>
<url>https://maven.pkg.github.com/shautvast/JsonToy</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
</profile>
<profile>
<id>reflective</id>
<repositories>
<repository>
<id>central</id>
<url>https://repo1.maven.org/maven2</url>
</repository>
<repository>
<id>github</id>
<url>https://maven.pkg.github.com/shautvast/reflective</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
</profile>
</profiles>
<servers>
<server>
<id>github</id>
<username>shautvast</username>
<password>foobar</password>
</server>
</servers>
</settings>

View file

@ -0,0 +1,8 @@
use undeepend::maven::settings::get_settings_from_string;
#[test]
fn test() {
let settings = include_str!("../maven/resources/settings.xml").to_string();
let settings = get_settings_from_string(settings).expect("no fail");
assert!(!settings.profiles.is_empty());
}