use std::collections::BTreeMap; use async_std::io::ReadExt; use color_eyre::{ eyre::{Context, ContextCompat}, Result, }; use isahc::HttpClient; use serde::{Deserialize, Serialize}; use tracing::{debug, error, info, instrument, trace, warn}; /// Abstraction over an adoptium API instance #[derive(custom_debug::Debug)] pub struct AdoptiumAPI { /// Base URL base_url: String, /// Client #[debug(skip)] client: HttpClient, /// Jvm_Impl string jvm_impl: String, } impl AdoptiumAPI { /// Create a new adoptium api pointing at adoptium pub fn adoptium() -> Result { Ok(AdoptiumAPI { base_url: "https://api.adoptium.net".to_string(), client: HttpClient::new().context("failed to open http client")?, jvm_impl: "hotspot".to_string(), }) } /// Create a new api pointing at semeru pub fn semeru() -> Result { Ok(AdoptiumAPI { base_url: "https://api.adoptopenjdk.net".to_string(), client: HttpClient::new().context("failed to open http client")?, jvm_impl: "openj9".to_string(), }) } /// Get the availble releases #[instrument] pub async fn available_releases(&self) -> Result { let response = self .client .get_async(format!("{}/v3/info/available_releases", self.base_url)) .await .context("Failed to request releases")?; let mut body = response.into_body(); let mut contents = String::new(); body.read_to_string(&mut contents) .await .context("Failed to convert body to string")?; let output = serde_json::from_str(&contents).context("Failed to parse json")?; Ok(output) } /// Return the most recent version for the specified release and architecture #[instrument(skip(arch, os), fields(arch = arch.as_ref(), os = os.as_ref()))] pub async fn release( &self, version: u32, arch: impl AsRef, os: impl AsRef, pre_release: bool, ) -> Result { let release_type = if pre_release { "ea" } else { "ga" }; let arch = arch.as_ref(); let os = os.as_ref(); let version = format!("[{version}, {})", version + 1); let version = urlencoding::encode(&version); let url = format!( "{}/v3/info/release_versions?architecture={arch}&heap_size=normal&image_type=jdk&os_type={os}&project=jdk&release_type={release_type}&sort_method=DATE&sort_order=DESC&jvm_impl={}&version={version}", self.base_url, self.jvm_impl, ); let mut response = self .client .get_async(url) .await .context("Failed to request release")? .into_body(); let mut body = String::new(); response .read_to_string(&mut body) .await .context("Failed to convert response to string")?; let output: Versions = serde_json::from_str(&body).context("Failed to parse json")?; let output = output.versions.get(0).context("No Versions")?; Ok(output.clone()) } /// Return latest release #[instrument(skip(arch, os), fields(arch = arch.as_ref(), os = os.as_ref()))] pub async fn latest( &self, version: u32, arch: impl AsRef, os: impl AsRef, pre_release: bool, ) -> Result { let release_type = if pre_release { "ea" } else { "ga" }; debug!(?release_type); let arch = arch.as_ref(); let os = os.as_ref(); let url = format!( "{}/v3/assets/feature_releases/{version}/{release_type}?architecture={arch}&heap_size=normal&image_type=jdk&os={os}&page=0&page_size=10&project=jdk&sort_method=DATE&sort_order=DESC&jvm_impl={}", self.base_url, self.jvm_impl ); trace!(?url); let mut response = self .client .get_async(&url) .await .context("Failed to request release")?; debug!(?response); // If we get a 301, respond to it match response.status().as_u16() { 301 => { let location = response .headers() .get("location") .context("Failed to get redirect location")? .to_str() .context("Failed to parse redirect location")?; warn!(?location, ?url, "Redirecting"); response = self .client .get_async(location) .await .context("Failed to request release")?; debug!(?response, "New response"); } 404 => { error!(?url, "Location not found"); } _ => (), } let mut response = response.into_body(); let mut body = String::new(); response .read_to_string(&mut body) .await .context("Failed to convert response to string")?; let releases: Vec = serde_json::from_str(&body).context("Failed to parse json")?; let release = releases.get(0).context("No releases")?; Ok(release.clone()) } /// Get all versions #[instrument(skip(arch, os), fields(arch = arch.as_ref(), os = os.as_ref()))] pub async fn get_all( &self, arch: impl AsRef, os: impl AsRef, ) -> Result { let input_versions = self .available_releases() .await .context("Failed to get releases")?; let arch = arch.as_ref(); let os = os.as_ref(); info!(?input_versions, "Getting versions"); let mut versions: BTreeMap = BTreeMap::new(); for release in &input_versions.available_releases { match self.latest(*release, arch, os, false).await { Ok(output_release) => { let output_release: OutputRelease = output_release.into(); trace!(?output_release, "Inserting version"); versions.insert( format!("jdk{}", output_release.major_version), output_release, ); } Err(e) => { warn!(?e, ?release, "Version not available for os/architecture"); } }; } info!("Getting latest"); let latest: OutputRelease = match self .latest(input_versions.most_recent_feature_version, arch, os, true) .await { Ok(x) => x.into(), Err(_) => self .latest(input_versions.most_recent_feature_release, arch, os, false) .await .context("Failed to get latest version")? .into(), }; info!("Getting stable"); let stable: OutputRelease = self .latest( input_versions.available_releases[input_versions.available_releases.len() - 1], arch, os, false, ) .await .context("Failed to get version - stable")? .into(); info!("Getting lts"); let lts: OutputRelease = self .latest( input_versions.available_lts_releases [input_versions.available_lts_releases.len() - 1], arch, os, false, ) .await .context("Failed to get version - lts")? .into(); Ok(OutputReleases { versions, latest, stable, lts, }) } } /// Output for an arch #[derive(Clone, Debug, Serialize, Deserialize)] pub struct OutputReleases { versions: BTreeMap, latest: OutputRelease, stable: OutputRelease, lts: OutputRelease, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct OutputRelease { link: String, major_version: u32, sha256: String, java_version: String, } impl From for OutputRelease { fn from(release: Release) -> Self { OutputRelease { link: release.binaries[0].package.link.clone(), major_version: release.version_data.major, sha256: release.binaries[0].package.checksum.clone(), java_version: release.version_data.openjdk_version, } } } /// `/v3/info/available_releases` #[derive(Debug, Serialize, Deserialize)] pub struct AvailableReleases { pub available_releases: Vec, pub available_lts_releases: Vec, pub most_recent_lts: u32, pub most_recent_feature_release: u32, pub most_recent_feature_version: u32, pub tip_version: u32, } /// `/v3/info/release_verions` #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Version { major: u32, minor: u32, security: u32, patch: Option, pre: Option, adopt_build_number: Option, semver: String, openjdk_version: String, build: Option, optional: Option, } #[derive(Debug, Serialize, Deserialize)] pub struct Versions { versions: Vec, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Release { version_data: Version, binaries: Vec, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Binary { architecture: String, package: Package, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Package { checksum: String, link: String, name: String, }