use crate::api::client::ApiClient;
use crate::api::query::PackageQuery;
use crate::error::Result;
use crate::indicator::ProgressIndicator;
use crate::metadata::source::{MetadataSource, PackageDetails};
use crate::models::metadata::JdkMetadata;
use crate::models::package::{ArchiveType, ChecksumType, PackageType};
use crate::models::platform::{Architecture, OperatingSystem};
use crate::version::Version;
use std::str::FromStr;
pub struct FoojayMetadataSource {
client: ApiClient,
}
impl FoojayMetadataSource {
pub fn new() -> Self {
Self {
client: ApiClient::new(),
}
}
pub fn with_base_url(mut self, base_url: String) -> Self {
self.client = self.client.with_base_url(base_url);
self
}
fn convert_package_to_metadata_incomplete(
&self,
package: crate::models::api::Package,
) -> Result<JdkMetadata> {
let version = Version::from_str(&package.java_version)
.unwrap_or_else(|_| Version::new(package.major_version, 0, 0));
let distribution_version =
Version::from_str(&package.distribution_version).unwrap_or_else(|_| version.clone());
let architecture = crate::cache::parse_architecture_from_filename(&package.filename)
.unwrap_or(Architecture::X64);
let operating_system =
OperatingSystem::from_str(&package.operating_system).unwrap_or(OperatingSystem::Linux);
let archive_type =
ArchiveType::from_str(&package.archive_type).unwrap_or(ArchiveType::TarGz);
let package_type = PackageType::from_str(&package.package_type).unwrap_or(PackageType::Jdk);
Ok(JdkMetadata {
id: package.id,
distribution: package.distribution,
version,
distribution_version,
architecture,
operating_system,
package_type,
archive_type,
download_url: None,
checksum: None,
checksum_type: None,
size: package.size,
lib_c_type: package.lib_c_type,
javafx_bundled: package.javafx_bundled,
term_of_support: package.term_of_support,
release_status: package.release_status,
latest_build_available: package.latest_build_available,
})
}
}
impl MetadataSource for FoojayMetadataSource {
fn id(&self) -> &str {
"foojay"
}
fn name(&self) -> &str {
"Foojay Discovery API"
}
fn is_available(&self) -> Result<bool> {
match self.client.get_distributions() {
Ok(_) => Ok(true),
Err(_) => Ok(false),
}
}
fn fetch_all(&self, progress: &mut dyn ProgressIndicator) -> Result<Vec<JdkMetadata>> {
let mut child = progress.create_child();
let config = crate::indicator::ProgressConfig::new(crate::indicator::ProgressStyle::Count)
.with_total(4); child.start(config);
child.update(1, Some(4));
child.set_message("Connecting to Foojay API...".to_string());
let query = PackageQuery {
archive_types: Some(vec![
"tar.gz".to_string(),
"zip".to_string(),
"tgz".to_string(),
]),
..Default::default()
};
child.update(2, Some(4));
child.set_message("Fetching package list...".to_string());
let packages = self.client.get_packages(Some(query))?;
child.update(3, Some(4));
child.set_message(format!("Processing {} packages...", packages.len()));
let result: Result<Vec<JdkMetadata>> = packages
.into_iter()
.map(|pkg| self.convert_package_to_metadata_incomplete(pkg))
.collect();
child.update(4, Some(4));
if let Ok(ref metadata) = result {
child.complete(Some(format!(
"Retrieved {} packages from Foojay",
metadata.len()
)));
progress.set_message(format!("Retrieved {} packages from Foojay", metadata.len()));
} else {
child.error("Failed to process Foojay metadata".to_string());
}
result
}
fn fetch_distribution(
&self,
distribution: &str,
progress: &mut dyn ProgressIndicator,
) -> Result<Vec<JdkMetadata>> {
let mut child = progress.create_child();
let config = crate::indicator::ProgressConfig::new(crate::indicator::ProgressStyle::Count)
.with_total(4); child.start(config);
child.update(1, Some(4));
child.set_message(format!(
"Fetching {distribution} packages from Foojay API..."
));
let query = PackageQuery {
distribution: Some(distribution.to_string()),
archive_types: Some(vec![
"tar.gz".to_string(),
"zip".to_string(),
"tgz".to_string(),
]),
..Default::default()
};
child.update(2, Some(4));
child.set_message(format!("Fetching {distribution} package list..."));
let packages = self.client.get_packages(Some(query))?;
let count = packages.len();
child.update(3, Some(4));
child.set_message(format!("Processing {count} {distribution} packages..."));
let result: Result<Vec<JdkMetadata>> = packages
.into_iter()
.map(|pkg| self.convert_package_to_metadata_incomplete(pkg))
.collect();
child.update(4, Some(4));
if let Ok(ref metadata) = result {
let count = metadata.len();
child.complete(Some(format!(
"Retrieved {count} {distribution} packages from Foojay"
)));
progress.set_message(format!(
"Retrieved {count} {distribution} packages from Foojay"
));
} else {
child.error(format!("Failed to process {distribution} metadata"));
}
result
}
fn fetch_package_details(
&self,
package_id: &str,
progress: &mut dyn ProgressIndicator,
) -> Result<PackageDetails> {
progress.set_message(format!("Fetching package details for {package_id}..."));
let package_info = self.client.get_package_by_id(package_id)?;
let checksum_type = if !package_info.checksum_type.is_empty() {
match package_info.checksum_type.to_lowercase().as_str() {
"sha256" => Some(ChecksumType::Sha256),
"sha512" => Some(ChecksumType::Sha512),
"sha1" => Some(ChecksumType::Sha1),
"md5" => Some(ChecksumType::Md5),
_ => None,
}
} else {
None
};
progress.set_message(format!("Retrieved details for package {package_id}"));
Ok(PackageDetails {
download_url: package_info.direct_download_uri,
checksum: if package_info.checksum.is_empty() {
None
} else {
Some(package_info.checksum)
},
checksum_type,
})
}
fn last_updated(&self) -> Result<Option<chrono::DateTime<chrono::Utc>>> {
Ok(None)
}
}
impl Default for FoojayMetadataSource {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_foojay_metadata_source_id() {
let source = FoojayMetadataSource::new();
assert_eq!(source.id(), "foojay");
assert_eq!(source.name(), "Foojay Discovery API");
}
#[test]
fn test_fetch_all_filters_archive_types() {
let _source = FoojayMetadataSource::new();
let expected_archive_types =
vec!["tar.gz".to_string(), "zip".to_string(), "tgz".to_string()];
let query = PackageQuery {
archive_types: Some(expected_archive_types.clone()),
..Default::default()
};
assert!(query.archive_types.is_some());
assert_eq!(query.archive_types.unwrap(), expected_archive_types);
}
#[test]
fn test_convert_package_to_metadata_incomplete() {
let source = FoojayMetadataSource::new();
let api_package = crate::models::api::Package {
id: "test123".to_string(),
distribution: "temurin".to_string(),
major_version: 21,
java_version: "21.0.1".to_string(),
distribution_version: "21.0.1+12".to_string(),
jdk_version: 21,
operating_system: "linux".to_string(),
architecture: Some("x64".to_string()),
package_type: "jdk".to_string(),
archive_type: "tar.gz".to_string(),
filename: "OpenJDK21U-jdk_x64_linux_hotspot_21.0.1_12.tar.gz".to_string(),
directly_downloadable: true,
links: crate::models::api::Links {
pkg_download_redirect: "https://example.com/download".to_string(),
pkg_info_uri: None,
},
free_use_in_production: true,
tck_tested: "yes".to_string(),
size: 195000000,
lib_c_type: Some("glibc".to_string()),
javafx_bundled: false,
term_of_support: Some("lts".to_string()),
release_status: Some("ga".to_string()),
latest_build_available: Some(true),
};
let result = source.convert_package_to_metadata_incomplete(api_package);
assert!(result.is_ok());
let metadata = result.unwrap();
assert_eq!(metadata.id, "test123");
assert_eq!(metadata.distribution, "temurin");
assert_eq!(metadata.version.major(), 21);
assert_eq!(metadata.architecture.to_string(), "x64");
assert_eq!(metadata.download_url, None); assert_eq!(metadata.checksum, None);
assert_eq!(metadata.checksum_type, None);
assert!(!metadata.is_complete()); }
#[test]
fn test_fetch_package_details_parsing() {
let _source = FoojayMetadataSource::new();
let test_cases = vec![
("sha256", Some(ChecksumType::Sha256)),
("SHA256", Some(ChecksumType::Sha256)),
("sha512", Some(ChecksumType::Sha512)),
("sha1", Some(ChecksumType::Sha1)),
("md5", Some(ChecksumType::Md5)),
("unknown", None),
("", None),
];
for (checksum_type_str, expected) in test_cases {
let package_info = crate::models::api::PackageInfo {
filename: "test.tar.gz".to_string(),
direct_download_uri: "https://example.com/download".to_string(),
download_site_uri: None,
checksum: "abc123".to_string(),
checksum_type: checksum_type_str.to_string(),
checksum_uri: "https://example.com/checksum".to_string(),
signature_uri: None,
};
let checksum_type = if !package_info.checksum_type.is_empty() {
match package_info.checksum_type.to_lowercase().as_str() {
"sha256" => Some(ChecksumType::Sha256),
"sha512" => Some(ChecksumType::Sha512),
"sha1" => Some(ChecksumType::Sha1),
"md5" => Some(ChecksumType::Md5),
_ => None,
}
} else {
None
};
assert_eq!(
checksum_type, expected,
"Failed for checksum type: {checksum_type_str}"
);
}
}
}