|
#!/home/nipa/.sdkman/candidates/java/25/bin/java --source 25 |
|
import static java.util.Comparator.comparing; |
|
|
|
private static final String CANDIDATE_TABLE_START = "----------"; |
|
private static final String CANDIDATE_TABLE_END = "=========="; |
|
private static final String IN_PROGRESS_MESSAGE = "In progress..."; |
|
private static final int PROGRESS_MESSAGE_INTERVAL = 5; |
|
|
|
private static final Pattern FEATURE_VERSION = Pattern.compile("(\\d+)[\\.\\-].*"); |
|
private static final Pattern JDK_CANDIDATE = Pattern.compile(""" |
|
^\\s*(?<vendor>[^|]+\\S+)?\\s*\ |
|
\\|\\s*(?<use>>*)\\s*\ |
|
\\|\\s*(?<version>\\S+)\\s*\ |
|
\\|\\s*(?<dist>\\w+)\\s*\ |
|
\\|\\s*(?<status>\\w+.*\\w)?\\s*\ |
|
\\|\\s*(?<identifier>\\S+)\\s*$\ |
|
"""); |
|
private static final Pattern DEFAULT_VERSION = Pattern.compile("Using java version (.+)"); |
|
private static final Pattern DOWNLOAD_PROGRESS = Pattern.compile("#+\\s*([\\d\\.]+%)"); |
|
|
|
private static final List<String> DISTRIBUTIONS_FOR_LATEST_VERSION = List.of("open"); |
|
private static final List<String> DISTRIBUTIONS_FOR_FEATURE_VERSION = List.of("open", "oracle"); |
|
|
|
private static final Path SDKMAN_CANDIDATES_DIR = Path.of(System.getProperty("user.home"), ".sdkman", "candidates", "java"); |
|
|
|
public static void main(String[] args) { |
|
try { |
|
var jdkCandidate = selectJdkToInstall(args); |
|
if (jdkCandidate.isEmpty()) { |
|
IO.println("⛔ No suitable JDK found"); |
|
} else if (jdkCandidate.get().installed()) { |
|
var jdk = jdkCandidate.get(); |
|
IO.println("✅ Selected " + jdk.identifier() + ", which is already installed"); |
|
int featureVersion = extractFeatureVersion(jdk.identifier()); |
|
createSymlink(jdk.identifier(), featureVersion); |
|
} else { |
|
var jdk = jdkCandidate.get().identifier(); |
|
IO.println("✅ Selected " + jdk + " to install"); |
|
|
|
int featureVersion = extractFeatureVersion(jdk); |
|
var defaultJdk = determineDefaultJdk(); |
|
installJdk(jdk); |
|
createSymlink(jdk, featureVersion); |
|
resetDefaultJdk(defaultJdk); |
|
|
|
IO.println("✅ Installed JDK " + jdk + " and created/updated the symlink"); |
|
} |
|
} catch (Exception ex) { |
|
IO.println(); |
|
System.err.println("⛔ Error (%s): %s".formatted(ex.getClass().getSimpleName(), ex.getMessage())); |
|
System.exit(1); |
|
} |
|
} |
|
|
|
private static Optional<JdkCandidate> selectJdkToInstall(String[] args) throws IOException, InterruptedException { |
|
if (args.length == 0) { |
|
IO.println("⏳ No version/identifier specified -> determining latest suitable JDK..."); |
|
return getAvailableJdks() |
|
.filter(candidate -> DISTRIBUTIONS_FOR_LATEST_VERSION.contains(candidate.distribution())) |
|
.max(comparing(JdkCandidate::version)); |
|
} |
|
|
|
if (args.length == 1) { |
|
try { |
|
var featureVersion = Integer.parseInt(args[0]); |
|
IO.println("⏳ Feature version " + args[0] + " specified -> determining latest suitable JDK..."); |
|
return getAvailableJdks() |
|
.filter(candidate -> DISTRIBUTIONS_FOR_FEATURE_VERSION.contains(candidate.distribution())) |
|
.filter(candidate -> candidate.version().feature() == featureVersion) |
|
.max(comparing(JdkCandidate::version)); |
|
} catch (NumberFormatException _) { |
|
var identifier = args[0]; |
|
IO.println("⏳ Identifier " + identifier + " specified -> checking status..."); |
|
return getAvailableJdks() |
|
.filter(candidate -> candidate.identifier().equals(identifier)) |
|
.findFirst(); |
|
} |
|
} |
|
|
|
throw new IllegalArgumentException("should be `jdk_update`, `jdk_update $feature_version` or `jdk_update $jdk_identifier`"); |
|
} |
|
|
|
private static Stream<JdkCandidate> getAvailableJdks() throws IOException, InterruptedException { |
|
IO.println("☎ Fetching available JDK candidates..."); |
|
var process = execute("source $HOME/.sdkman/bin/sdkman-init.sh && sdk list java"); |
|
|
|
var candidates = new ArrayList<JdkCandidate>(); |
|
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { |
|
String line; |
|
var tableStarted = false; |
|
while ((line = reader.readLine()) != null) { |
|
if (tableStarted) { |
|
if (line.startsWith(CANDIDATE_TABLE_END)) |
|
break; |
|
|
|
Matcher matcher = JDK_CANDIDATE.matcher(line); |
|
if (matcher.matches()) { |
|
var dist = matcher.group("dist"); |
|
var version = JdkVersion.parse(matcher.group("version")); |
|
var status = matcher.group("status"); |
|
// `status` may be null, so compare this way around |
|
var installed = "installed".equals(status); |
|
var identifier = matcher.group("identifier"); |
|
candidates.add(new JdkCandidate(dist, version, installed, identifier)); |
|
} else |
|
IO.println("⚠️ Could not parse candidate line: \"" + line + "\""); |
|
} else if (line.startsWith(CANDIDATE_TABLE_START)) |
|
tableStarted = true; |
|
} |
|
} |
|
|
|
waitForProcessAndThrowOnError(process, exit -> "determining JDK candidates via SDKMAN failed with exit code " + exit); |
|
|
|
IO.println("✅ Found " + candidates.size() + " candidates"); |
|
return candidates.stream(); |
|
} |
|
|
|
private static int extractFeatureVersion(String jdkIdentifier) { |
|
Matcher matcher = FEATURE_VERSION.matcher(jdkIdentifier); |
|
if (matcher.matches()) |
|
return Integer.parseInt(matcher.group(1)); |
|
throw new IllegalArgumentException("could not extract feature version from JDK identifier: " + jdkIdentifier); |
|
} |
|
|
|
private static String determineDefaultJdk() throws IOException { |
|
IO.println("☎ Determining current default JDK version..."); |
|
var process = execute("source $HOME/.sdkman/bin/sdkman-init.sh && sdk current java"); |
|
|
|
String defaultVersion = null; |
|
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { |
|
String line; |
|
while ((line = reader.readLine()) != null) { |
|
Matcher matcher = DEFAULT_VERSION.matcher(line); |
|
if (matcher.find()) |
|
defaultVersion = matcher.group(1); |
|
} |
|
} |
|
|
|
if (defaultVersion == null) |
|
throw new RuntimeException("could not determine the current default JDK"); |
|
|
|
IO.println("✅ Identified " + defaultVersion); |
|
return defaultVersion; |
|
} |
|
|
|
private static void installJdk(String jdkIdentifier) throws IOException, InterruptedException { |
|
IO.println("☎ Installing " + jdkIdentifier + "..."); |
|
var process = execute("source $HOME/.sdkman/bin/sdkman-init.sh && sdk install java " + jdkIdentifier); |
|
|
|
// pipe SDKMAN output to System.out |
|
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { |
|
String line; |
|
var inProgress = false; |
|
var progressCount = 0; |
|
while ((line = reader.readLine()) != null) { |
|
if (line.isBlank()) |
|
continue; |
|
if (inProgress) { |
|
var matcher = DOWNLOAD_PROGRESS.matcher(line); |
|
if (matcher.matches()) { |
|
var progress = matcher.group(1); |
|
if (progress.equals("100.0%")) { |
|
inProgress = false; |
|
IO.println(" | 100.0%"); |
|
} else if (progressCount % PROGRESS_MESSAGE_INTERVAL == 0) { |
|
var separator = progressCount == 0 ? " " : " | "; |
|
IO.print(separator + progress); |
|
} |
|
progressCount++; |
|
} |
|
} else { |
|
if (line.equals(IN_PROGRESS_MESSAGE)) { |
|
inProgress = true; |
|
IO.print("👷" + line); |
|
} else |
|
IO.println("👷" + line); |
|
} |
|
} |
|
} |
|
|
|
waitForProcessAndThrowOnError(process, exit -> "installation via SDKMAN failed with exit code " + exit); |
|
} |
|
|
|
private static void createSymlink(String jdkIdentifier, int featureVersion) throws IOException { |
|
if (!Files.exists(SDKMAN_CANDIDATES_DIR.resolve(jdkIdentifier))) |
|
throw new IllegalArgumentException("JDK installation directory not found: " + jdkIdentifier); |
|
Path symlink = SDKMAN_CANDIDATES_DIR.resolve("" + featureVersion); |
|
|
|
Files.deleteIfExists(symlink); |
|
Files.createSymbolicLink(symlink, Path.of(jdkIdentifier)); |
|
IO.println("✅ Created symlink " + symlink + " -> " + jdkIdentifier); |
|
} |
|
|
|
private static void resetDefaultJdk(String jdkIdentifier) throws IOException, InterruptedException { |
|
IO.println("☎ Setting default JDK back to " + jdkIdentifier + "..."); |
|
var process = execute("source $HOME/.sdkman/bin/sdkman-init.sh && sdk default java " + jdkIdentifier); |
|
|
|
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { |
|
String line; |
|
while ((line = reader.readLine()) != null) |
|
if (!line.isBlank()) |
|
IO.println("👷" + line); |
|
} |
|
|
|
waitForProcessAndThrowOnError(process, exit -> "setting the default JDK version via SDKMAN failed with exit code " + exit); |
|
} |
|
|
|
private static Process execute(String command) throws IOException { |
|
ProcessBuilder processBuilder = new ProcessBuilder("bash", "-c", command); |
|
processBuilder.redirectErrorStream(true); |
|
return processBuilder.start(); |
|
} |
|
|
|
private static void waitForProcessAndThrowOnError(Process process, IntFunction<String> errorMessage) throws InterruptedException { |
|
process.waitFor(); |
|
int exitCode = process.exitValue(); |
|
if (exitCode != 0) { |
|
throw new RuntimeException(errorMessage.apply(exitCode)); |
|
} |
|
} |
|
|
|
record JdkCandidate(String distribution, JdkVersion version, boolean installed, String identifier) { } |
|
|
|
record JdkVersion(int feature, int update, int patch, int ea, String qualifier) implements Comparable<JdkVersion> { |
|
|
|
static JdkVersion parse(String version) { |
|
try { |
|
var segments = new ArrayList<>(List.of(version.split("\\."))); |
|
if (segments.isEmpty()) |
|
throw new IllegalArgumentException("empty version string"); |
|
|
|
int feature, update, patch, ea; |
|
|
|
// LAST segment: remove if it's a qualifier (so it doesn't get parsed as a version number later) |
|
String qualifier; |
|
try { |
|
// if this succeeds, the last segment is a number and the version has no qualifier |
|
Integer.parseInt(segments.getLast()); |
|
qualifier = ""; |
|
} catch (NumberFormatException _) { |
|
// this failed, so the last segment is the qualifier |
|
qualifier = segments.removeLast(); |
|
} |
|
if (segments.isEmpty()) |
|
throw new IllegalArgumentException("version string must start with a feature number"); |
|
|
|
// FIRST segment: parse to "feature" |
|
feature = Integer.parseInt(segments.get(0)); |
|
if (segments.size() == 1) |
|
return new JdkVersion(feature, 0, 0, 0, qualifier); |
|
|
|
// SECOND segment: parse to "update" unless it's "ea", in which case we can determine all remaining segments |
|
if (segments.get(1).equals("ea")) { |
|
update = 0; |
|
patch = 0; |
|
if (segments.size() == 2) |
|
throw new IllegalArgumentException("EA version string should consist of at least three segments"); |
|
ea = Integer.parseInt(segments.get(2)); |
|
return new JdkVersion(feature, update, patch, ea, qualifier); |
|
} else { |
|
update = Integer.parseInt(segments.get(1)); |
|
ea = 0; |
|
} |
|
if (segments.size() == 2) |
|
return new JdkVersion(feature, update, 0, ea, qualifier); |
|
|
|
// THIRD segment: parse to "patch" (and lean on the fact that EA versions were already returned) |
|
patch = Integer.parseInt(segments.get(2)); |
|
return new JdkVersion(feature, update, patch, ea, qualifier); |
|
} catch (Exception ex) { |
|
throw new IllegalArgumentException("could not parse '" + version + "' to a version - reason: " + ex.getMessage(), ex); |
|
} |
|
} |
|
|
|
@Override |
|
public int compareTo(JdkVersion o) { |
|
int featureDiff = Integer.compare(feature, o.feature); |
|
if (featureDiff != 0) |
|
return featureDiff; |
|
|
|
int updateDiff = Integer.compare(update, o.update); |
|
if (updateDiff != 0) |
|
return updateDiff; |
|
|
|
int patchDiff = Integer.compare(patch, o.patch); |
|
if (patchDiff != 0) |
|
return patchDiff; |
|
|
|
int eaDiff = Integer.compare(ea, o.ea); |
|
if (eaDiff != 0) |
|
return eaDiff; |
|
|
|
return qualifier.compareTo(o.qualifier); |
|
} |
|
|
|
} |