java_manager/info.rs
1// Copyright 2026 TaimWay
2//
3// @file: info.rs
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17use std::fmt;
18use std::process::{Child, Command, Stdio};
19use std::str;
20
21/// Represents detailed information about a Java installation.
22///
23/// This struct contains all relevant information about a Java installation,
24/// including version, architecture, supplier, and path information.
25///
26/// # Fields
27///
28/// - `name`: The name of the Java executable (e.g., "java", "javac")
29/// - `path`: Full path to the Java executable
30/// - `version`: Java version string (e.g., "11.0.12", "1.8.0_312")
31/// - `architecture`: Architecture information (e.g., "64-bit", "32-bit")
32/// - `suppliers`: Java supplier/vendor (e.g., "OpenJDK", "Oracle")
33///
34/// # Examples
35///
36/// ```rust
37/// use java_manager::JavaInfo;
38///
39/// // Create a JavaInfo instance
40/// let java_info = JavaInfo::new(
41/// "java",
42/// "/usr/bin/java",
43/// "11.0.12",
44/// "64-bit",
45/// "OpenJDK"
46/// );
47///
48/// println!("Java Info: {}", java_info);
49/// println!("Major version: {:?}", java_info.get_major_version());
50/// ```
51#[derive(Debug, Clone)]
52pub struct JavaInfo {
53 /// Name of the Java executable
54 pub name: String,
55 /// Full path to the Java executable
56 pub path: String,
57 /// Java version string
58 pub version: String,
59 /// Architecture (32-bit or 64-bit)
60 pub architecture: String,
61 /// Java supplier/vendor
62 pub suppliers: String,
63}
64
65impl JavaInfo {
66 /// Creates a new `JavaInfo` instance.
67 ///
68 /// # Arguments
69 ///
70 /// * `name` - Name of the Java executable
71 /// * `path` - Full path to the Java executable
72 /// * `version` - Java version string
73 /// * `architecture` - Architecture information
74 /// * `suppliers` - Java supplier/vendor
75 ///
76 /// # Returns
77 ///
78 /// A new `JavaInfo` instance
79 ///
80 /// # Examples
81 ///
82 /// ```rust
83 /// use java_manager::JavaInfo;
84 ///
85 /// let info = JavaInfo::new(
86 /// "java",
87 /// "/usr/bin/java",
88 /// "11.0.12",
89 /// "64-bit",
90 /// "OpenJDK"
91 /// );
92 /// ```
93 pub fn new(name: &str, path: &str, version: &str, architecture: &str, suppliers: &str) -> Self {
94 JavaInfo {
95 name: name.to_string(),
96 path: path.to_string(),
97 version: version.to_string(),
98 architecture: architecture.to_string(),
99 suppliers: suppliers.to_string(),
100 }
101 }
102
103 /// Executes a Java command asynchronously.
104 ///
105 /// This method spawns a new process and returns immediately without waiting
106 /// for the command to complete.
107 ///
108 /// # Arguments
109 ///
110 /// * `args` - Command-line arguments to pass to Java
111 ///
112 /// # Returns
113 ///
114 /// - `Ok(Child)` - A handle to the child process
115 /// - `Err(std::io::Error)` - If the process cannot be spawned
116 ///
117 /// # Examples
118 ///
119 /// ```rust
120 /// use java_manager::JavaInfo;
121 ///
122 /// let info = JavaInfo::new("java", "/usr/bin/java", "11.0.12", "64-bit", "OpenJDK");
123 /// let child = info.execute(&["-version"]);
124 /// if let Ok(mut child) = child {
125 /// let _ = child.wait();
126 /// }
127 /// ```
128 pub fn execute(&self, args: &[&str]) -> std::io::Result<Child> {
129 Command::new(&self.path)
130 .args(args)
131 .spawn()
132 }
133
134 /// Executes a Java command and waits for completion.
135 ///
136 /// This method runs the command and waits for it to complete, returning
137 /// the exit status and output.
138 ///
139 /// # Arguments
140 ///
141 /// * `args` - Command-line arguments to pass to Java
142 ///
143 /// # Returns
144 ///
145 /// - `Ok(Output)` - Command output including status, stdout, and stderr
146 /// - `Err(std::io::Error)` - If the command fails to execute
147 ///
148 /// # Examples
149 ///
150 /// ```rust
151 /// use java_manager::JavaInfo;
152 ///
153 /// let info = JavaInfo::new("java", "/usr/bin/java", "11.0.12", "64-bit", "OpenJDK");
154 /// let output = info.execute_and_wait(&["-version"]);
155 /// if let Ok(output) = output {
156 /// println!("Exit status: {}", output.status);
157 /// }
158 /// ```
159 pub fn execute_and_wait(&self, args: &[&str]) -> std::io::Result<std::process::Output> {
160 Command::new(&self.path)
161 .args(args)
162 .output()
163 }
164
165 /// Executes a Java command and returns the output as a string.
166 ///
167 /// This method captures both stdout and stderr, returning them as a string.
168 /// If the command succeeds, stdout is returned. If it fails, stderr is returned.
169 ///
170 /// # Arguments
171 ///
172 /// * `args` - Command-line arguments to pass to Java
173 ///
174 /// # Returns
175 ///
176 /// - `Ok(String)` - Command output as a string
177 /// - `Err(std::io::Error)` - If the command fails to execute
178 ///
179 /// # Examples
180 ///
181 /// ```rust
182 /// use java_manager::JavaInfo;
183 ///
184 /// fn main() -> std::io::Result<()> {
185 /// let info = JavaInfo::new("java", "/usr/bin/java", "11.0.12", "64-bit", "OpenJDK");
186 /// let output = info.execute_with_output(&["-version"])?;
187 /// println!("Java version:\n{}", output);
188 /// Ok(())
189 /// }
190 /// ```
191 pub fn execute_with_output(&self, args: &[&str]) -> std::io::Result<String> {
192 let output = Command::new(&self.path)
193 .args(args)
194 .stdout(Stdio::piped())
195 .stderr(Stdio::piped())
196 .output()?;
197
198 if output.status.success() {
199 Ok(String::from_utf8_lossy(&output.stdout).to_string())
200 } else {
201 Ok(String::from_utf8_lossy(&output.stderr).to_string())
202 }
203 }
204
205 /// Executes a Java command and returns both stdout and stderr as separate strings.
206 ///
207 /// # Arguments
208 ///
209 /// * `args` - Command-line arguments to pass to Java
210 ///
211 /// # Returns
212 ///
213 /// - `Ok((String, String))` - Tuple containing (stdout, stderr)
214 /// - `Err(std::io::Error)` - If the command fails to execute
215 ///
216 /// # Examples
217 ///
218 /// ```rust
219 /// use java_manager::JavaInfo;
220 ///
221 /// fn main() -> std::io::Result<()> {
222 /// let info = JavaInfo::new("java", "/usr/bin/java", "11.0.12", "64-bit", "OpenJDK");
223 /// let (stdout, stderr) = info.execute_with_separate_output(&["-version"])?;
224 /// println!("Stdout:\n{}", stdout);
225 /// println!("Stderr:\n{}", stderr);
226 /// Ok(())
227 /// }
228 /// ```
229 pub fn execute_with_separate_output(&self, args: &[&str]) -> std::io::Result<(String, String)> {
230 let output = Command::new(&self.path)
231 .args(args)
232 .stdout(Stdio::piped())
233 .stderr(Stdio::piped())
234 .output()?;
235
236 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
237 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
238
239 Ok((stdout, stderr))
240 }
241
242 /// Returns the major version number of Java.
243 ///
244 /// Parses the version string to extract the major version.
245 /// Handles both old (1.8) and new (9+) version formats.
246 ///
247 /// # Returns
248 ///
249 /// - `Some(u32)` - Major version number
250 /// - `None` - If version cannot be parsed
251 ///
252 /// # Examples
253 ///
254 /// ```rust
255 /// use java_manager::JavaInfo;
256 ///
257 /// let info1 = JavaInfo::new("java", "/usr/bin/java", "1.8.0_312", "64-bit", "OpenJDK");
258 /// assert_eq!(info1.get_major_version(), Some(8));
259 ///
260 /// let info2 = JavaInfo::new("java", "/usr/bin/java", "11.0.12", "64-bit", "OpenJDK");
261 /// assert_eq!(info2.get_major_version(), Some(11));
262 /// ```
263 pub fn get_major_version(&self) -> Option<u32> {
264 let version_parts: Vec<&str> = self.version.split('.').collect();
265
266 if version_parts.is_empty() {
267 return None;
268 }
269
270 if version_parts[0] == "1" && version_parts.len() > 1 {
271 version_parts[1].parse::<u32>().ok()
272 } else {
273 version_parts[0].parse::<u32>().ok()
274 }
275 }
276
277 /// Checks if the Java version is at least the specified minimum version.
278 ///
279 /// # Arguments
280 ///
281 /// * `min_version` - Minimum major version required
282 ///
283 /// # Returns
284 ///
285 /// - `true` if Java version >= min_version
286 /// - `false` if Java version < min_version or version cannot be parsed
287 ///
288 /// # Examples
289 ///
290 /// ```rust
291 /// use java_manager::JavaInfo;
292 ///
293 /// let info = JavaInfo::new("java", "/usr/bin/java", "11.0.12", "64-bit", "OpenJDK");
294 /// assert!(info.is_at_least_version(8)); // Java 11 >= 8
295 /// assert!(info.is_at_least_version(11)); // Java 11 >= 11
296 /// assert!(!info.is_at_least_version(17)); // Java 11 < 17
297 /// ```
298 pub fn is_at_least_version(&self, min_version: u32) -> bool {
299 match self.get_major_version() {
300 Some(version) => version >= min_version,
301 None => false,
302 }
303 }
304
305 /// Extracts the Java home directory from the executable path.
306 ///
307 /// Removes the "bin" directory from the path to get the JAVA_HOME.
308 ///
309 /// # Returns
310 ///
311 /// Java home directory path
312 ///
313 /// # Examples
314 ///
315 /// ```rust
316 /// use java_manager::JavaInfo;
317 ///
318 /// let info = JavaInfo::new("java", "/usr/lib/jvm/java-11-openjdk/bin/java", "11.0.12", "64-bit", "OpenJDK");
319 /// assert_eq!(info.get_java_home(), "/usr/lib/jvm/java-11-openjdk");
320 /// ```
321 pub fn get_java_home(&self) -> String {
322 let path = std::path::Path::new(&self.path);
323 if let Some(parent) = path.parent() {
324 if parent.ends_with("bin") {
325 if let Some(java_home) = parent.parent() {
326 return java_home.to_string_lossy().to_string();
327 }
328 }
329 }
330 self.path.clone()
331 }
332
333 /// Returns a display-friendly string with key Java information.
334 ///
335 /// # Returns
336 ///
337 /// Formatted string with Java information
338 ///
339 /// # Examples
340 ///
341 /// ```rust
342 /// use java_manager::JavaInfo;
343 ///
344 /// let info = JavaInfo::new("java", "/usr/bin/java", "11.0.12", "64-bit", "OpenJDK");
345 /// println!("Display: {}", info.to_display_string());
346 /// ```
347 pub fn to_display_string(&self) -> String {
348 format!(
349 "{} {} ({}, {}) at {}",
350 self.suppliers, self.version, self.architecture, self.name, self.path
351 )
352 }
353
354 /// Validates that the Java executable exists and is executable.
355 ///
356 /// # Returns
357 ///
358 /// - `true` if the executable exists and is accessible
359 /// - `false` otherwise
360 ///
361 /// # Examples
362 ///
363 /// ```rust
364 /// use java_manager::JavaInfo;
365 ///
366 /// let info = JavaInfo::new("java", "/usr/bin/java", "11.0.12", "64-bit", "OpenJDK");
367 /// if info.is_valid() {
368 /// println!("Java installation is valid");
369 /// }
370 /// ```
371 pub fn is_valid(&self) -> bool {
372 let path = std::path::Path::new(&self.path);
373 path.exists()
374 }
375}
376
377impl fmt::Display for JavaInfo {
378 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
379 write!(
380 f,
381 "JavaInfo {{ name: {}, version: {}, architecture: {}, supplier: {}, path: {} }}",
382 self.name, self.version, self.architecture, self.suppliers, self.path
383 )
384 }
385}
386
387impl PartialEq for JavaInfo {
388 fn eq(&self, other: &Self) -> bool {
389 self.version == other.version && self.path == other.path
390 }
391}
392
393impl Eq for JavaInfo {}
394
395#[cfg(test)]
396mod tests {
397 use super::*;
398
399 /// Tests JavaInfo creation and basic properties
400 #[test]
401 fn test_java_info_creation() {
402 let info = JavaInfo::new(
403 "java",
404 "/usr/bin/java",
405 "11.0.12",
406 "64-bit",
407 "OpenJDK"
408 );
409
410 assert_eq!(info.name, "java");
411 assert_eq!(info.path, "/usr/bin/java");
412 assert_eq!(info.version, "11.0.12");
413 assert_eq!(info.architecture, "64-bit");
414 assert_eq!(info.suppliers, "OpenJDK");
415 }
416
417 /// Tests major version extraction
418 #[test]
419 fn test_get_major_version() {
420 // Test old version format (Java 8)
421 let info_8 = JavaInfo::new("java", "/path", "1.8.0_312", "64-bit", "Oracle");
422 assert_eq!(info_8.get_major_version(), Some(8));
423
424 // Test new version format (Java 11)
425 let info_11 = JavaInfo::new("java", "/path", "11.0.12", "64-bit", "OpenJDK");
426 assert_eq!(info_11.get_major_version(), Some(11));
427
428 // Test invalid version
429 let info_invalid = JavaInfo::new("java", "/path", "invalid", "64-bit", "Unknown");
430 assert_eq!(info_invalid.get_major_version(), None);
431 }
432
433 /// Tests version comparison
434 #[test]
435 fn test_is_at_least_version() {
436 let info = JavaInfo::new("java", "/path", "11.0.12", "64-bit", "OpenJDK");
437
438 assert!(info.is_at_least_version(8));
439 assert!(info.is_at_least_version(11));
440 assert!(!info.is_at_least_version(17));
441
442 // Test with invalid version
443 let info_invalid = JavaInfo::new("java", "/path", "invalid", "64-bit", "Unknown");
444 assert!(!info_invalid.is_at_least_version(8));
445 }
446
447 /// Tests Java home extraction
448 #[test]
449 fn test_get_java_home() {
450 // Test standard path
451 let info1 = JavaInfo::new("java", "/usr/lib/jvm/java-11-openjdk/bin/java", "11.0.12", "64-bit", "OpenJDK");
452 assert_eq!(info1.get_java_home(), "/usr/lib/jvm/java-11-openjdk");
453
454 // Test Windows path
455 let info2 = JavaInfo::new("java.exe", "C:\\Program Files\\Java\\jdk-11\\bin\\java.exe", "11.0.12", "64-bit", "Oracle");
456 assert_eq!(info2.get_java_home(), "C:\\Program Files\\Java\\jdk-11");
457
458 // Test path without bin directory
459 let info3 = JavaInfo::new("java", "/usr/bin/java", "11.0.12", "64-bit", "OpenJDK");
460 assert_eq!(info3.get_java_home(), "/usr/bin/java");
461 }
462
463 /// Tests display formatting
464 #[test]
465 fn test_display_formatting() {
466 let info = JavaInfo::new("java", "/usr/bin/java", "11.0.12", "64-bit", "OpenJDK");
467
468 let display_str = info.to_display_string();
469 assert!(display_str.contains("OpenJDK"));
470 assert!(display_str.contains("11.0.12"));
471 assert!(display_str.contains("64-bit"));
472 assert!(display_str.contains("/usr/bin/java"));
473 }
474
475 /// Tests equality comparison
476 #[test]
477 fn test_equality() {
478 let info1 = JavaInfo::new("java", "/usr/bin/java", "11.0.12", "64-bit", "OpenJDK");
479 let info2 = JavaInfo::new("java", "/usr/bin/java", "11.0.12", "64-bit", "OpenJDK");
480 let info3 = JavaInfo::new("java", "/usr/bin/java", "17.0.1", "64-bit", "OpenJDK");
481 let info4 = JavaInfo::new("java", "/usr/local/bin/java", "11.0.12", "64-bit", "OpenJDK");
482
483 assert_eq!(info1, info2);
484 assert_ne!(info1, info3);
485 assert_ne!(info1, info4);
486 }
487
488 /// Tests the Display trait implementation
489 #[test]
490 fn test_display_trait() {
491 let info = JavaInfo::new("java", "/usr/bin/java", "11.0.12", "64-bit", "OpenJDK");
492 let display_output = format!("{}", info);
493
494 assert!(display_output.contains("JavaInfo"));
495 assert!(display_output.contains("java"));
496 assert!(display_output.contains("11.0.12"));
497 assert!(display_output.contains("64-bit"));
498 assert!(display_output.contains("OpenJDK"));
499 assert!(display_output.contains("/usr/bin/java"));
500 }
501
502 /// Tests validation of Java executable
503 #[test]
504 fn test_is_valid() {
505 // Test with a path that should exist (current executable)
506 let current_exe = std::env::current_exe().unwrap();
507 let info_valid = JavaInfo::new(
508 "test",
509 current_exe.to_str().unwrap(),
510 "1.0.0",
511 "64-bit",
512 "Test"
513 );
514 assert!(info_valid.is_valid());
515
516 // Test with non-existent path
517 let info_invalid = JavaInfo::new(
518 "nonexistent",
519 "/path/that/does/not/exist",
520 "1.0.0",
521 "64-bit",
522 "Test"
523 );
524 assert!(!info_invalid.is_valid());
525 }
526
527 /// Tests command execution (if Java is available)
528 #[test]
529 fn test_execute_with_output() {
530 // Only run this test if Java is available
531 if let Ok(java_home) = crate::locate_java_home() {
532 let java_exec = format!("{}/bin/java", java_home);
533 if std::path::Path::new(&java_exec).exists() {
534 let info = JavaInfo::new("java", &java_exec, "unknown", "unknown", "unknown");
535
536 // Test version command
537 let result = info.execute_with_output(&["-version"]);
538 assert!(result.is_ok());
539
540 let output = result.unwrap();
541 assert!(!output.is_empty());
542 println!("Java version output:\n{}", output);
543 }
544 }
545 }
546}