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 = "0.2.1";
13const MAVEN_CENTRAL_URL: &str = "https://repo1.maven.org/maven2";
14
15pub struct JarManager {
16    submodule_dir: PathBuf,
17    cache_dir: PathBuf,
18    java_home: Option<PathBuf>,
19}
20
21impl JarManager {
22    pub fn new() -> ServiceResult<Self> {
23        let submodule_dir = find_submodule_dir()?;
24        let cache_dir = get_cache_dir();
25        Ok(Self {
26            submodule_dir,
27            cache_dir,
28            java_home: None,
29        })
30    }
31
32    pub fn with_paths(submodule_dir: PathBuf, cache_dir: PathBuf) -> Self {
33        Self {
34            submodule_dir,
35            cache_dir,
36            java_home: None,
37        }
38    }
39
40    pub fn with_java_home(mut self, java_home: Option<&Path>) -> Self {
41        self.java_home = java_home.map(|p| p.to_path_buf());
42        self
43    }
44
45    pub fn ensure_jar(&self) -> ServiceResult<PathBuf> {
46        let jar_path = self.jar_path();
47
48        // 1. Check cache first
49        if jar_path.exists() {
50            debug!("Using cached JAR: {}", jar_path.display());
51            return Ok(jar_path);
52        }
53
54        // 2. Try local build if submodule exists (dev mode)
55        if self.submodule_dir.join("pom.xml").exists() {
56            info!("Building solverforge-wasm-service JAR from submodule...");
57            match self.build_jar() {
58                Ok(()) => {
59                    if jar_path.exists() {
60                        return Ok(jar_path);
61                    }
62                }
63                Err(e) => {
64                    warn!("Local build failed: {}, trying Maven download...", e);
65                }
66            }
67        }
68
69        // 3. Download from Maven Central (production mode)
70        info!("Downloading solverforge-wasm-service from Maven Central...");
71        self.download_from_maven()?;
72
73        if !jar_path.exists() {
74            return Err(ServiceError::BuildFailed(
75                "JAR not found after download".to_string(),
76            ));
77        }
78
79        Ok(jar_path)
80    }
81
82    pub fn jar_exists(&self) -> bool {
83        self.jar_path().exists()
84    }
85
86    pub fn jar_path(&self) -> PathBuf {
87        // Uber-jar is a single self-contained JAR
88        self.cache_dir.join(format!(
89            "{}-{}-runner.jar",
90            MAVEN_ARTIFACT_ID, MAVEN_VERSION
91        ))
92    }
93
94    pub fn rebuild(&self) -> ServiceResult<PathBuf> {
95        let jar_path = self.jar_path();
96        if jar_path.exists() {
97            fs::remove_file(&jar_path)?;
98        }
99        self.ensure_jar()
100    }
101
102    fn build_jar(&self) -> ServiceResult<()> {
103        let mvn = find_maven()?;
104
105        fs::create_dir_all(&self.cache_dir)?;
106
107        // Determine JAVA_HOME for Maven - it must use the same Java version
108        // that solverforge-wasm-service was compiled with (Java 24)
109        let java_home = if let Some(ref home) = self.java_home {
110            home.clone()
111        } else {
112            // Find java and derive JAVA_HOME from it
113            let java = find_java(None)?;
114            // java is typically at $JAVA_HOME/bin/java, so go up two levels
115            java.parent()
116                .and_then(|bin| bin.parent())
117                .map(|home| home.to_path_buf())
118                .ok_or_else(|| {
119                    ServiceError::JavaNotFound("Cannot determine JAVA_HOME from java path".into())
120                })?
121        };
122
123        info!(
124            "Running mvn package in {} with JAVA_HOME={}",
125            self.submodule_dir.display(),
126            java_home.display()
127        );
128
129        let output = Command::new(&mvn)
130            .current_dir(&self.submodule_dir)
131            .env("JAVA_HOME", &java_home)
132            .args(["package", "-DskipTests", "-q"])
133            .output()?;
134
135        if !output.status.success() {
136            let stderr = String::from_utf8_lossy(&output.stderr);
137            return Err(ServiceError::BuildFailed(format!(
138                "Maven build failed: {}",
139                stderr
140            )));
141        }
142
143        // Uber-jar is named <artifactId>-<version>-runner.jar
144        let built_jar = self.submodule_dir.join("target").join(format!(
145            "{}-{}-runner.jar",
146            MAVEN_ARTIFACT_ID, MAVEN_VERSION
147        ));
148
149        if !built_jar.exists() {
150            return Err(ServiceError::BuildFailed(format!(
151                "Expected JAR not found at {}",
152                built_jar.display()
153            )));
154        }
155
156        fs::create_dir_all(&self.cache_dir)?;
157        let cached_jar = self.jar_path();
158
159        info!("Copying JAR to cache: {}", cached_jar.display());
160        fs::copy(&built_jar, &cached_jar)?;
161
162        Ok(())
163    }
164
165    fn download_from_maven(&self) -> ServiceResult<()> {
166        // Maven Central URL pattern: /group/artifact/version/artifact-version-classifier.jar
167        let group_path = MAVEN_GROUP_ID.replace('.', "/");
168        let jar_url = format!(
169            "{}/{}/{}/{}/{}-{}-runner.jar",
170            MAVEN_CENTRAL_URL,
171            group_path,
172            MAVEN_ARTIFACT_ID,
173            MAVEN_VERSION,
174            MAVEN_ARTIFACT_ID,
175            MAVEN_VERSION
176        );
177
178        info!("Downloading from: {}", jar_url);
179
180        let response = reqwest::blocking::get(&jar_url)
181            .map_err(|e| ServiceError::DownloadFailed(format!("Failed to download JAR: {}", e)))?;
182
183        if !response.status().is_success() {
184            return Err(ServiceError::DownloadFailed(format!(
185                "HTTP {}: {}",
186                response.status(),
187                jar_url
188            )));
189        }
190
191        let bytes = response
192            .bytes()
193            .map_err(|e| ServiceError::DownloadFailed(format!("Failed to read response: {}", e)))?;
194
195        fs::create_dir_all(&self.cache_dir)?;
196        let jar_path = self.jar_path();
197
198        let mut file = File::create(&jar_path)?;
199        file.write_all(&bytes)?;
200
201        info!("Downloaded JAR to: {}", jar_path.display());
202        Ok(())
203    }
204
205    pub fn cache_dir(&self) -> &Path {
206        &self.cache_dir
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213    use tempfile::TempDir;
214
215    #[test]
216    fn test_jar_path() {
217        let temp = TempDir::new().unwrap();
218        let manager =
219            JarManager::with_paths(PathBuf::from("/fake/submodule"), temp.path().to_path_buf());
220
221        let jar_path = manager.jar_path();
222        // Uber-jar is named <artifactId>-<version>-runner.jar
223        assert!(jar_path
224            .to_string_lossy()
225            .contains("solverforge-wasm-service"));
226        assert!(jar_path.to_string_lossy().contains("-runner.jar"));
227    }
228
229    #[test]
230    fn test_jar_exists_false() {
231        let temp = TempDir::new().unwrap();
232        let manager =
233            JarManager::with_paths(PathBuf::from("/fake/submodule"), temp.path().to_path_buf());
234
235        assert!(!manager.jar_exists());
236    }
237
238    #[test]
239    fn test_cache_dir() {
240        let temp = TempDir::new().unwrap();
241        let manager =
242            JarManager::with_paths(PathBuf::from("/fake/submodule"), temp.path().to_path_buf());
243
244        assert_eq!(manager.cache_dir(), temp.path());
245    }
246}