aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/src-tauri/src/core/java
diff options
context:
space:
mode:
Diffstat (limited to 'src-tauri/src/core/java')
-rw-r--r--src-tauri/src/core/java/detection.rs311
-rw-r--r--src-tauri/src/core/java/error.rs95
-rw-r--r--src-tauri/src/core/java/mod.rs538
-rw-r--r--src-tauri/src/core/java/persistence.rs115
-rw-r--r--src-tauri/src/core/java/priority.rs59
-rw-r--r--src-tauri/src/core/java/provider.rs58
-rw-r--r--src-tauri/src/core/java/providers/adoptium.rs340
-rw-r--r--src-tauri/src/core/java/providers/mod.rs3
-rw-r--r--src-tauri/src/core/java/validation.rs146
9 files changed, 1665 insertions, 0 deletions
diff --git a/src-tauri/src/core/java/detection.rs b/src-tauri/src/core/java/detection.rs
new file mode 100644
index 0000000..08dcebb
--- /dev/null
+++ b/src-tauri/src/core/java/detection.rs
@@ -0,0 +1,311 @@
+use std::io::Read;
+use std::path::Path;
+use std::path::PathBuf;
+use std::process::{Command, Stdio};
+use std::time::Duration;
+
+#[cfg(target_os = "windows")]
+use std::os::windows::process::CommandExt;
+
+use crate::core::java::strip_unc_prefix;
+
+const WHICH_TIMEOUT: Duration = Duration::from_secs(2);
+
+/// Scans a directory for Java installations, filtering out symlinks
+///
+/// # Arguments
+/// * `base_dir` - Base directory to scan (e.g., mise or SDKMAN java dir)
+/// * `should_skip` - Predicate to determine if an entry should be skipped
+///
+/// # Returns
+/// First valid Java installation found, or `None`
+fn scan_java_dir<F>(base_dir: &Path, should_skip: F) -> Option<PathBuf>
+where
+ F: Fn(&std::fs::DirEntry) -> bool,
+{
+ std::fs::read_dir(base_dir)
+ .ok()?
+ .flatten()
+ .filter(|entry| {
+ let path = entry.path();
+ // Only consider real directories, not symlinks
+ path.is_dir() && !path.is_symlink() && !should_skip(entry)
+ })
+ .find_map(|entry| {
+ let java_path = entry.path().join("bin/java");
+ if java_path.exists() && java_path.is_file() {
+ Some(java_path)
+ } else {
+ None
+ }
+ })
+}
+
+/// Finds Java installation from SDKMAN! if available
+///
+/// Scans the SDKMAN! candidates directory and returns the first valid Java installation found.
+/// Skips the 'current' symlink to avoid duplicates.
+///
+/// Path: `~/.sdkman/candidates/java/`
+///
+/// # Returns
+/// `Some(PathBuf)` pointing to `bin/java` if found, `None` otherwise
+pub fn find_sdkman_java() -> Option<PathBuf> {
+ let home = std::env::var("HOME").ok()?;
+ let sdkman_base = PathBuf::from(&home).join(".sdkman/candidates/java/");
+
+ if !sdkman_base.exists() {
+ return None;
+ }
+
+ scan_java_dir(&sdkman_base, |entry| entry.file_name() == "current")
+}
+
+/// Finds Java installation from mise if available
+///
+/// Scans the mise Java installation directory and returns the first valid installation found.
+/// Skips version alias symlinks (e.g., `21`, `21.0`, `latest`, `lts`) to avoid duplicates.
+///
+/// Path: `~/.local/share/mise/installs/java/`
+///
+/// # Returns
+/// `Some(PathBuf)` pointing to `bin/java` if found, `None` otherwise
+pub fn find_mise_java() -> Option<PathBuf> {
+ let home = std::env::var("HOME").ok()?;
+ let mise_base = PathBuf::from(&home).join(".local/share/mise/installs/java/");
+
+ if !mise_base.exists() {
+ return None;
+ }
+
+ scan_java_dir(&mise_base, |_| false) // mise: no additional filtering needed
+}
+
+/// Runs `which` (Unix) or `where` (Windows) command to find Java in PATH with timeout
+///
+/// This function spawns a subprocess to locate the `java` executable in the system PATH.
+/// It enforces a 2-second timeout to prevent hanging if the command takes too long.
+///
+/// # Returns
+/// `Some(String)` containing the output (paths separated by newlines) if successful,
+/// `None` if the command fails, times out, or returns non-zero exit code
+///
+/// # Platform-specific behavior
+/// - Unix/Linux/macOS: Uses `which java`
+/// - Windows: Uses `where java` and hides the console window
+///
+/// # Timeout Behavior
+/// If the command does not complete within 2 seconds, the process is killed
+/// and `None` is returned. This prevents the launcher from hanging on systems
+/// where `which`/`where` may be slow or unresponsive.
+fn run_which_command_with_timeout() -> Option<String> {
+ let mut cmd = Command::new(if cfg!(windows) { "where" } else { "which" });
+ cmd.arg("java");
+ // Hide console window on Windows
+ #[cfg(target_os = "windows")]
+ cmd.creation_flags(0x08000000);
+ cmd.stdout(Stdio::piped());
+
+ let mut child = cmd.spawn().ok()?;
+ let start = std::time::Instant::now();
+
+ loop {
+ // Check if timeout has been exceeded
+ if start.elapsed() > WHICH_TIMEOUT {
+ let _ = child.kill();
+ let _ = child.wait();
+ return None;
+ }
+
+ match child.try_wait() {
+ Ok(Some(status)) => {
+ if status.success() {
+ let mut output = String::new();
+ if let Some(mut stdout) = child.stdout.take() {
+ let _ = stdout.read_to_string(&mut output);
+ }
+ return Some(output);
+ } else {
+ let _ = child.wait();
+ return None;
+ }
+ }
+ Ok(None) => {
+ // Command still running, sleep briefly before checking again
+ std::thread::sleep(Duration::from_millis(50));
+ }
+ Err(_) => {
+ let _ = child.kill();
+ let _ = child.wait();
+ return None;
+ }
+ }
+ }
+}
+
+/// Detects all available Java installations on the system
+///
+/// This function searches for Java installations in multiple locations:
+/// - **All platforms**: `JAVA_HOME` environment variable, `java` in PATH
+/// - **Linux**: `/usr/lib/jvm`, `/usr/java`, `/opt/java`, `/opt/jdk`, `/opt/openjdk`, SDKMAN!
+/// - **macOS**: `/Library/Java/JavaVirtualMachines`, `/System/Library/Java/JavaVirtualMachines`,
+/// Homebrew paths (`/usr/local/opt/openjdk`, `/opt/homebrew/opt/openjdk`), SDKMAN!
+/// - **Windows**: `Program Files`, `Program Files (x86)`, `LOCALAPPDATA` for various JDK distributions
+///
+/// # Returns
+/// A vector of `PathBuf` pointing to Java executables found on the system.
+/// Note: Paths may include symlinks and duplicates; callers should canonicalize and deduplicate as needed.
+///
+/// # Examples
+/// ```ignore
+/// let candidates = get_java_candidates();
+/// for java_path in candidates {
+/// println!("Found Java at: {}", java_path.display());
+/// }
+/// ```
+pub fn get_java_candidates() -> Vec<PathBuf> {
+ let mut candidates = Vec::new();
+
+ // Try to find Java in PATH using 'which' or 'where' command with timeout
+ // CAUTION: linux 'which' may return symlinks, so we need to canonicalize later
+ if let Some(paths_str) = run_which_command_with_timeout() {
+ for line in paths_str.lines() {
+ let path = PathBuf::from(line.trim());
+ if path.exists() {
+ let resolved = std::fs::canonicalize(&path).unwrap_or(path);
+ let final_path = strip_unc_prefix(resolved);
+ candidates.push(final_path);
+ }
+ }
+ }
+
+ #[cfg(target_os = "linux")]
+ {
+ let linux_paths = [
+ "/usr/lib/jvm",
+ "/usr/java",
+ "/opt/java",
+ "/opt/jdk",
+ "/opt/openjdk",
+ ];
+
+ for base in &linux_paths {
+ if let Ok(entries) = std::fs::read_dir(base) {
+ for entry in entries.flatten() {
+ let java_path = entry.path().join("bin/java");
+ if java_path.exists() {
+ candidates.push(java_path);
+ }
+ }
+ }
+ }
+
+ // Check common SDKMAN! java candidates
+ if let Some(sdkman_java) = find_sdkman_java() {
+ candidates.push(sdkman_java);
+ }
+
+ // Check common mise java candidates
+ if let Some(mise_java) = find_mise_java() {
+ candidates.push(mise_java);
+ }
+ }
+
+ #[cfg(target_os = "macos")]
+ {
+ let mac_paths = [
+ "/Library/Java/JavaVirtualMachines",
+ "/System/Library/Java/JavaVirtualMachines",
+ "/usr/local/opt/openjdk/bin/java",
+ "/opt/homebrew/opt/openjdk/bin/java",
+ ];
+
+ for path in &mac_paths {
+ let p = PathBuf::from(path);
+ if p.is_dir() {
+ if let Ok(entries) = std::fs::read_dir(&p) {
+ for entry in entries.flatten() {
+ let java_path = entry.path().join("Contents/Home/bin/java");
+ if java_path.exists() {
+ candidates.push(java_path);
+ }
+ }
+ }
+ } else if p.exists() {
+ candidates.push(p);
+ }
+ }
+
+ // Check common Homebrew java candidates for aarch64 macs
+ let homebrew_arm = PathBuf::from("/opt/homebrew/Cellar/openjdk");
+ if homebrew_arm.exists() {
+ if let Ok(entries) = std::fs::read_dir(&homebrew_arm) {
+ for entry in entries.flatten() {
+ let java_path = entry
+ .path()
+ .join("libexec/openjdk.jdk/Contents/Home/bin/java");
+ if java_path.exists() {
+ candidates.push(java_path);
+ }
+ }
+ }
+ }
+
+ // Check common SDKMAN! java candidates
+ if let Some(sdkman_java) = find_sdkman_java() {
+ candidates.push(sdkman_java);
+ }
+
+ // Check common mise java candidates
+ if let Some(mise_java) = find_mise_java() {
+ candidates.push(mise_java);
+ }
+ }
+
+ #[cfg(target_os = "windows")]
+ {
+ let program_files =
+ std::env::var("ProgramFiles").unwrap_or_else(|_| "C:\\Program Files".to_string());
+ let program_files_x86 = std::env::var("ProgramFiles(x86)")
+ .unwrap_or_else(|_| "C:\\Program Files (x86)".to_string());
+ let local_app_data = std::env::var("LOCALAPPDATA").unwrap_or_default();
+
+ // Common installation paths for various JDK distributions
+ let mut win_paths = vec![];
+ for base in &[&program_files, &program_files_x86, &local_app_data] {
+ win_paths.push(format!("{}\\Java", base));
+ win_paths.push(format!("{}\\Eclipse Adoptium", base));
+ win_paths.push(format!("{}\\AdoptOpenJDK", base));
+ win_paths.push(format!("{}\\Microsoft\\jdk", base));
+ win_paths.push(format!("{}\\Zulu", base));
+ win_paths.push(format!("{}\\Amazon Corretto", base));
+ win_paths.push(format!("{}\\BellSoft\\LibericaJDK", base));
+ win_paths.push(format!("{}\\Programs\\Eclipse Adoptium", base));
+ }
+
+ for base in &win_paths {
+ let base_path = PathBuf::from(base);
+ if base_path.exists() {
+ if let Ok(entries) = std::fs::read_dir(&base_path) {
+ for entry in entries.flatten() {
+ let java_path = entry.path().join("bin\\java.exe");
+ if java_path.exists() {
+ candidates.push(java_path);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Check JAVA_HOME environment variable
+ if let Ok(java_home) = std::env::var("JAVA_HOME") {
+ let bin_name = if cfg!(windows) { "java.exe" } else { "java" };
+ let java_path = PathBuf::from(&java_home).join("bin").join(bin_name);
+ if java_path.exists() {
+ candidates.push(java_path);
+ }
+ }
+
+ candidates
+}
diff --git a/src-tauri/src/core/java/error.rs b/src-tauri/src/core/java/error.rs
new file mode 100644
index 0000000..bf78d3b
--- /dev/null
+++ b/src-tauri/src/core/java/error.rs
@@ -0,0 +1,95 @@
+use std::fmt;
+
+/// Unified error type for Java component operations
+///
+/// This enum represents all possible errors that can occur in the Java component,
+/// providing a consistent error handling interface across all modules.
+#[derive(Debug, Clone)]
+pub enum JavaError {
+ // Java installation not found at the specified path
+ NotFound,
+ // Invalid Java version format or unable to parse version
+ InvalidVersion(String),
+ // Java installation verification failed (e.g., -version command failed)
+ VerificationFailed(String),
+ // Network error during API calls or downloads
+ NetworkError(String),
+ // File I/O error (reading, writing, or accessing files)
+ IoError(String),
+ // Timeout occurred during operation
+ Timeout(String),
+ // Serialization/deserialization error
+ SerializationError(String),
+ // Invalid configuration or parameters
+ InvalidConfig(String),
+ // Download or installation failed
+ DownloadFailed(String),
+ // Extraction or decompression failed
+ ExtractionFailed(String),
+ // Checksum verification failed
+ ChecksumMismatch(String),
+ // Other unspecified errors
+ Other(String),
+}
+
+impl fmt::Display for JavaError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ JavaError::NotFound => write!(f, "Java installation not found"),
+ JavaError::InvalidVersion(msg) => write!(f, "Invalid Java version: {}", msg),
+ JavaError::VerificationFailed(msg) => write!(f, "Java verification failed: {}", msg),
+ JavaError::NetworkError(msg) => write!(f, "Network error: {}", msg),
+ JavaError::IoError(msg) => write!(f, "I/O error: {}", msg),
+ JavaError::Timeout(msg) => write!(f, "Operation timeout: {}", msg),
+ JavaError::SerializationError(msg) => write!(f, "Serialization error: {}", msg),
+ JavaError::InvalidConfig(msg) => write!(f, "Invalid configuration: {}", msg),
+ JavaError::DownloadFailed(msg) => write!(f, "Download failed: {}", msg),
+ JavaError::ExtractionFailed(msg) => write!(f, "Extraction failed: {}", msg),
+ JavaError::ChecksumMismatch(msg) => write!(f, "Checksum mismatch: {}", msg),
+ JavaError::Other(msg) => write!(f, "{}", msg),
+ }
+ }
+}
+
+impl std::error::Error for JavaError {}
+
+/// Convert JavaError to String for Tauri command results
+impl From<JavaError> for String {
+ fn from(err: JavaError) -> Self {
+ err.to_string()
+ }
+}
+
+/// Convert std::io::Error to JavaError
+impl From<std::io::Error> for JavaError {
+ fn from(err: std::io::Error) -> Self {
+ JavaError::IoError(err.to_string())
+ }
+}
+
+/// Convert serde_json::Error to JavaError
+impl From<serde_json::Error> for JavaError {
+ fn from(err: serde_json::Error) -> Self {
+ JavaError::SerializationError(err.to_string())
+ }
+}
+
+/// Convert reqwest::Error to JavaError
+impl From<reqwest::Error> for JavaError {
+ fn from(err: reqwest::Error) -> Self {
+ if err.is_timeout() {
+ JavaError::Timeout(err.to_string())
+ } else if err.is_connect() || err.is_request() {
+ JavaError::NetworkError(err.to_string())
+ } else {
+ JavaError::NetworkError(err.to_string())
+ }
+ }
+}
+
+/// Convert String to JavaError
+impl From<String> for JavaError {
+ fn from(err: String) -> Self {
+ JavaError::Other(err)
+ }
+}
diff --git a/src-tauri/src/core/java/mod.rs b/src-tauri/src/core/java/mod.rs
new file mode 100644
index 0000000..091ad0a
--- /dev/null
+++ b/src-tauri/src/core/java/mod.rs
@@ -0,0 +1,538 @@
+use serde::{Deserialize, Serialize};
+use std::path::PathBuf;
+use tauri::{AppHandle, Emitter, Manager};
+
+pub mod detection;
+pub mod error;
+pub mod persistence;
+pub mod priority;
+pub mod provider;
+pub mod providers;
+pub mod validation;
+
+pub use error::JavaError;
+use ts_rs::TS;
+
+/// Remove the UNC prefix (\\?\) from Windows paths
+pub fn strip_unc_prefix(path: PathBuf) -> PathBuf {
+ #[cfg(target_os = "windows")]
+ {
+ let s = path.to_string_lossy().to_string();
+ if s.starts_with(r"\\?\\") {
+ return PathBuf::from(&s[4..]);
+ }
+ }
+ path
+}
+
+use crate::core::downloader::{DownloadQueue, JavaDownloadProgress, PendingJavaDownload};
+use crate::utils::zip;
+use provider::JavaProvider;
+use providers::AdoptiumProvider;
+
+const CACHE_DURATION_SECS: u64 = 24 * 60 * 60;
+
+#[derive(Debug, Clone, Serialize, Deserialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(export, export_to = "java/core.ts")]
+pub struct JavaInstallation {
+ pub path: String,
+ pub version: String,
+ pub arch: String,
+ pub vendor: String,
+ pub source: String,
+ pub is_64bit: bool,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+pub enum ImageType {
+ Jre,
+ Jdk,
+}
+
+impl Default for ImageType {
+ fn default() -> Self {
+ Self::Jre
+ }
+}
+
+impl std::fmt::Display for ImageType {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::Jre => write!(f, "jre"),
+ Self::Jdk => write!(f, "jdk"),
+ }
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, TS)]
+#[ts(export, export_to = "java/core.ts")]
+#[serde(rename_all = "camelCase")]
+pub struct JavaReleaseInfo {
+ pub major_version: u32,
+ pub image_type: String,
+ pub version: String,
+ pub release_name: String,
+ pub release_date: Option<String>,
+ pub file_size: u64,
+ pub checksum: Option<String>,
+ pub download_url: String,
+ pub is_lts: bool,
+ pub is_available: bool,
+ pub architecture: String,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
+#[ts(export, export_to = "java/core.ts")]
+#[serde(rename_all = "camelCase")]
+pub struct JavaCatalog {
+ pub releases: Vec<JavaReleaseInfo>,
+ pub available_major_versions: Vec<u32>,
+ pub lts_versions: Vec<u32>,
+ pub cached_at: u64,
+}
+
+#[derive(Debug, Clone, Serialize, TS)]
+#[ts(export, export_to = "java/core.ts")]
+pub struct JavaDownloadInfo {
+ pub version: String, // e.g., "17.0.2+8"
+ pub release_name: String, // e.g., "jdk-17.0.2+8"
+ pub download_url: String, // Direct download URL
+ pub file_name: String, // e.g., "OpenJDK17U-jre_x64_linux_hotspot_17.0.2_8.tar.gz"
+ pub file_size: u64, // in bytes
+ pub checksum: Option<String>, // SHA256 checksum
+ pub image_type: String, // "jre" or "jdk"
+}
+
+pub fn get_java_install_dir(app_handle: &AppHandle) -> PathBuf {
+ app_handle.path().app_data_dir().unwrap().join("java")
+}
+
+fn get_catalog_cache_path(app_handle: &AppHandle) -> PathBuf {
+ app_handle
+ .path()
+ .app_data_dir()
+ .unwrap()
+ .join("java_catalog_cache.json")
+}
+
+pub fn load_cached_catalog(app_handle: &AppHandle) -> Option<JavaCatalog> {
+ let cache_path = get_catalog_cache_path(app_handle);
+ if !cache_path.exists() {
+ return None;
+ }
+
+ // Read cache file
+ let content = std::fs::read_to_string(&cache_path).ok()?;
+ let catalog: JavaCatalog = serde_json::from_str(&content).ok()?;
+
+ // Get current time in seconds since UNIX_EPOCH
+ let now = std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap()
+ .as_secs();
+
+ // Check if cache is still valid
+ if now - catalog.cached_at < CACHE_DURATION_SECS {
+ Some(catalog)
+ } else {
+ None
+ }
+}
+
+pub fn save_catalog_cache(app_handle: &AppHandle, catalog: &JavaCatalog) -> Result<(), String> {
+ let cache_path = get_catalog_cache_path(app_handle);
+ let content = serde_json::to_string_pretty(catalog).map_err(|e| e.to_string())?;
+ std::fs::write(&cache_path, content).map_err(|e| e.to_string())?;
+ Ok(())
+}
+
+#[allow(dead_code)]
+pub fn clear_catalog_cache(app_handle: &AppHandle) -> Result<(), String> {
+ let cache_path = get_catalog_cache_path(app_handle);
+ if cache_path.exists() {
+ std::fs::remove_file(&cache_path).map_err(|e| e.to_string())?;
+ }
+ Ok(())
+}
+
+pub async fn fetch_java_catalog(
+ app_handle: &AppHandle,
+ force_refresh: bool,
+) -> Result<JavaCatalog, String> {
+ let provider = AdoptiumProvider::new();
+ provider
+ .fetch_catalog(app_handle, force_refresh)
+ .await
+ .map_err(|e| e.to_string())
+}
+
+pub async fn fetch_java_release(
+ major_version: u32,
+ image_type: ImageType,
+) -> Result<JavaDownloadInfo, String> {
+ let provider = AdoptiumProvider::new();
+ provider
+ .fetch_release(major_version, image_type)
+ .await
+ .map_err(|e| e.to_string())
+}
+
+pub async fn fetch_available_versions() -> Result<Vec<u32>, String> {
+ let provider = AdoptiumProvider::new();
+ provider
+ .available_versions()
+ .await
+ .map_err(|e| e.to_string())
+}
+
+pub async fn download_and_install_java(
+ app_handle: &AppHandle,
+ major_version: u32,
+ image_type: ImageType,
+ custom_path: Option<PathBuf>,
+) -> Result<JavaInstallation, String> {
+ let provider = AdoptiumProvider::new();
+ let info = provider.fetch_release(major_version, image_type).await?;
+ let file_name = info.file_name.clone();
+
+ let install_base = custom_path.unwrap_or_else(|| get_java_install_dir(app_handle));
+ let version_dir = install_base.join(format!(
+ "{}-{}-{}",
+ provider.install_prefix(),
+ major_version,
+ image_type
+ ));
+
+ std::fs::create_dir_all(&install_base)
+ .map_err(|e| format!("Failed to create installation directory: {}", e))?;
+
+ let mut queue = DownloadQueue::load(app_handle);
+ queue.add(PendingJavaDownload {
+ major_version,
+ image_type: image_type.to_string(),
+ download_url: info.download_url.clone(),
+ file_name: info.file_name.clone(),
+ file_size: info.file_size,
+ checksum: info.checksum.clone(),
+ install_path: install_base.to_string_lossy().to_string(),
+ created_at: std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap()
+ .as_secs(),
+ });
+ queue.save(app_handle)?;
+
+ let archive_path = install_base.join(&info.file_name);
+
+ let need_download = if archive_path.exists() {
+ if let Some(expected_checksum) = &info.checksum {
+ let data = std::fs::read(&archive_path)
+ .map_err(|e| format!("Failed to read downloaded file: {}", e))?;
+ !crate::core::downloader::verify_checksum(&data, Some(expected_checksum), None)
+ } else {
+ false
+ }
+ } else {
+ true
+ };
+
+ if need_download {
+ crate::core::downloader::download_with_resume(
+ app_handle,
+ &info.download_url,
+ &archive_path,
+ info.checksum.as_deref(),
+ info.file_size,
+ )
+ .await?;
+ }
+
+ let _ = app_handle.emit(
+ "java-download-progress",
+ JavaDownloadProgress {
+ file_name: file_name.clone(),
+ downloaded_bytes: info.file_size,
+ total_bytes: info.file_size,
+ speed_bytes_per_sec: 0,
+ eta_seconds: 0,
+ status: "Extracting".to_string(),
+ percentage: 100.0,
+ },
+ );
+
+ if version_dir.exists() {
+ std::fs::remove_dir_all(&version_dir)
+ .map_err(|e| format!("Failed to remove old version directory: {}", e))?;
+ }
+
+ std::fs::create_dir_all(&version_dir)
+ .map_err(|e| format!("Failed to create version directory: {}", e))?;
+
+ let top_level_dir = if info.file_name.ends_with(".tar.gz") || info.file_name.ends_with(".tgz") {
+ zip::extract_tar_gz(&archive_path, &version_dir)?
+ } else if info.file_name.ends_with(".zip") {
+ zip::extract_zip(&archive_path, &version_dir)?;
+ find_top_level_dir(&version_dir)?
+ } else {
+ return Err(format!("Unsupported archive format: {}", info.file_name));
+ };
+
+ let _ = std::fs::remove_file(&archive_path);
+
+ let java_home = version_dir.join(&top_level_dir);
+ let java_bin = if cfg!(target_os = "macos") {
+ java_home
+ .join("Contents")
+ .join("Home")
+ .join("bin")
+ .join("java")
+ } else if cfg!(windows) {
+ java_home.join("bin").join("java.exe")
+ } else {
+ java_home.join("bin").join("java")
+ };
+
+ if !java_bin.exists() {
+ return Err(format!(
+ "Installation completed but Java executable not found: {}",
+ java_bin.display()
+ ));
+ }
+
+ let java_bin = std::fs::canonicalize(&java_bin).map_err(|e| e.to_string())?;
+ let java_bin = strip_unc_prefix(java_bin);
+
+ let installation = validation::check_java_installation(&java_bin)
+ .await
+ .ok_or_else(|| "Failed to verify Java installation".to_string())?;
+
+ queue.remove(major_version, &image_type.to_string());
+ queue.save(app_handle)?;
+
+ let _ = app_handle.emit(
+ "java-download-progress",
+ JavaDownloadProgress {
+ file_name,
+ downloaded_bytes: info.file_size,
+ total_bytes: info.file_size,
+ speed_bytes_per_sec: 0,
+ eta_seconds: 0,
+ status: "Completed".to_string(),
+ percentage: 100.0,
+ },
+ );
+
+ Ok(installation)
+}
+
+fn find_top_level_dir(extract_dir: &PathBuf) -> Result<String, String> {
+ let entries: Vec<_> = std::fs::read_dir(extract_dir)
+ .map_err(|e| format!("Failed to read directory: {}", e))?
+ .filter_map(|e| e.ok())
+ .filter(|e| e.path().is_dir())
+ .collect();
+
+ if entries.len() == 1 {
+ Ok(entries[0].file_name().to_string_lossy().to_string())
+ } else {
+ Ok(String::new())
+ }
+}
+
+pub async fn detect_java_installations() -> Vec<JavaInstallation> {
+ let mut installations = Vec::new();
+ let candidates = detection::get_java_candidates();
+
+ for candidate in candidates {
+ if let Some(java) = validation::check_java_installation(&candidate).await {
+ if !installations
+ .iter()
+ .any(|j: &JavaInstallation| j.path == java.path)
+ {
+ installations.push(java);
+ }
+ }
+ }
+
+ installations.sort_by(|a, b| {
+ let v_a = validation::parse_java_version(&a.version);
+ let v_b = validation::parse_java_version(&b.version);
+ v_b.cmp(&v_a)
+ });
+
+ installations
+}
+
+pub async fn get_recommended_java(required_major_version: Option<u64>) -> Option<JavaInstallation> {
+ let installations = detect_java_installations().await;
+
+ if let Some(required) = required_major_version {
+ installations.into_iter().find(|java| {
+ let major = validation::parse_java_version(&java.version);
+ major >= required as u32
+ })
+ } else {
+ installations.into_iter().next()
+ }
+}
+
+pub async fn get_compatible_java(
+ app_handle: &AppHandle,
+ required_major_version: Option<u64>,
+ max_major_version: Option<u32>,
+) -> Option<JavaInstallation> {
+ let installations = detect_all_java_installations(app_handle).await;
+
+ installations.into_iter().find(|java| {
+ let major = validation::parse_java_version(&java.version);
+ validation::is_version_compatible(major, required_major_version, max_major_version)
+ })
+}
+
+pub async fn is_java_compatible(
+ java_path: &str,
+ required_major_version: Option<u64>,
+ max_major_version: Option<u32>,
+) -> bool {
+ let java_path_buf = PathBuf::from(java_path);
+ if let Some(java) = validation::check_java_installation(&java_path_buf).await {
+ let major = validation::parse_java_version(&java.version);
+ validation::is_version_compatible(major, required_major_version, max_major_version)
+ } else {
+ false
+ }
+}
+
+pub async fn detect_all_java_installations(app_handle: &AppHandle) -> Vec<JavaInstallation> {
+ let mut installations = detect_java_installations().await;
+
+ let dropout_java_dir = get_java_install_dir(app_handle);
+ if dropout_java_dir.exists() {
+ if let Ok(entries) = std::fs::read_dir(&dropout_java_dir) {
+ for entry in entries.flatten() {
+ let path = entry.path();
+ if path.is_dir() {
+ let java_bin = find_java_executable(&path);
+ if let Some(java_path) = java_bin {
+ if let Some(java) = validation::check_java_installation(&java_path).await {
+ if !installations.iter().any(|j| j.path == java.path) {
+ installations.push(java);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ installations.sort_by(|a, b| {
+ let v_a = validation::parse_java_version(&a.version);
+ let v_b = validation::parse_java_version(&b.version);
+ v_b.cmp(&v_a)
+ });
+
+ installations
+}
+
+fn find_java_executable(dir: &PathBuf) -> Option<PathBuf> {
+ let bin_name = if cfg!(windows) { "java.exe" } else { "java" };
+
+ let direct_bin = dir.join("bin").join(bin_name);
+ if direct_bin.exists() {
+ let resolved = std::fs::canonicalize(&direct_bin).unwrap_or(direct_bin);
+ return Some(strip_unc_prefix(resolved));
+ }
+
+ #[cfg(target_os = "macos")]
+ {
+ let macos_bin = dir.join("Contents").join("Home").join("bin").join(bin_name);
+ if macos_bin.exists() {
+ return Some(macos_bin);
+ }
+ }
+
+ if let Ok(entries) = std::fs::read_dir(dir) {
+ for entry in entries.flatten() {
+ let path = entry.path();
+ if path.is_dir() {
+ let nested_bin = path.join("bin").join(bin_name);
+ if nested_bin.exists() {
+ let resolved = std::fs::canonicalize(&nested_bin).unwrap_or(nested_bin);
+ return Some(strip_unc_prefix(resolved));
+ }
+
+ #[cfg(target_os = "macos")]
+ {
+ let macos_nested = path
+ .join("Contents")
+ .join("Home")
+ .join("bin")
+ .join(bin_name);
+ if macos_nested.exists() {
+ return Some(macos_nested);
+ }
+ }
+ }
+ }
+ }
+
+ None
+}
+
+pub async fn resume_pending_downloads(
+ app_handle: &AppHandle,
+) -> Result<Vec<JavaInstallation>, String> {
+ let queue = DownloadQueue::load(app_handle);
+ let mut installed = Vec::new();
+
+ for pending in queue.pending_downloads.iter() {
+ let image_type = if pending.image_type == "jdk" {
+ ImageType::Jdk
+ } else {
+ ImageType::Jre
+ };
+
+ match download_and_install_java(
+ app_handle,
+ pending.major_version,
+ image_type,
+ Some(PathBuf::from(&pending.install_path)),
+ )
+ .await
+ {
+ Ok(installation) => {
+ installed.push(installation);
+ }
+ Err(e) => {
+ eprintln!(
+ "Failed to resume Java {} {} download: {}",
+ pending.major_version, pending.image_type, e
+ );
+ }
+ }
+ }
+
+ Ok(installed)
+}
+
+pub fn cancel_current_download() {
+ crate::core::downloader::cancel_java_download();
+}
+
+pub fn get_pending_downloads(app_handle: &AppHandle) -> Vec<PendingJavaDownload> {
+ let queue = DownloadQueue::load(app_handle);
+ queue.pending_downloads
+}
+
+#[allow(dead_code)]
+pub fn clear_pending_download(
+ app_handle: &AppHandle,
+ major_version: u32,
+ image_type: &str,
+) -> Result<(), String> {
+ let mut queue = DownloadQueue::load(app_handle);
+ queue.remove(major_version, image_type);
+ queue.save(app_handle)
+}
diff --git a/src-tauri/src/core/java/persistence.rs b/src-tauri/src/core/java/persistence.rs
new file mode 100644
index 0000000..a6727d7
--- /dev/null
+++ b/src-tauri/src/core/java/persistence.rs
@@ -0,0 +1,115 @@
+use crate::core::java::error::JavaError;
+use serde::{Deserialize, Serialize};
+use std::path::PathBuf;
+use tauri::{AppHandle, Manager};
+use ts_rs::TS;
+
+#[derive(Debug, Clone, Serialize, Deserialize, TS)]
+#[ts(export, export_to = "java/persistence.ts")]
+pub struct JavaConfig {
+ pub user_defined_paths: Vec<String>,
+ pub preferred_java_path: Option<String>,
+ pub last_detection_time: u64,
+}
+
+impl Default for JavaConfig {
+ fn default() -> Self {
+ Self {
+ user_defined_paths: Vec::new(),
+ preferred_java_path: None,
+ last_detection_time: 0,
+ }
+ }
+}
+
+fn get_java_config_path(app_handle: &AppHandle) -> PathBuf {
+ app_handle
+ .path()
+ .app_data_dir()
+ .unwrap()
+ .join("java_config.json")
+}
+
+pub fn load_java_config(app_handle: &AppHandle) -> JavaConfig {
+ let config_path = get_java_config_path(app_handle);
+ if !config_path.exists() {
+ return JavaConfig::default();
+ }
+
+ match std::fs::read_to_string(&config_path) {
+ Ok(content) => match serde_json::from_str(&content) {
+ Ok(config) => config,
+ Err(err) => {
+ // Log the error but don't panic - return default config
+ log::warn!(
+ "Failed to parse Java config at {}: {}. Using default configuration.",
+ config_path.display(),
+ err
+ );
+ JavaConfig::default()
+ }
+ },
+ Err(err) => {
+ log::warn!(
+ "Failed to read Java config at {}: {}. Using default configuration.",
+ config_path.display(),
+ err
+ );
+ JavaConfig::default()
+ }
+ }
+}
+
+pub fn save_java_config(app_handle: &AppHandle, config: &JavaConfig) -> Result<(), JavaError> {
+ let config_path = get_java_config_path(app_handle);
+ let content = serde_json::to_string_pretty(config)?;
+
+ std::fs::create_dir_all(config_path.parent().ok_or_else(|| {
+ JavaError::InvalidConfig("Java config path has no parent directory".to_string())
+ })?)?;
+
+ std::fs::write(&config_path, content)?;
+ Ok(())
+}
+
+#[allow(dead_code)]
+pub fn add_user_defined_path(app_handle: &AppHandle, path: String) -> Result<(), JavaError> {
+ let mut config = load_java_config(app_handle);
+ if !config.user_defined_paths.contains(&path) {
+ config.user_defined_paths.push(path);
+ }
+ save_java_config(app_handle, &config)
+}
+
+#[allow(dead_code)]
+pub fn remove_user_defined_path(app_handle: &AppHandle, path: &str) -> Result<(), JavaError> {
+ let mut config = load_java_config(app_handle);
+ config.user_defined_paths.retain(|p| p != path);
+ save_java_config(app_handle, &config)
+}
+
+#[allow(dead_code)]
+pub fn set_preferred_java_path(
+ app_handle: &AppHandle,
+ path: Option<String>,
+) -> Result<(), JavaError> {
+ let mut config = load_java_config(app_handle);
+ config.preferred_java_path = path;
+ save_java_config(app_handle, &config)
+}
+
+#[allow(dead_code)]
+pub fn get_preferred_java_path(app_handle: &AppHandle) -> Option<String> {
+ let config = load_java_config(app_handle);
+ config.preferred_java_path
+}
+
+#[allow(dead_code)]
+pub fn update_last_detection_time(app_handle: &AppHandle) -> Result<(), JavaError> {
+ let mut config = load_java_config(app_handle);
+ config.last_detection_time = std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .map_err(|e| JavaError::Other(format!("System time error: {}", e)))?
+ .as_secs();
+ save_java_config(app_handle, &config)
+}
diff --git a/src-tauri/src/core/java/priority.rs b/src-tauri/src/core/java/priority.rs
new file mode 100644
index 0000000..f991eb7
--- /dev/null
+++ b/src-tauri/src/core/java/priority.rs
@@ -0,0 +1,59 @@
+use tauri::AppHandle;
+
+use crate::core::java::JavaInstallation;
+use crate::core::java::persistence;
+use crate::core::java::validation;
+
+pub async fn resolve_java_for_launch(
+ app_handle: &AppHandle,
+ instance_java_override: Option<&str>,
+ global_java_path: Option<&str>,
+ required_major_version: Option<u64>,
+ max_major_version: Option<u32>,
+) -> Option<JavaInstallation> {
+ if let Some(override_path) = instance_java_override {
+ if !override_path.is_empty() {
+ let path_buf = std::path::PathBuf::from(override_path);
+ if let Some(java) = validation::check_java_installation(&path_buf).await {
+ if is_version_compatible(&java, required_major_version, max_major_version) {
+ return Some(java);
+ }
+ }
+ }
+ }
+
+ if let Some(global_path) = global_java_path {
+ if !global_path.is_empty() {
+ let path_buf = std::path::PathBuf::from(global_path);
+ if let Some(java) = validation::check_java_installation(&path_buf).await {
+ if is_version_compatible(&java, required_major_version, max_major_version) {
+ return Some(java);
+ }
+ }
+ }
+ }
+
+ let preferred = persistence::get_preferred_java_path(app_handle);
+ if let Some(pref_path) = preferred {
+ let path_buf = std::path::PathBuf::from(&pref_path);
+ if let Some(java) = validation::check_java_installation(&path_buf).await {
+ if is_version_compatible(&java, required_major_version, max_major_version) {
+ return Some(java);
+ }
+ }
+ }
+
+ let installations = super::detect_all_java_installations(app_handle).await;
+ installations
+ .into_iter()
+ .find(|java| is_version_compatible(java, required_major_version, max_major_version))
+}
+
+fn is_version_compatible(
+ java: &JavaInstallation,
+ required_major_version: Option<u64>,
+ max_major_version: Option<u32>,
+) -> bool {
+ let major = validation::parse_java_version(&java.version);
+ validation::is_version_compatible(major, required_major_version, max_major_version)
+}
diff --git a/src-tauri/src/core/java/provider.rs b/src-tauri/src/core/java/provider.rs
new file mode 100644
index 0000000..8aa0a0d
--- /dev/null
+++ b/src-tauri/src/core/java/provider.rs
@@ -0,0 +1,58 @@
+use crate::core::java::{ImageType, JavaCatalog, JavaDownloadInfo, JavaError};
+use tauri::AppHandle;
+
+/// Trait for Java distribution providers (e.g., Adoptium, Corretto)
+///
+/// Implementations handle fetching Java catalogs and release information
+/// from different distribution providers.
+pub trait JavaProvider: Send + Sync {
+ /// Fetch the Java catalog (all available versions for this provider)
+ ///
+ /// # Arguments
+ /// * `app_handle` - The Tauri app handle for cache access
+ /// * `force_refresh` - If true, bypass cache and fetch fresh data
+ ///
+ /// # Returns
+ /// * `Ok(JavaCatalog)` with available versions
+ /// * `Err(JavaError)` if fetch or parsing fails
+ async fn fetch_catalog(
+ &self,
+ app_handle: &AppHandle,
+ force_refresh: bool,
+ ) -> Result<JavaCatalog, JavaError>;
+
+ /// Fetch a specific Java release
+ ///
+ /// # Arguments
+ /// * `major_version` - The major version number (e.g., 17, 21)
+ /// * `image_type` - Whether to fetch JRE or JDK
+ ///
+ /// # Returns
+ /// * `Ok(JavaDownloadInfo)` with download details
+ /// * `Err(JavaError)` if fetch or parsing fails
+ async fn fetch_release(
+ &self,
+ major_version: u32,
+ image_type: ImageType,
+ ) -> Result<JavaDownloadInfo, JavaError>;
+
+ /// Get list of available major versions
+ ///
+ /// # Returns
+ /// * `Ok(Vec<u32>)` with available major versions
+ /// * `Err(JavaError)` if fetch fails
+ async fn available_versions(&self) -> Result<Vec<u32>, JavaError>;
+
+ /// Get provider name (e.g., "adoptium", "corretto")
+ #[allow(dead_code)]
+ fn provider_name(&self) -> &'static str;
+
+ /// Get OS name for this provider's API
+ fn os_name(&self) -> &'static str;
+
+ /// Get architecture name for this provider's API
+ fn arch_name(&self) -> &'static str;
+
+ /// Get installation directory prefix (e.g., "temurin", "corretto")
+ fn install_prefix(&self) -> &'static str;
+}
diff --git a/src-tauri/src/core/java/providers/adoptium.rs b/src-tauri/src/core/java/providers/adoptium.rs
new file mode 100644
index 0000000..1765a99
--- /dev/null
+++ b/src-tauri/src/core/java/providers/adoptium.rs
@@ -0,0 +1,340 @@
+use crate::core::java::error::JavaError;
+use crate::core::java::provider::JavaProvider;
+use crate::core::java::save_catalog_cache;
+use crate::core::java::{ImageType, JavaCatalog, JavaDownloadInfo, JavaReleaseInfo};
+use serde::Deserialize;
+use tauri::AppHandle;
+use ts_rs::TS;
+
+const ADOPTIUM_API_BASE: &str = "https://api.adoptium.net/v3";
+
+#[derive(Debug, Clone, Deserialize, TS)]
+#[ts(export, export_to = "java/providers/adoptium.ts")]
+pub struct AdoptiumAsset {
+ pub binary: AdoptiumBinary,
+ pub release_name: String,
+ pub version: AdoptiumVersionData,
+}
+
+#[derive(Debug, Clone, Deserialize, TS)]
+#[allow(dead_code)]
+#[ts(export, export_to = "java/providers/adoptium.ts")]
+pub struct AdoptiumBinary {
+ pub os: String,
+ pub architecture: String,
+ pub image_type: String,
+ pub package: AdoptiumPackage,
+ #[serde(default)]
+ pub updated_at: Option<String>,
+}
+
+#[derive(Debug, Clone, Deserialize, TS)]
+#[ts(export, export_to = "java/providers/adoptium.ts")]
+pub struct AdoptiumPackage {
+ pub name: String,
+ pub link: String,
+ pub size: u64,
+ pub checksum: Option<String>,
+}
+
+#[derive(Debug, Clone, Deserialize, TS)]
+#[allow(dead_code)]
+#[ts(export, export_to = "java/providers/adoptium.ts")]
+pub struct AdoptiumVersionData {
+ pub major: u32,
+ pub minor: u32,
+ pub security: u32,
+ pub semver: String,
+ pub openjdk_version: String,
+}
+
+#[derive(Debug, Clone, Deserialize, TS)]
+#[allow(dead_code)]
+#[ts(export, export_to = "java/providers/adoptium.ts")]
+pub struct AvailableReleases {
+ pub available_releases: Vec<u32>,
+ pub available_lts_releases: Vec<u32>,
+ pub most_recent_lts: Option<u32>,
+ pub most_recent_feature_release: Option<u32>,
+}
+
+pub struct AdoptiumProvider;
+
+impl AdoptiumProvider {
+ pub fn new() -> Self {
+ Self
+ }
+}
+
+impl Default for AdoptiumProvider {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl JavaProvider for AdoptiumProvider {
+ async fn fetch_catalog(
+ &self,
+ app_handle: &AppHandle,
+ force_refresh: bool,
+ ) -> Result<JavaCatalog, JavaError> {
+ if !force_refresh {
+ if let Some(cached) = crate::core::java::load_cached_catalog(app_handle) {
+ return Ok(cached);
+ }
+ }
+
+ let os = self.os_name();
+ let arch = self.arch_name();
+ let client = reqwest::Client::new();
+
+ let releases_url = format!("{}/info/available_releases", ADOPTIUM_API_BASE);
+ let available: AvailableReleases = client
+ .get(&releases_url)
+ .header("Accept", "application/json")
+ .send()
+ .await
+ .map_err(|e| {
+ JavaError::NetworkError(format!("Failed to fetch available releases: {}", e))
+ })?
+ .json::<AvailableReleases>()
+ .await
+ .map_err(|e| {
+ JavaError::SerializationError(format!("Failed to parse available releases: {}", e))
+ })?;
+
+ // Parallelize HTTP requests for better performance
+ let mut fetch_tasks = Vec::new();
+
+ for major_version in &available.available_releases {
+ for image_type in &["jre", "jdk"] {
+ let major_version = *major_version;
+ let image_type = image_type.to_string();
+ let url = format!(
+ "{}/assets/latest/{}/hotspot?os={}&architecture={}&image_type={}",
+ ADOPTIUM_API_BASE, major_version, os, arch, image_type
+ );
+ let client = client.clone();
+ let is_lts = available.available_lts_releases.contains(&major_version);
+ let arch = arch.to_string();
+
+ let task = tokio::spawn(async move {
+ match client
+ .get(&url)
+ .header("Accept", "application/json")
+ .send()
+ .await
+ {
+ Ok(response) => {
+ if response.status().is_success() {
+ if let Ok(assets) = response.json::<Vec<AdoptiumAsset>>().await {
+ if let Some(asset) = assets.into_iter().next() {
+ let release_date = asset.binary.updated_at.clone();
+ return Some(JavaReleaseInfo {
+ major_version,
+ image_type,
+ version: asset.version.semver.clone(),
+ release_name: asset.release_name.clone(),
+ release_date,
+ file_size: asset.binary.package.size,
+ checksum: asset.binary.package.checksum,
+ download_url: asset.binary.package.link,
+ is_lts,
+ is_available: true,
+ architecture: asset.binary.architecture.clone(),
+ });
+ }
+ }
+ }
+ // Fallback for unsuccessful response
+ Some(JavaReleaseInfo {
+ major_version,
+ image_type,
+ version: format!("{}.x", major_version),
+ release_name: format!("jdk-{}", major_version),
+ release_date: None,
+ file_size: 0,
+ checksum: None,
+ download_url: String::new(),
+ is_lts,
+ is_available: false,
+ architecture: arch,
+ })
+ }
+ Err(_) => Some(JavaReleaseInfo {
+ major_version,
+ image_type,
+ version: format!("{}.x", major_version),
+ release_name: format!("jdk-{}", major_version),
+ release_date: None,
+ file_size: 0,
+ checksum: None,
+ download_url: String::new(),
+ is_lts,
+ is_available: false,
+ architecture: arch,
+ }),
+ }
+ });
+ fetch_tasks.push(task);
+ }
+ }
+
+ // Collect all results concurrently
+ let mut releases = Vec::new();
+ for task in fetch_tasks {
+ match task.await {
+ Ok(Some(release)) => {
+ releases.push(release);
+ }
+ Ok(None) => {
+ // Task completed but returned None, should not happen in current implementation
+ }
+ Err(e) => {
+ return Err(JavaError::NetworkError(format!(
+ "Failed to join Adoptium catalog fetch task: {}",
+ e
+ )));
+ }
+ }
+ }
+
+ let now = std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap()
+ .as_secs();
+
+ let catalog = JavaCatalog {
+ releases,
+ available_major_versions: available.available_releases,
+ lts_versions: available.available_lts_releases,
+ cached_at: now,
+ };
+
+ let _ = save_catalog_cache(app_handle, &catalog);
+
+ Ok(catalog)
+ }
+
+ async fn fetch_release(
+ &self,
+ major_version: u32,
+ image_type: ImageType,
+ ) -> Result<JavaDownloadInfo, JavaError> {
+ let os = self.os_name();
+ let arch = self.arch_name();
+
+ let url = format!(
+ "{}/assets/latest/{}/hotspot?os={}&architecture={}&image_type={}",
+ ADOPTIUM_API_BASE, major_version, os, arch, image_type
+ );
+
+ let client = reqwest::Client::new();
+ let response = client
+ .get(&url)
+ .header("Accept", "application/json")
+ .send()
+ .await
+ .map_err(|e| JavaError::NetworkError(format!("Network request failed: {}", e)))?;
+
+ if !response.status().is_success() {
+ return Err(JavaError::NetworkError(format!(
+ "Adoptium API returned error: {} - The version/platform might be unavailable",
+ response.status()
+ )));
+ }
+
+ let assets: Vec<AdoptiumAsset> =
+ response.json::<Vec<AdoptiumAsset>>().await.map_err(|e| {
+ JavaError::SerializationError(format!("Failed to parse API response: {}", e))
+ })?;
+
+ let asset = assets
+ .into_iter()
+ .next()
+ .ok_or_else(|| JavaError::NotFound)?;
+
+ Ok(JavaDownloadInfo {
+ version: asset.version.semver.clone(),
+ release_name: asset.release_name,
+ download_url: asset.binary.package.link,
+ file_name: asset.binary.package.name,
+ file_size: asset.binary.package.size,
+ checksum: asset.binary.package.checksum,
+ image_type: asset.binary.image_type,
+ })
+ }
+
+ async fn available_versions(&self) -> Result<Vec<u32>, JavaError> {
+ let url = format!("{}/info/available_releases", ADOPTIUM_API_BASE);
+
+ let response = reqwest::get(url)
+ .await
+ .map_err(|e| JavaError::NetworkError(format!("Network request failed: {}", e)))?;
+
+ let releases: AvailableReleases =
+ response.json::<AvailableReleases>().await.map_err(|e| {
+ JavaError::SerializationError(format!("Failed to parse response: {}", e))
+ })?;
+
+ Ok(releases.available_releases)
+ }
+
+ fn provider_name(&self) -> &'static str {
+ "adoptium"
+ }
+
+ fn os_name(&self) -> &'static str {
+ #[cfg(target_os = "linux")]
+ {
+ if std::path::Path::new("/etc/alpine-release").exists() {
+ return "alpine-linux";
+ }
+ "linux"
+ }
+ #[cfg(target_os = "macos")]
+ {
+ "mac"
+ }
+ #[cfg(target_os = "windows")]
+ {
+ "windows"
+ }
+ #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
+ {
+ "linux"
+ }
+ }
+
+ fn arch_name(&self) -> &'static str {
+ #[cfg(target_arch = "x86_64")]
+ {
+ "x64"
+ }
+ #[cfg(target_arch = "aarch64")]
+ {
+ "aarch64"
+ }
+ #[cfg(target_arch = "x86")]
+ {
+ "x86"
+ }
+ #[cfg(target_arch = "arm")]
+ {
+ "arm"
+ }
+ #[cfg(not(any(
+ target_arch = "x86_64",
+ target_arch = "aarch64",
+ target_arch = "x86",
+ target_arch = "arm"
+ )))]
+ {
+ "x64"
+ }
+ }
+
+ fn install_prefix(&self) -> &'static str {
+ "temurin"
+ }
+}
diff --git a/src-tauri/src/core/java/providers/mod.rs b/src-tauri/src/core/java/providers/mod.rs
new file mode 100644
index 0000000..16eb5c7
--- /dev/null
+++ b/src-tauri/src/core/java/providers/mod.rs
@@ -0,0 +1,3 @@
+pub mod adoptium;
+
+pub use adoptium::AdoptiumProvider;
diff --git a/src-tauri/src/core/java/validation.rs b/src-tauri/src/core/java/validation.rs
new file mode 100644
index 0000000..b56ad59
--- /dev/null
+++ b/src-tauri/src/core/java/validation.rs
@@ -0,0 +1,146 @@
+use std::collections::HashMap;
+use std::path::PathBuf;
+use std::process::Command;
+
+#[cfg(target_os = "windows")]
+use std::os::windows::process::CommandExt;
+
+use crate::core::java::JavaInstallation;
+
+pub async fn check_java_installation(path: &PathBuf) -> Option<JavaInstallation> {
+ let path = path.clone();
+ tokio::task::spawn_blocking(move || check_java_installation_blocking(&path))
+ .await
+ .ok()?
+}
+
+fn check_java_installation_blocking(path: &PathBuf) -> Option<JavaInstallation> {
+ let mut cmd = Command::new(path);
+ cmd.arg("-version");
+
+ // Hide console window
+ #[cfg(target_os = "windows")]
+ cmd.creation_flags(0x08000000);
+
+ let output = cmd.output().ok()?;
+
+ let version_output = String::from_utf8_lossy(&output.stderr);
+
+ let version = parse_version_string(&version_output)?;
+ let arch = extract_architecture(&version_output);
+ let vendor = extract_vendor(&version_output);
+ let is_64bit = version_output.to_lowercase().contains("64-bit") || arch == "aarch64";
+
+ Some(JavaInstallation {
+ path: path.to_string_lossy().to_string(),
+ version,
+ arch,
+ vendor,
+ source: "system".to_string(),
+ is_64bit,
+ })
+}
+
+pub fn parse_version_string(output: &str) -> Option<String> {
+ for line in output.lines() {
+ if line.contains("version") {
+ if let Some(start) = line.find('"') {
+ if let Some(end) = line[start + 1..].find('"') {
+ return Some(line[start + 1..start + 1 + end].to_string());
+ }
+ }
+ }
+ }
+ None
+}
+
+pub fn parse_java_version(version: &str) -> u32 {
+ let parts: Vec<&str> = version.split('.').collect();
+ if let Some(first) = parts.first() {
+ // Handle both legacy (1.x) and modern (x) versioning
+ if *first == "1" {
+ // Legacy versioning
+ parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0)
+ } else {
+ // Modern versioning
+ first.parse().unwrap_or(0)
+ }
+ } else {
+ 0
+ }
+}
+
+pub fn extract_architecture(version_output: &str) -> String {
+ if version_output.contains("64-Bit") {
+ "x64".to_string()
+ } else if version_output.contains("32-Bit") {
+ "x86".to_string()
+ } else if version_output.contains("aarch64") || version_output.contains("ARM64") {
+ "aarch64".to_string()
+ } else {
+ "x64".to_string()
+ }
+}
+
+pub fn extract_vendor(version_output: &str) -> String {
+ let lower = version_output.to_lowercase();
+
+ let vendor_name: HashMap<&str, &str> = [
+ // Eclipse/Adoptium
+ ("temurin", "Temurin (Eclipse)"),
+ ("adoptium", "Eclipse Adoptium"),
+ // Amazon
+ ("corretto", "Corretto (Amazon)"),
+ ("amzn", "Corretto (Amazon)"),
+ // Alibaba
+ ("dragonwell", "Dragonwell (Alibaba)"),
+ ("albba", "Dragonwell (Alibaba)"),
+ // GraalVM
+ ("graalvm", "GraalVM"),
+ // Oracle
+ ("oracle", "Java SE Development Kit (Oracle)"),
+ // Tencent
+ ("kona", "Kona (Tencent)"),
+ // BellSoft
+ ("liberica", "Liberica (Bellsoft)"),
+ ("mandrel", "Mandrel (Red Hat)"),
+ // Microsoft
+ ("microsoft", "OpenJDK (Microsoft)"),
+ // SAP
+ ("sapmachine", "SapMachine (SAP)"),
+ // IBM
+ ("semeru", "Semeru (IBM)"),
+ ("sem", "Semeru (IBM)"),
+ // Azul
+ ("zulu", "Zulu (Azul Systems)"),
+ // Trava
+ ("trava", "Trava (Trava)"),
+ // Huawei
+ ("bisheng", "BiSheng (Huawei)"),
+ // Generic OpenJDK
+ ("openjdk", "OpenJDK"),
+ ]
+ .iter()
+ .cloned()
+ .collect();
+
+ for (key, name) in vendor_name {
+ if lower.contains(key) {
+ return name.to_string();
+ }
+ }
+
+ "Unknown".to_string()
+}
+
+pub fn is_version_compatible(
+ major: u32,
+ required_major_version: Option<u64>,
+ max_major_version: Option<u32>,
+) -> bool {
+ let meets_min = required_major_version
+ .map(|r| major >= r as u32)
+ .unwrap_or(true);
+ let meets_max = max_major_version.map(|m| major <= m).unwrap_or(true);
+ meets_min && meets_max
+}