use super::{IndexError, PackageIndex, PackageMeta, VersionMeta};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MavenRepo {
Central,
Google,
Sonatype,
}
impl MavenRepo {
fn base_url(&self) -> &'static str {
match self {
Self::Central => "https://repo1.maven.org/maven2",
Self::Google => "https://maven.google.com",
Self::Sonatype => "https://oss.sonatype.org/content/repositories/releases",
}
}
pub fn name(&self) -> &'static str {
match self {
Self::Central => "central",
Self::Google => "google",
Self::Sonatype => "sonatype",
}
}
pub fn all() -> &'static [MavenRepo] {
&[Self::Central, Self::Google, Self::Sonatype]
}
pub fn central() -> &'static [MavenRepo] {
&[Self::Central]
}
pub fn google() -> &'static [MavenRepo] {
&[Self::Google]
}
pub fn android() -> &'static [MavenRepo] {
&[Self::Central, Self::Google]
}
}
pub struct Maven {
repos: Vec<MavenRepo>,
}
impl Maven {
pub fn all() -> Self {
Self {
repos: MavenRepo::all().to_vec(),
}
}
pub fn central() -> Self {
Self {
repos: MavenRepo::central().to_vec(),
}
}
pub fn google() -> Self {
Self {
repos: MavenRepo::google().to_vec(),
}
}
pub fn android() -> Self {
Self {
repos: MavenRepo::android().to_vec(),
}
}
pub fn with_repos(repos: &[MavenRepo]) -> Self {
Self {
repos: repos.to_vec(),
}
}
fn check_repo_exists(
group_id: &str,
artifact_id: &str,
repo: MavenRepo,
) -> Result<(String, MavenRepo), IndexError> {
let group_path = group_id.replace('.', "/");
let metadata_url = format!(
"{}/{}/{}/maven-metadata.xml",
repo.base_url(),
group_path,
artifact_id
);
let response = ureq::get(&metadata_url)
.call()
.map_err(|_| IndexError::NotFound(format!("{}:{}", group_id, artifact_id)))?;
let body = response
.into_string()
.map_err(|e| IndexError::Parse(e.to_string()))?;
let version = extract_version_from_metadata(&body).unwrap_or_else(|| "unknown".to_string());
Ok((version, repo))
}
fn fetch_from_central_api(
group_id: &str,
artifact_id: &str,
) -> Result<(PackageMeta, MavenRepo), IndexError> {
let url = format!(
"https://search.maven.org/solrsearch/select?q=g:{}+AND+a:{}&rows=1&wt=json",
group_id, artifact_id
);
let response: serde_json::Value = ureq::get(&url).call()?.into_json()?;
let docs = response["response"]["docs"]
.as_array()
.ok_or_else(|| IndexError::Parse("missing docs".into()))?;
let doc = docs
.first()
.ok_or_else(|| IndexError::NotFound(format!("{}:{}", group_id, artifact_id)))?;
let mut extra = HashMap::new();
extra.insert(
"source_repo".to_string(),
serde_json::Value::String("central".to_string()),
);
let g = doc["g"].as_str().unwrap_or("");
let a = doc["a"].as_str().unwrap_or("");
Ok((
PackageMeta {
name: format!("{}:{}", g, a),
version: doc["latestVersion"]
.as_str()
.or_else(|| doc["v"].as_str())
.unwrap_or("unknown")
.to_string(),
description: None,
homepage: Some(format!("https://mvnrepository.com/artifact/{}/{}", g, a)),
repository: None,
license: None,
binaries: Vec::new(),
keywords: Vec::new(),
maintainers: Vec::new(),
published: None,
downloads: None,
archive_url: None,
checksum: None,
extra,
},
MavenRepo::Central,
))
}
fn search_central(query: &str) -> Result<Vec<PackageMeta>, IndexError> {
let url = format!(
"https://search.maven.org/solrsearch/select?q={}&rows=50&wt=json",
query
);
let response: serde_json::Value = ureq::get(&url).call()?.into_json()?;
let docs = response["response"]["docs"]
.as_array()
.ok_or_else(|| IndexError::Parse("missing docs".into()))?;
let mut extra = HashMap::new();
extra.insert(
"source_repo".to_string(),
serde_json::Value::String("central".to_string()),
);
Ok(docs
.iter()
.map(|doc| PackageMeta {
name: format!(
"{}:{}",
doc["g"].as_str().unwrap_or(""),
doc["a"].as_str().unwrap_or("")
),
version: doc["latestVersion"]
.as_str()
.or_else(|| doc["v"].as_str())
.unwrap_or("unknown")
.to_string(),
description: None,
homepage: Some(format!(
"https://mvnrepository.com/artifact/{}/{}",
doc["g"].as_str().unwrap_or(""),
doc["a"].as_str().unwrap_or("")
)),
repository: None,
license: None,
binaries: Vec::new(),
extra: extra.clone(),
keywords: Vec::new(),
maintainers: Vec::new(),
published: None,
downloads: None,
archive_url: None,
checksum: None,
})
.collect())
}
}
impl PackageIndex for Maven {
fn ecosystem(&self) -> &'static str {
"maven"
}
fn display_name(&self) -> &'static str {
"Maven (Java)"
}
fn fetch(&self, name: &str) -> Result<PackageMeta, IndexError> {
let (group_id, artifact_id) = if let Some((g, a)) = name.split_once(':') {
(g, a)
} else {
return self
.search(name)?
.into_iter()
.next()
.ok_or_else(|| IndexError::NotFound(name.to_string()));
};
if self.repos.contains(&MavenRepo::Central)
&& let Ok((pkg, _)) = Self::fetch_from_central_api(group_id, artifact_id)
{
return Ok(pkg);
}
for &repo in &self.repos {
if let Ok((version, repo)) = Self::check_repo_exists(group_id, artifact_id, repo) {
let mut extra = HashMap::new();
extra.insert(
"source_repo".to_string(),
serde_json::Value::String(repo.name().to_string()),
);
return Ok(PackageMeta {
name: format!("{}:{}", group_id, artifact_id),
version,
description: None,
homepage: Some(format!(
"https://mvnrepository.com/artifact/{}/{}",
group_id, artifact_id
)),
repository: None,
license: None,
binaries: Vec::new(),
extra,
keywords: Vec::new(),
maintainers: Vec::new(),
published: None,
downloads: None,
archive_url: None,
checksum: None,
});
}
}
Err(IndexError::NotFound(name.to_string()))
}
fn fetch_versions(&self, name: &str) -> Result<Vec<VersionMeta>, IndexError> {
let (group_id, artifact_id) = if let Some((g, a)) = name.split_once(':') {
(g, a)
} else {
return Err(IndexError::Parse(
"Maven package name must be groupId:artifactId".into(),
));
};
let mut all_versions: Vec<(String, MavenRepo)> = Vec::new();
if self.repos.contains(&MavenRepo::Central) {
let url = format!(
"https://search.maven.org/solrsearch/select?q=g:{}+AND+a:{}&core=gav&rows=100&wt=json",
group_id, artifact_id
);
if let Ok(response) = ureq::get(&url).call()
&& let Ok(json) = response.into_json::<serde_json::Value>()
&& let Some(docs) = json["response"]["docs"].as_array()
{
for doc in docs {
if let Some(v) = doc["v"].as_str() {
all_versions.push((v.to_string(), MavenRepo::Central));
}
}
}
}
for &repo in &self.repos {
if repo == MavenRepo::Central && !all_versions.is_empty() {
continue; }
let group_path = group_id.replace('.', "/");
let metadata_url = format!(
"{}/{}/{}/maven-metadata.xml",
repo.base_url(),
group_path,
artifact_id
);
if let Ok(response) = ureq::get(&metadata_url).call()
&& let Ok(body) = response.into_string()
{
for version in extract_versions_from_metadata(&body) {
if !all_versions.iter().any(|(v, _)| v == &version) {
all_versions.push((version, repo));
}
}
}
}
if all_versions.is_empty() {
return Err(IndexError::NotFound(name.to_string()));
}
Ok(all_versions
.into_iter()
.map(|(version, repo)| VersionMeta {
version: format!("{} ({})", version, repo.name()),
released: None,
yanked: false,
})
.collect())
}
fn search(&self, query: &str) -> Result<Vec<PackageMeta>, IndexError> {
if self.repos.contains(&MavenRepo::Central) {
return Self::search_central(query);
}
Err(IndexError::Parse(
"Search only available for Maven Central".into(),
))
}
}
fn extract_version_from_metadata(xml: &str) -> Option<String> {
for tag in ["<latest>", "<release>"] {
if let Some(start) = xml.find(tag) {
let start = start + tag.len();
let end_tag = tag.replace('<', "</");
if let Some(end) = xml[start..].find(&end_tag) {
return Some(xml[start..start + end].to_string());
}
}
}
None
}
fn extract_versions_from_metadata(xml: &str) -> Vec<String> {
let mut versions = Vec::new();
let mut search_start = 0;
while let Some(pos) = xml[search_start..].find("<version>") {
let start = search_start + pos + "<version>".len();
if let Some(end) = xml[start..].find("</version>") {
versions.push(xml[start..start + end].to_string());
search_start = start + end;
} else {
break;
}
}
versions
}