use std::ffi::OsStr;
use std::path::{Component, Path, PathBuf};
use crate::models::{DatasourceId, FileInfo, PackageUid};
use super::path_identity::{normalize_lexical_path, scanned_file_dir, scanned_path};
pub(super) struct MavenReactorRootHint {
pub(super) root_dir: PathBuf,
pub(super) root_pom_idx: usize,
pub(super) module_paths: Vec<String>,
}
pub(super) struct MavenReactorDomain {
pub(super) root_dir: PathBuf,
pub(super) root_pom_idx: usize,
pub(super) member_pom_indices: Vec<usize>,
}
pub(super) fn collect_maven_reactor_hints(files: &[FileInfo]) -> Vec<MavenReactorRootHint> {
let mut hints = Vec::new();
for (idx, file) in files.iter().enumerate() {
let path = Path::new(&file.path);
if path.file_name().and_then(|name| name.to_str()) != Some("pom.xml") {
continue;
}
for pkg_data in &file.package_data {
if pkg_data.datasource_id != Some(DatasourceId::MavenPom) {
continue;
}
let module_paths: Vec<String> = pkg_data
.extra_data
.as_ref()
.and_then(|extra_data| extra_data.get("modules"))
.and_then(|modules| modules.as_array())
.map(|modules| {
modules
.iter()
.filter_map(|module| module.as_str())
.map(|module| module.to_string())
.collect()
})
.unwrap_or_default();
if module_paths.is_empty() {
continue;
}
let Some(root_dir) = scanned_file_dir(&file.path) else {
continue;
};
hints.push(MavenReactorRootHint {
root_dir,
root_pom_idx: idx,
module_paths,
});
}
}
hints.sort_by(|left, right| left.root_dir.cmp(&right.root_dir));
hints
}
pub(super) fn plan_maven_reactor_domains(
files: &[FileInfo],
reactor_hints: &[&MavenReactorRootHint],
) -> Vec<MavenReactorDomain> {
let mut domains = Vec::new();
for hint in reactor_hints {
domains.push(MavenReactorDomain {
root_dir: hint.root_dir.clone(),
root_pom_idx: hint.root_pom_idx,
member_pom_indices: resolve_maven_reactor_members(files, hint),
});
}
domains.sort_by(|left, right| left.root_dir.cmp(&right.root_dir));
domains
}
fn resolve_maven_reactor_members(files: &[FileInfo], hint: &MavenReactorRootHint) -> Vec<usize> {
let mut member_indices = Vec::new();
for module in &hint.module_paths {
let candidate_path = normalize_lexical_path(&hint.root_dir.join(module)).join("pom.xml");
let Some(found_idx) = files.iter().position(|file| {
scanned_path(&file.path) == candidate_path
}) else {
continue;
};
let has_identity = files[found_idx].package_data.iter().any(|pkg_data| {
pkg_data.datasource_id == Some(DatasourceId::MavenPom) && pkg_data.purl.is_some()
});
if has_identity {
member_indices.push(found_idx);
}
}
member_indices
}
pub(super) fn apply_maven_reactor_domains<'a>(
domains: impl IntoIterator<Item = &'a MavenReactorDomain>,
files: &mut [FileInfo],
) {
let mut scope_roots: Vec<PathBuf> = Vec::new();
let mut anchors: Vec<(PathBuf, Vec<PackageUid>)> = Vec::new();
for domain in domains {
let root_dir = normalize_lexical_path(&domain.root_dir);
scope_roots.push(root_dir.clone());
anchors.push((root_dir, files[domain.root_pom_idx].for_packages.clone()));
for &member_idx in &domain.member_pom_indices {
let Some(member_dir) = scanned_file_dir(&files[member_idx].path) else {
continue;
};
scope_roots.push(member_dir.clone());
anchors.push((member_dir, files[member_idx].for_packages.clone()));
}
}
if anchors.is_empty() {
return;
}
for file in files.iter_mut() {
if !file.for_packages.is_empty() {
continue;
}
let Some(file_dir) = scanned_file_dir(&file.path) else {
continue;
};
if !scope_roots.iter().any(|root| file_dir.starts_with(root)) {
continue;
}
let Some((anchor_dir, package_uids)) = anchors
.iter()
.filter(|(anchor_dir, _)| file_dir.starts_with(anchor_dir))
.max_by_key(|(anchor_dir, _)| anchor_dir.as_os_str().len())
else {
continue;
};
if package_uids.is_empty() || crosses_maven_build_output_dir(anchor_dir, &file_dir) {
continue;
}
file.for_packages.extend(package_uids.iter().cloned());
}
}
fn crosses_maven_build_output_dir(anchor_dir: &Path, file_dir: &Path) -> bool {
file_dir
.strip_prefix(anchor_dir)
.map(|relative| {
relative.components().next() == Some(Component::Normal(OsStr::new("target")))
})
.unwrap_or(false)
}