Skip to main content

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}