use std::collections::HashMap;
use std::path::{Path, PathBuf};
use crate::config::Settings;
use crate::project_resolver::{
ResolutionResult, Sha256Hash,
helpers::{
compute_config_shas, extract_language_config_paths, is_language_enabled,
module_for_file_generic, parse_gradle_source_roots,
},
memo::ResolutionMemo,
persist::{ResolutionPersistence, ResolutionRules},
provider::ProjectResolutionProvider,
};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct JavaProjectPath(PathBuf);
impl JavaProjectPath {
pub fn new(path: PathBuf) -> Self {
Self(path)
}
pub fn as_path(&self) -> &PathBuf {
&self.0
}
}
pub struct JavaProvider {
#[allow(dead_code)] memo: ResolutionMemo<HashMap<JavaProjectPath, Sha256Hash>>,
}
impl Default for JavaProvider {
fn default() -> Self {
Self::new()
}
}
impl JavaProvider {
pub fn new() -> Self {
Self {
memo: ResolutionMemo::new(),
}
}
pub fn module_path_for_file(&self, file_path: &Path) -> Option<String> {
module_for_file_generic(file_path, "java", ".")
}
fn parse_maven_config(&self, pom_path: &Path) -> ResolutionResult<Vec<PathBuf>> {
use std::fs;
let content = fs::read_to_string(pom_path).map_err(|e| {
crate::project_resolver::ResolutionError::IoError {
path: pom_path.to_path_buf(),
cause: e.to_string(),
}
})?;
let mut source_roots = Vec::new();
let project_dir = pom_path.parent().unwrap_or(Path::new("."));
if !content.contains("<sourceDirectory>") {
source_roots.push(project_dir.join("src/main/java"));
} else {
if let Some(start) = content.find("<sourceDirectory>") {
if let Some(end) = content[start..].find("</sourceDirectory>") {
let src_dir = &content[start + 17..start + end];
source_roots.push(project_dir.join(src_dir.trim()));
}
}
}
if !content.contains("<testSourceDirectory>") {
source_roots.push(project_dir.join("src/test/java"));
}
Ok(source_roots)
}
fn parse_gradle_config(&self, gradle_path: &Path) -> ResolutionResult<Vec<PathBuf>> {
parse_gradle_source_roots(gradle_path, "java", None)
}
fn build_rules_for_config(&self, config_path: &Path) -> ResolutionResult<ResolutionRules> {
let source_roots = if config_path.file_name().unwrap_or_default() == "pom.xml" {
self.parse_maven_config(config_path)?
} else if config_path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.contains("build.gradle")
{
self.parse_gradle_config(config_path)?
} else {
return Err(crate::project_resolver::ResolutionError::ParseError {
message: format!("Unknown Java config file: {}", config_path.display()),
});
};
let mut paths = HashMap::new();
for root in source_roots {
paths.insert(root.to_string_lossy().to_string(), Vec::new());
}
Ok(ResolutionRules {
base_url: None,
paths,
})
}
}
impl ProjectResolutionProvider for JavaProvider {
fn language_id(&self) -> &'static str {
"java"
}
fn is_enabled(&self, settings: &Settings) -> bool {
is_language_enabled(settings, "java")
}
fn config_paths(&self, settings: &Settings) -> Vec<PathBuf> {
extract_language_config_paths(settings, "java")
}
fn compute_shas(&self, configs: &[PathBuf]) -> ResolutionResult<HashMap<PathBuf, Sha256Hash>> {
compute_config_shas(configs)
}
fn rebuild_cache(&self, settings: &Settings) -> ResolutionResult<()> {
use crate::project_resolver::persist::ResolutionIndex;
let config_paths = self.config_paths(settings);
if config_paths.is_empty() {
return Ok(());
}
let persistence = ResolutionPersistence::new(Path::new(crate::init::local_dir_name()));
let mut index = ResolutionIndex::new();
for config_path in &config_paths {
if !config_path.exists() {
continue;
}
let rules = self.build_rules_for_config(config_path)?;
let project_dir = config_path.parent().unwrap_or(Path::new("."));
let pattern = format!("{}/**/*.java", project_dir.display());
index.mappings.insert(pattern, config_path.clone());
index.rules.insert(config_path.clone(), rules);
}
let shas = self.compute_shas(&config_paths)?;
for (path, sha) in shas {
index.hashes.insert(path, sha.0);
}
persistence.save("java", &index)?;
Ok(())
}
fn select_affected_files(&self, _settings: &Settings) -> Vec<PathBuf> {
vec![]
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_parse_maven_default_source_roots() {
let temp_dir = TempDir::new().unwrap();
let pom_path = temp_dir.path().join("pom.xml");
let pom_content = 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-project</artifactId>
<version>1.0.0</version>
</project>"#;
fs::write(&pom_path, pom_content).unwrap();
let provider = JavaProvider::new();
let roots = provider.parse_maven_config(&pom_path).unwrap();
assert_eq!(roots.len(), 2, "Should have main and test source roots");
assert!(
roots.iter().any(|r| r.ends_with("src/main/java")),
"Should have src/main/java"
);
assert!(
roots.iter().any(|r| r.ends_with("src/test/java")),
"Should have src/test/java"
);
}
#[test]
fn test_parse_maven_custom_source_directory() {
let temp_dir = TempDir::new().unwrap();
let pom_path = temp_dir.path().join("pom.xml");
let pom_content = r#"<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<build>
<sourceDirectory>custom/src/main</sourceDirectory>
</build>
</project>"#;
fs::write(&pom_path, pom_content).unwrap();
let provider = JavaProvider::new();
let roots = provider.parse_maven_config(&pom_path).unwrap();
assert!(
roots.iter().any(|r| r.ends_with("custom/src/main")),
"Should have custom source directory"
);
}
#[test]
#[ignore = "Requires filesystem isolation (changes cwd, conflicts with parallel tests)"]
fn test_rebuild_cache_creates_resolution_json() {
let temp_dir = TempDir::new().unwrap();
let pom_path = temp_dir.path().join("pom.xml");
let codanna_dir = temp_dir.path().join(crate::init::local_dir_name());
let pom_content = r#"<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
</project>"#;
fs::write(&pom_path, pom_content).unwrap();
let settings_content = format!(
r#"
[languages.java]
enabled = true
config_files = ["{}"]
"#,
pom_path.display()
);
let settings: Settings = toml::from_str(&settings_content).unwrap();
let original_dir = std::env::current_dir().unwrap();
let provider = JavaProvider::new();
std::env::set_current_dir(&temp_dir).unwrap();
fs::create_dir_all(&codanna_dir).unwrap();
provider.rebuild_cache(&settings).unwrap();
std::env::set_current_dir(&original_dir).unwrap();
let cache_path = codanna_dir.join("index/resolvers/java_resolution.json");
assert!(
cache_path.exists(),
"Cache file should be created at {}",
cache_path.display()
);
let cache_content = fs::read_to_string(&cache_path).unwrap();
assert!(
cache_content.contains("src/main/java")
|| cache_content.contains("src\\\\main\\\\java"),
"Cache should contain source root path"
);
}
#[test]
#[ignore = "Requires filesystem isolation (changes cwd, conflicts with parallel tests)"]
fn test_module_path_for_file_converts_path_to_package() {
let temp_dir = TempDir::new().unwrap();
let pom_path = temp_dir.path().join("pom.xml");
let pom_content = r#"<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
</project>"#;
fs::write(&pom_path, pom_content).unwrap();
let settings_content = format!(
r#"
[languages.java]
enabled = true
config_files = ["{}"]
"#,
pom_path.display()
);
let settings: Settings = toml::from_str(&settings_content).unwrap();
let original_dir = std::env::current_dir().unwrap();
std::env::set_current_dir(&temp_dir).unwrap();
fs::create_dir_all(temp_dir.path().join(crate::init::local_dir_name())).unwrap();
let provider = JavaProvider::new();
provider.rebuild_cache(&settings).unwrap();
let java_file = temp_dir
.path()
.join("src/main/java/com/example/owner/Owner.java");
fs::create_dir_all(java_file.parent().unwrap()).unwrap();
fs::write(
&java_file,
"package com.example.owner; public class Owner {}",
)
.unwrap();
let cache_path = temp_dir.path().join(format!(
"{}/index/resolvers/java_resolution.json",
crate::init::local_dir_name()
));
let cache_content = fs::read_to_string(&cache_path).unwrap();
eprintln!("Cache content: {cache_content}");
eprintln!("Java file path: {}", java_file.display());
let package = provider.module_path_for_file(&java_file);
assert_eq!(
package,
Some("com.example.owner".to_string()),
"Should convert file path to package notation"
);
std::env::set_current_dir(&original_dir).unwrap();
}
#[test]
fn test_provider_language_id() {
let provider = JavaProvider::new();
assert_eq!(provider.language_id(), "java");
}
#[test]
#[ignore] fn test_package_for_owner_file() {
let provider = JavaProvider::new();
let owner_file = std::path::Path::new(
"/Users/bartolli/Projects/codanna/test_monorepos/spring-petclinic/src/main/java/org/springframework/samples/petclinic/owner/Owner.java",
);
let package = provider.module_path_for_file(owner_file);
println!("module_path_for_file result: {package:?}");
assert_eq!(
package,
Some("org.springframework.samples.petclinic.owner".to_string()),
"Should extract package path from file path"
);
}
}