Skip to main content

solverforge_service/
jar.rs

1use crate::error::{ServiceError, ServiceResult};
2use crate::util::{find_java, find_maven, find_submodule_dir, get_cache_dir};
3use log::{debug, info, warn};
4use std::fs::{self, File};
5use std::io::Write;
6use std::path::{Path, PathBuf};
7use std::process::Command;
8
9// Maven Central coordinates
10const MAVEN_GROUP_ID: &str = "org.solverforge";
11const MAVEN_ARTIFACT_ID: &str = "solverforge-wasm-service";
12const MAVEN_VERSION: &str = env!("CARGO_PKG_VERSION");
13const MAVEN_CENTRAL_URL: &str = "https://repo1.maven.org/maven2";
14
15pub struct JarManager {
16    /// Optional path to the solverforge-wasm-service submodule (for local builds).
17    submodule_dir: Option<PathBuf>,
18    cache_dir: PathBuf,
19    java_home: Option<PathBuf>,
20}
21
22impl Default for JarManager {
23    fn default() -> Self {
24        Self::new()
25    }
26}
27
28impl JarManager {
29    pub fn new() -> Self {
30        // Try to find submodule, but don't fail - we can download from Maven Central
31        let submodule_dir = find_submodule_dir().ok();
32        let cache_dir = get_cache_dir();
33        Self {
34            submodule_dir,
35            cache_dir,
36            java_home: None,
37        }
38    }
39
40    pub fn with_paths(submodule_dir: PathBuf, cache_dir: PathBuf) -> Self {
41        Self {
42            submodule_dir: Some(submodule_dir),
43            cache_dir,
44            java_home: None,
45        }
46    }
47
48    pub fn with_java_home(mut self, java_home: Option<&Path>) -> Self {
49        self.java_home = java_home.map(|p| p.to_path_buf());
50        self
51    }
52
53    pub fn ensure_jar(&self) -> ServiceResult<PathBuf> {
54        let jar_path = self.jar_path();
55
56        // 1. Check cache first
57        if jar_path.exists() {
58            debug!("Using cached JAR: {}", jar_path.display());
59            return Ok(jar_path);
60        }
61
62        // 2. Try local build if submodule exists (dev mode)
63        if let Some(ref submodule_dir) = self.submodule_dir {
64            if submodule_dir.join("pom.xml").exists() {
65                info!("Building solverforge-wasm-service JAR from submodule...");
66                match self.build_jar() {
67                    Ok(()) => {
68                        if jar_path.exists() {
69                            return Ok(jar_path);
70                        }
71                    }
72                    Err(e) => {
73                        warn!("Local build failed: {}, trying Maven download...", e);
74                    }
75                }
76            }
77        }
78
79        // 3. Download from Maven Central (production mode)
80        info!("Downloading solverforge-wasm-service from Maven Central...");
81        self.download_from_maven()?;
82
83        if !jar_path.exists() {
84            return Err(ServiceError::BuildFailed(
85                "JAR not found after download".to_string(),
86            ));
87        }
88
89        // Clean up old versions
90        if let Ok(removed) = self.cleanup_old_versions() {
91            if removed > 0 {
92                info!("Cleaned up {} old JAR version(s) from cache", removed);
93            }
94        }
95
96        Ok(jar_path)
97    }
98
99    pub fn jar_exists(&self) -> bool {
100        self.jar_path().exists()
101    }
102
103    pub fn jar_path(&self) -> PathBuf {
104        // Uber-jar is a single self-contained JAR
105        self.cache_dir.join(format!(
106            "{}-{}-runner.jar",
107            MAVEN_ARTIFACT_ID, MAVEN_VERSION
108        ))
109    }
110
111    pub fn rebuild(&self) -> ServiceResult<PathBuf> {
112        let jar_path = self.jar_path();
113        if jar_path.exists() {
114            fs::remove_file(&jar_path)?;
115        }
116        self.ensure_jar()
117    }
118
119    fn build_jar(&self) -> ServiceResult<()> {
120        let submodule_dir = self.submodule_dir.as_ref().ok_or_else(|| {
121            ServiceError::SubmoduleNotFound(
122                "Cannot build JAR: submodule directory not configured".to_string(),
123            )
124        })?;
125
126        let mvn = find_maven()?;
127
128        fs::create_dir_all(&self.cache_dir)?;
129
130        // Determine JAVA_HOME for Maven - it must use the same Java version
131        // that solverforge-wasm-service was compiled with (Java 24)
132        let java_home = if let Some(ref home) = self.java_home {
133            home.clone()
134        } else {
135            // Find java and derive JAVA_HOME from it
136            let java = find_java(None)?;
137            // java is typically at $JAVA_HOME/bin/java, so go up two levels
138            java.parent()
139                .and_then(|bin| bin.parent())
140                .map(|home| home.to_path_buf())
141                .ok_or_else(|| {
142                    ServiceError::JavaNotFound("Cannot determine JAVA_HOME from java path".into())
143                })?
144        };
145
146        info!(
147            "Running mvn package in {} with JAVA_HOME={}",
148            submodule_dir.display(),
149            java_home.display()
150        );
151
152        let output = Command::new(&mvn)
153            .current_dir(submodule_dir)
154            .env("JAVA_HOME", &java_home)
155            .args(["package", "-DskipTests", "-q"])
156            .output()?;
157
158        if !output.status.success() {
159            let stderr = String::from_utf8_lossy(&output.stderr);
160            return Err(ServiceError::BuildFailed(format!(
161                "Maven build failed: {}",
162                stderr
163            )));
164        }
165
166        // Uber-jar is named <artifactId>-<version>-runner.jar
167        let built_jar = submodule_dir.join("target").join(format!(
168            "{}-{}-runner.jar",
169            MAVEN_ARTIFACT_ID, MAVEN_VERSION
170        ));
171
172        if !built_jar.exists() {
173            return Err(ServiceError::BuildFailed(format!(
174                "Expected JAR not found at {}",
175                built_jar.display()
176            )));
177        }
178
179        fs::create_dir_all(&self.cache_dir)?;
180        let cached_jar = self.jar_path();
181
182        info!("Copying JAR to cache: {}", cached_jar.display());
183        fs::copy(&built_jar, &cached_jar)?;
184
185        // Clean up old versions
186        if let Ok(removed) = self.cleanup_old_versions() {
187            if removed > 0 {
188                info!("Cleaned up {} old JAR version(s) from cache", removed);
189            }
190        }
191
192        Ok(())
193    }
194
195    fn download_from_maven(&self) -> ServiceResult<()> {
196        // Maven Central URL pattern: /group/artifact/version/artifact-version-classifier.jar
197        let group_path = MAVEN_GROUP_ID.replace('.', "/");
198        let jar_url = format!(
199            "{}/{}/{}/{}/{}-{}-runner.jar",
200            MAVEN_CENTRAL_URL,
201            group_path,
202            MAVEN_ARTIFACT_ID,
203            MAVEN_VERSION,
204            MAVEN_ARTIFACT_ID,
205            MAVEN_VERSION
206        );
207
208        info!("Downloading from: {}", jar_url);
209
210        let response = reqwest::blocking::get(&jar_url)
211            .map_err(|e| ServiceError::DownloadFailed(format!("Failed to download JAR: {}", e)))?;
212
213        if !response.status().is_success() {
214            return Err(ServiceError::DownloadFailed(format!(
215                "HTTP {}: {}",
216                response.status(),
217                jar_url
218            )));
219        }
220
221        let bytes = response
222            .bytes()
223            .map_err(|e| ServiceError::DownloadFailed(format!("Failed to read response: {}", e)))?;
224
225        fs::create_dir_all(&self.cache_dir)?;
226        let jar_path = self.jar_path();
227
228        let mut file = File::create(&jar_path)?;
229        file.write_all(&bytes)?;
230
231        info!("Downloaded JAR to: {}", jar_path.display());
232        Ok(())
233    }
234
235    pub fn cache_dir(&self) -> &Path {
236        &self.cache_dir
237    }
238
239    /// Clear all cached JARs. Use when you need a fresh download.
240    pub fn clear_cache(&self) -> ServiceResult<usize> {
241        let mut removed = 0;
242
243        if let Ok(entries) = fs::read_dir(&self.cache_dir) {
244            for entry in entries.flatten() {
245                let path = entry.path();
246                if let Some(name) = path.file_name().map(|s| s.to_string_lossy()) {
247                    if name.starts_with("solverforge-wasm-service-")
248                        && name.ends_with("-runner.jar")
249                    {
250                        info!("Removing cached JAR: {}", path.display());
251                        if fs::remove_file(&path).is_ok() {
252                            removed += 1;
253                        }
254                    }
255                }
256            }
257        }
258
259        // Also remove quarkus-app directory if it exists (legacy)
260        let quarkus_dir = self.cache_dir.join("quarkus-app");
261        if quarkus_dir.is_dir() {
262            info!("Removing legacy quarkus-app directory");
263            if fs::remove_dir_all(&quarkus_dir).is_ok() {
264                removed += 1;
265            }
266        }
267
268        Ok(removed)
269    }
270
271    /// Clean up old JAR versions from cache, keeping only the current version.
272    pub fn cleanup_old_versions(&self) -> ServiceResult<usize> {
273        let current_jar = self.jar_path();
274        let current_name = current_jar
275            .file_name()
276            .map(|s| s.to_string_lossy().to_string())
277            .unwrap_or_default();
278
279        let mut removed = 0;
280
281        if let Ok(entries) = fs::read_dir(&self.cache_dir) {
282            for entry in entries.flatten() {
283                let path = entry.path();
284                if let Some(name) = path.file_name().map(|s| s.to_string_lossy()) {
285                    // Only remove old solverforge-wasm-service JARs
286                    if name.starts_with("solverforge-wasm-service-")
287                        && name.ends_with("-runner.jar")
288                        && name != current_name
289                    {
290                        info!("Removing old JAR: {}", path.display());
291                        if fs::remove_file(&path).is_ok() {
292                            removed += 1;
293                        }
294                    }
295                }
296            }
297        }
298
299        // Also remove quarkus-app directory if it exists (legacy)
300        let quarkus_dir = self.cache_dir.join("quarkus-app");
301        if quarkus_dir.is_dir() {
302            info!("Removing legacy quarkus-app directory");
303            if fs::remove_dir_all(&quarkus_dir).is_ok() {
304                removed += 1;
305            }
306        }
307
308        Ok(removed)
309    }
310}
311
312#[cfg(test)]
313mod tests {
314    use super::*;
315    use tempfile::TempDir;
316
317    #[test]
318    fn test_jar_path() {
319        let temp = TempDir::new().unwrap();
320        let manager =
321            JarManager::with_paths(PathBuf::from("/fake/submodule"), temp.path().to_path_buf());
322
323        let jar_path = manager.jar_path();
324        // Uber-jar is named <artifactId>-<version>-runner.jar
325        assert!(jar_path
326            .to_string_lossy()
327            .contains("solverforge-wasm-service"));
328        assert!(jar_path.to_string_lossy().contains("-runner.jar"));
329    }
330
331    #[test]
332    fn test_jar_exists_false() {
333        let temp = TempDir::new().unwrap();
334        let manager =
335            JarManager::with_paths(PathBuf::from("/fake/submodule"), temp.path().to_path_buf());
336
337        assert!(!manager.jar_exists());
338    }
339
340    #[test]
341    fn test_cache_dir() {
342        let temp = TempDir::new().unwrap();
343        let manager =
344            JarManager::with_paths(PathBuf::from("/fake/submodule"), temp.path().to_path_buf());
345
346        assert_eq!(manager.cache_dir(), temp.path());
347    }
348}