solverforge_service/
jar.rs1use 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
9const 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 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 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 if jar_path.exists() {
58 debug!("Using cached JAR: {}", jar_path.display());
59 return Ok(jar_path);
60 }
61
62 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 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 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 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 let java_home = if let Some(ref home) = self.java_home {
133 home.clone()
134 } else {
135 let java = find_java(None)?;
137 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 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 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 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 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 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 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 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 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 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}