1use crate::{
2 prelude::{candidates_api, candidates_dir, platform, Candidate},
3 sdkman::{archives_dir, temp_dir},
4};
5use std::{ffi::OsStr, io};
6
7pub(crate) mod results {
8 pub use super::default_command::{ChangedDefault, DefaultResult};
9 pub use super::install_command::InstallError;
10 pub use super::install_command::InstallResult;
11 pub use super::use_command::{UseResult, Using};
12}
13
14pub struct JdkCurrent;
15pub struct JdkDefault;
16pub struct JdkInstall;
17pub struct JdkList;
18pub struct JdkUse;
19
20impl JdkCurrent {
21 pub fn run() -> Option<Candidate> {
22 current_command::run(candidates_dir())
23 }
24}
25
26impl JdkDefault {
27 pub fn run(
28 query: Option<impl AsRef<OsStr>>,
29 show_suggested_default: bool,
30 verbose: bool,
31 ) -> io::Result<results::DefaultResult> {
32 if show_suggested_default {
33 Ok(results::DefaultResult::Suggest(default_command::suggested(
34 verbose,
35 candidates_api(),
36 )?))
37 } else {
38 default_command::run(candidates_dir(), verbose, query.as_ref().map(OsStr::new))
39 }
40 }
41
42 pub fn set(candidate: Candidate) -> io::Result<results::ChangedDefault> {
43 default_command::set_default(candidates_dir(), candidate)
44 }
45}
46
47impl JdkList {
48 pub fn run(verbose: bool, all: bool) -> io::Result<String> {
49 let current = JdkCurrent::run();
50 if all {
51 list_command::run_online(
52 current.as_ref().map(Candidate::name),
53 candidates_dir(),
54 candidates_api(),
55 platform(),
56 verbose,
57 )
58 } else {
59 list_command::run(current.as_ref().map(Candidate::name), candidates_dir())
60 }
61 }
62}
63
64impl JdkInstall {
65 pub fn run(
66 verbose: bool,
67 install_suggested_default: bool,
68 validate_version: bool,
69 version: &str,
70 ) -> results::InstallResult {
71 install_command::run(
72 candidates_dir(),
73 archives_dir(),
74 temp_dir(),
75 candidates_api(),
76 platform(),
77 verbose,
78 install_suggested_default,
79 validate_version,
80 version,
81 )
82 }
83}
84
85impl JdkUse {
86 pub fn run(query: Option<impl AsRef<OsStr>>, verbose: bool) -> io::Result<results::UseResult> {
87 let current = JdkCurrent::run();
88 use_command::run(
89 candidates_dir(),
90 current.as_ref().map(Candidate::name),
91 verbose,
92 query.as_ref().map(OsStr::new),
93 )
94 }
95
96 pub fn set(candidate: Candidate) -> results::Using {
97 use_command::set_use(candidates_dir(), candidate)
98 }
99}
100
101mod current_command {
102 use crate::candidate::Candidate;
103 use std::{env, fs, path::Path};
104
105 pub(super) fn run(candidates_dir: &Path) -> Option<Candidate> {
106 let path = env::var_os("PATH").unwrap_or_default();
112 env::split_paths(&path)
113 .find_map(|path| {
114 path.starts_with(candidates_dir)
115 .then(|| {
116 path.ancestors()
122 .skip(1)
125 .take_while(|path| path.starts_with(candidates_dir))
127 .try_fold(&*path, |parent_parent, parent| {
128 if parent == candidates_dir {
129 Err(parent_parent)
131 } else {
132 Ok(parent)
134 }
135 })
136 .err()
138 .map(ToOwned::to_owned)
139 })
140 .flatten()
141 })
142 .map(fs::canonicalize)
148 .and_then(Result::ok)
149 .map(Candidate::new)
150 }
151}
152
153mod use_command {
154 use super::shared::select_candidates;
155 use crate::{prelude::Candidate, select::Selection};
156 use std::{
157 env,
158 ffi::{OsStr, OsString},
159 io,
160 path::{Path, PathBuf},
161 };
162
163 pub enum UseResult {
164 KeepCurrent,
165 Invalid(Option<String>),
166 Use(Using),
167 }
168
169 pub struct Using {
170 pub name: String,
171 pub java_home: PathBuf,
172 pub path: Option<OsString>,
173 }
174
175 pub(super) fn run(
176 candidates_dir: &Path,
177 current: Option<&str>,
178 verbose: bool,
179 query: Option<&OsStr>,
180 ) -> io::Result<UseResult> {
181 let selected = select_candidates(query, current, candidates_dir, verbose)?;
182
183 let use_result = match selected {
184 Selection::Cancelled => UseResult::KeepCurrent,
185 Selection::NoMatch => {
186 UseResult::Invalid(query.and_then(|q| q.to_str()).map(String::from))
187 }
188 Selection::Selected(selection) => {
189 let using = set_use(candidates_dir, selection);
190 UseResult::Use(using)
191 }
192 };
193
194 Ok(use_result)
195 }
196
197 pub(super) fn set_use(candidates_dir: &Path, candidate: Candidate) -> Using {
198 let name = String::from(candidate.name());
199 let candidate_path = candidate.into_path();
200
201 let old_path = env::var_os("PATH");
202 let new_path = old_path.map(|path| {
203 let mut candidate_path = Some(candidate_path.clone());
205 let new_path = env::split_paths(&path).filter_map(|path| {
206 if path.starts_with(candidates_dir) {
207 candidate_path.take().map(|mut new_candidate_path| {
208 let previous_path = path
209 .strip_prefix(candidates_dir)
210 .expect("starts_with returned true");
211 for path_segment in previous_path.iter().skip(1) {
214 new_candidate_path.push(path_segment);
215 }
216 new_candidate_path
217 })
218 } else {
219 Some(path)
220 }
221 });
222 env::join_paths(new_path).expect("input is from existing PATH")
223 });
224
225 Using {
226 name,
227 java_home: candidate_path,
228 path: new_path,
229 }
230 }
231}
232
233mod default_command {
234 use super::shared::{request, select_candidates};
235 use crate::{prelude::Candidate, select::Selection};
236 use std::{
237 ffi::OsStr,
238 fs,
239 io::{self, ErrorKind},
240 path::Path,
241 };
242
243 #[cfg(unix)]
244 use std::os::unix::fs::symlink;
245 #[cfg(windows)]
246 use std::os::windows::fs::symlink_dir as symlink;
247
248 pub enum DefaultResult {
249 KeepCurrent,
250 CandidateNotFound { query: Option<String> },
251 Selected(ChangedDefault),
252 Suggest(String),
253 }
254
255 pub struct ChangedDefault {
256 pub name: String,
257 pub before: Option<String>,
258 }
259
260 pub(super) fn suggested(verbose: bool, candidates_api: &str) -> io::Result<String> {
261 request(format!("{candidates_api}/candidates/default/java"), verbose)
262 }
263
264 pub(super) fn run(
265 candidates_dir: &Path,
266 verbose: bool,
267 query: Option<&OsStr>,
268 ) -> io::Result<DefaultResult> {
269 let selected = select_candidates(query, None, candidates_dir, verbose)?;
270
271 let default_result = match selected {
272 Selection::Cancelled => DefaultResult::KeepCurrent,
273 Selection::NoMatch => {
274 let query = query.and_then(|q| q.to_str()).map(String::from);
275 DefaultResult::CandidateNotFound { query }
276 }
277 Selection::Selected(selection) => {
278 let selected = set_default(candidates_dir, selection)?;
279 DefaultResult::Selected(selected)
280 }
281 };
282
283 Ok(default_result)
284 }
285
286 pub(super) fn set_default(
287 candidates_dir: &Path,
288 candidate: Candidate,
289 ) -> io::Result<ChangedDefault> {
290 let name = String::from(candidate.name());
291 let candidate_path = candidate.into_path();
292 let current_path = candidates_dir.join("current");
293
294 let previous = match fs::symlink_metadata(¤t_path) {
296 Ok(meta) => {
297 if meta.is_file() || meta.is_dir() {
298 return Err(io::Error::new(
299 ErrorKind::AlreadyExists,
300 format!(
301 "The 'current' link [{}] cannot be changed since it is not a link.",
302 current_path.display()
303 ),
304 ));
305 }
306 let previous = fs::read_link(¤t_path)?;
307 let previous = previous.strip_prefix(candidates_dir).unwrap_or(&previous);
308 let previous = previous.to_str().map(String::from);
309
310 fs::remove_file(¤t_path)?;
311
312 previous
313 }
314 Err(err) if err.kind() == ErrorKind::NotFound => None,
315 Err(e) => return Err(e),
316 };
317
318 let candidate_path = candidate_path
320 .strip_prefix(candidates_dir)
321 .unwrap_or(&candidate_path);
322
323 symlink(candidate_path, ¤t_path)?;
324
325 Ok(ChangedDefault {
326 name,
327 before: previous,
328 })
329 }
330}
331
332mod validate_command {
333 use super::shared::request;
334 use std::io;
335
336 pub(super) fn run(
337 candidates_api: &str,
338 platform: &str,
339 verbose: bool,
340 version: &str,
341 ) -> io::Result<bool> {
342 let url = format!("{candidates_api}/candidates/validate/java/{version}/{platform}",);
343 let valid = request(url, verbose)?;
344 match valid.trim() {
345 "valid" => Ok(true),
346 "invalid" => Ok(false),
347 otherwise => {
348 let err_msg = format!("Unexpected validation response, expected either 'valid' or 'invalid', but got {otherwise}");
349 Err(crate::io_err(&err_msg))
350 }
351 }
352 }
353}
354
355mod install_command {
356 use super::{default_command, validate_command};
357 use crate::{eprintln_color, prelude::Candidate};
358 use bstr::ByteSlice;
359 use console::Style;
360 use std::{
361 fs::{self, OpenOptions},
362 io::{self, BufWriter, ErrorKind},
363 path::{Path, PathBuf},
364 process::{Command, Stdio},
365 time::Instant,
366 };
367
368 pub type InstallResult = Result<Candidate, InstallError>;
369
370 pub enum InstallError {
371 AlreadyInstalled(String),
372 InvalidVersion(String),
373 ArchiveCorrupt(String, PathBuf),
374 DownloadError(reqwest::Error),
375 Other(io::Error),
376 }
377
378 impl From<io::Error> for InstallError {
379 fn from(val: io::Error) -> Self {
380 Self::Other(val)
381 }
382 }
383 impl From<reqwest::Error> for InstallError {
384 fn from(val: reqwest::Error) -> Self {
385 Self::DownloadError(val)
386 }
387 }
388
389 #[allow(clippy::too_many_arguments)]
390 pub(super) fn run(
391 candidates_dir: &Path,
392 archives_dir: &Path,
393 temp_dir: &Path,
394 candidates_api: &str,
395 platform: &str,
396 verbose: bool,
397 install_suggested_default: bool,
398 validate_version: bool,
399 version: &str,
400 ) -> InstallResult {
401 let default_version;
403 let version = if install_suggested_default {
404 default_version = default_command::suggested(verbose, candidates_api)?;
405 &default_version
406 } else {
407 version
408 };
409
410 let candidates_path = validate_candidate(
411 candidates_dir,
412 candidates_api,
413 platform,
414 verbose,
415 validate_version,
416 version,
417 )?;
418
419 let zip_archive_target = archives_dir.join(format!("java-{version}.zip"));
420
421 if !matches!(fs::symlink_metadata(&zip_archive_target), Ok(meta) if meta.is_file()) {
423 download(
424 temp_dir,
425 candidates_api,
426 platform,
427 verbose,
428 version,
429 &zip_archive_target,
430 )?;
431 } else {
432 eprintln!(
433 "Found a previously downloaded java {version} archive. Not downloading it again..."
434 );
435 }
436
437 let zip_archive_target = validate_archive(version, zip_archive_target)?;
438
439 install_archive(temp_dir, version, candidates_path, zip_archive_target)
440 }
441
442 pub(super) fn validate_candidate(
443 candidates_dir: &Path,
444 candidates_api: &str,
445 platform: &str,
446 verbose: bool,
447 validate_version: bool,
448 version: &str,
449 ) -> Result<PathBuf, InstallError> {
450 let candidates_path = candidates_dir.join(version);
451 if matches!(fs::symlink_metadata(&candidates_path), Ok(existing) if !existing.is_file()) {
452 return Err(InstallError::AlreadyInstalled(version.into()));
453 }
454
455 if validate_version && !validate_command::run(candidates_api, platform, verbose, version)? {
456 return Err(InstallError::InvalidVersion(version.into()));
457 };
458
459 Ok(candidates_path)
460 }
461
462 pub(super) fn download(
463 temp_dir: &Path,
464 candidates_api: &str,
465 platform: &str,
466 verbose: bool,
467 version: &str,
468 zip_archive_target: &Path,
469 ) -> Result<(), InstallError> {
470 pre_install(temp_dir, candidates_api, platform, verbose, version)?;
471
472 let url = format!("{candidates_api}/broker/download/java/{version}/{platform}");
473
474 let binary_input = temp_dir.join(format!("java-{version}.bin"));
475
476 let file = OpenOptions::new()
477 .write(true)
478 .create_new(true)
479 .open(&binary_input)?;
480
481 let start = Instant::now();
485 let mut response = reqwest::blocking::get(&url)?;
486
487 if let Some(size) = response.content_length() {
488 file.set_len(size)?;
489 }
490
491 let mut wtr = BufWriter::new(file);
492 let bytes = response.copy_to(&mut wtr)?;
493
494 if verbose {
495 let took = start.elapsed();
496 eprintln_color!(@Style::new().for_stderr().dim(), "Downloaded {} bytes of binaryto {} in {:?}", bytes, binary_input.display(), took);
497 }
498
499 let zip_output = post_install(
500 temp_dir,
501 candidates_api,
502 platform,
503 verbose,
504 version,
505 binary_input,
506 )?;
507
508 dbg!(fs::rename(dbg!(zip_output), dbg!(&zip_archive_target)))?;
509
510 Ok(())
511 }
512
513 pub(super) fn validate_archive(
514 version: &str,
515 zip_archive_target: PathBuf,
516 ) -> Result<PathBuf, InstallError> {
517 let output = Command::new("unzip")
518 .arg("-t")
519 .arg(&zip_archive_target)
520 .stdin(Stdio::null())
521 .stdout(Stdio::piped())
522 .stderr(Stdio::inherit())
523 .output()?;
524
525 if !output.status.success()
526 || !output
527 .stdout
528 .contains_str("No errors detected in compressed data")
529 {
530 try_fs(|| fs::remove_file(&zip_archive_target))?;
531
532 return Err(InstallError::ArchiveCorrupt(
533 version.into(),
534 zip_archive_target,
535 ));
536 }
537
538 Ok(zip_archive_target)
539 }
540
541 pub(super) fn install_archive(
542 temp_dir: &Path,
543 version: &str,
544 candidates_path: PathBuf,
545 zip_archive_target: PathBuf,
546 ) -> InstallResult {
547 let out = temp_dir.join("out");
548 try_fs(|| fs::remove_dir_all(&out))?;
549
550 if !Command::new("unzip")
551 .arg("-oq")
552 .arg(&zip_archive_target)
553 .arg("-d")
554 .arg(&out)
555 .status()?
556 .success()
557 {
558 return Err(InstallError::ArchiveCorrupt(
559 version.into(),
560 zip_archive_target,
561 ));
562 }
563
564 dbg!(fs::rename(dbg!(out), dbg!(&candidates_path)))?;
565
566 Ok(Candidate::new(candidates_path))
567 }
568
569 fn pre_install(
570 _temp_dir: &Path,
571 _candidates_api: &str,
572 _platform: &str,
573 _verbose: bool,
574 _version: &str,
575 ) -> io::Result<()> {
576 Ok(())
587 }
588
589 fn post_install(
590 temp_dir: &Path,
591 _candidates_api: &str,
592 _platform: &str,
593 _verbose: bool,
594 version: &str,
595 binary_input: PathBuf,
596 ) -> Result<PathBuf, InstallError> {
597 eprintln!("Running tar test");
610
611 let binary_validation = Command::new("tar")
612 .arg("tzf")
613 .arg(&binary_input)
614 .stdout(Stdio::null())
615 .status()?;
616 if !binary_validation.success() {
617 return Err(InstallError::ArchiveCorrupt(version.into(), binary_input));
618 }
619
620 let work_dir = temp_dir.join("out");
621 fs::create_dir_all(&work_dir)?;
622
623 eprintln!("Running tar extract");
624
625 if !Command::new("tar")
626 .arg("zxf")
627 .arg(&binary_input)
628 .arg("-C")
629 .arg(&work_dir)
630 .status()?
631 .success()
632 {
633 return Err(InstallError::ArchiveCorrupt(version.into(), binary_input));
634 }
635
636 let zip_output = binary_input.with_extension("zip");
637
638 eprintln!("Running zip");
639
640 if !Command::new("zip")
641 .current_dir(&work_dir)
642 .arg("-qyr")
643 .arg(&zip_output)
644 .arg(".")
645 .status()?
646 .success()
647 {
648 return Err(InstallError::ArchiveCorrupt(version.into(), zip_output));
649 }
650
651 fs::remove_file(binary_input)?;
652 fs::remove_dir_all(work_dir)?;
653
654 Ok(zip_output)
655 }
656
657 fn try_fs(f: impl FnOnce() -> io::Result<()>) -> io::Result<()> {
658 match f() {
659 Err(e) if e.kind() == ErrorKind::NotFound => Ok(()),
660 otherwise => otherwise,
661 }
662 }
663}
664
665mod list_command {
666 use super::shared::list_candidates;
667 use crate::commands::shared::request;
668 use std::{fmt::Write, io, path::Path};
669
670 pub(super) fn run(current: Option<&str>, candidates_dir: &Path) -> io::Result<String> {
671 let candidates = list_candidates(candidates_dir)?;
672
673 let mut output = String::with_capacity(1024);
674 output.push_str(
675 "--------------------------------------------------------------------------------\n",
676 );
677 if candidates.is_empty() && crate::use_color() {
678 let _ = writeln!(
679 &mut output,
680 " {}",
681 console::style("None installed!").yellow()
682 );
683 }
684
685 for candidate in candidates {
686 let candidate = candidate.into_name();
687
688 if matches!(current, Some(c) if c == candidate) {
689 let _ = writeln!(&mut output, " > {candidate}");
690 } else {
691 let _ = writeln!(&mut output, " * {candidate}");
692 }
693 }
694
695 output.push_str(
696 "--------------------------------------------------------------------------------\n",
697 );
698 output.push_str(
699 "* - installed \n",
700 );
701 output.push_str(
702 "> - currently in use \n",
703 );
704 output.push_str(
705 "--------------------------------------------------------------------------------\n",
706 );
707
708 Ok(output)
709 }
710
711 pub(super) fn run_online(
712 current: Option<&str>,
713 candidates_dir: &Path,
714 candidates_api: &str,
715 platform: &str,
716 verbose: bool,
717 ) -> io::Result<String> {
718 let installed = list_candidates(candidates_dir)?;
719
720 let url = format!(
721 "{api}/candidates/java/{platform}/versions/list?current={current}&installed=${installed}",
722 api = candidates_api,
723 platform = platform,
724 current = current.unwrap_or_default(),
725 installed = installed.join(",")
726 );
727
728 request(url, verbose)
729 }
730}
731
732mod shared {
733
734 use crate::io_err;
735 use crate::{
736 prelude::Candidate,
737 select::{SelectOptions, Selection},
738 };
739 use std::{
740 ffi::OsStr,
741 fs::{self, DirEntry},
742 io,
743 path::Path,
744 };
745
746 pub(super) fn request(url: String, _verbose: bool) -> io::Result<String> {
747 static APP_USER_AGENT: &str =
748 concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
749 let response = reqwest::blocking::Client::builder()
752 .user_agent(APP_USER_AGENT)
753 .connection_verbose(_verbose)
754 .build()
755 .map_err(io_err)?
756 .get(&url)
757 .send()
758 .map_err(io_err)?
759 .error_for_status()
760 .map_err(io_err)?
761 .text()
762 .map_err(io_err)?;
763
764 Ok(response)
770 }
771
772 pub(super) fn list_candidates(candidates_dir: &Path) -> io::Result<Vec<Candidate>> {
786 let mut candidates = fs::read_dir(candidates_dir)?
787 .filter_map(read_entry)
788 .collect::<Vec<_>>();
789
790 candidates.sort();
792
793 Ok(candidates)
794 }
795
796 pub(super) fn select_candidates(
797 query: Option<&OsStr>,
798 current: Option<&str>,
799 candidates_dir: &Path,
800 verbose: bool,
801 ) -> io::Result<Selection<Candidate>> {
802 let candidates = list_candidates(candidates_dir)?;
803
804 let pre_select = current
806 .and_then(|current| candidates.iter().position(|c| c.name() == current))
807 .map(|p| p as u32);
808
809 let preview_command = format!(
813 "{0}/{{2}}/bin/java --version; {0}/{{2}}/bin/java -Xinternalversion",
814 candidates_dir.display()
815 );
816
817 let selected = SelectOptions::new(candidates)
818 .pre_select(pre_select)
819 .query(query)
820 .preview_command(preview_command.as_str())
821 .verbose(verbose)
822 .select()?;
823
824 Ok(selected)
825 }
826
827 fn read_entry(entry: io::Result<DirEntry>) -> Option<Candidate> {
828 let entry = entry.ok()?;
829
830 entry
835 .metadata()
836 .ok()?
837 .is_dir()
838 .then(|| Candidate::new(entry.path()))
839 }
840}