Skip to content

Instantly share code, notes, and snippets.

@nipafx
Last active July 25, 2025 21:27
Show Gist options
  • Save nipafx/49241062fc21dcc2e50059941ce6a09c to your computer and use it in GitHub Desktop.
Save nipafx/49241062fc21dcc2e50059941ce6a09c to your computer and use it in GitHub Desktop.
A wrapper for SDKMAN that makes it easier to install the latest JDK version and update a symlink to it that allows setting a java version with, e.g., `sdk use java 25`

Setup:

  1. have SDKMAN installed
  2. copy the content of JdkUpdate.java into a file jdk_update
  3. make it executable with chmod +x jdk_update
  4. either add it to the path or execute from within that directory

Features / how-to:

  • a command like jdk_update 24.0.2-open:
    • uses SDKMAN to install the JDK with identifier 24.0.2-open
    • creates/updates a symlink ~/.sdkman/candidates/java/24 ~> 24.0.2-open
    • thus enables sdk use java 24
  • a command like jdk_update 21:
    • uses SDKMAN to install the latest Oracle JDK or Oracle OpenJDK build for version 21, e.g. 21.0.8-oracle
    • creates/updates a symlink ~/.sdkman/candidates/java/21 ~> 21.0.8-oracle
    • thus enables sdk use java 21
  • a command like jdk_update:
    • uses SDKMAN to install the latest Oracle OpenJDK build (including EA builds), e.g. 26.ea.7-open
    • creates/updates a symlink ~/.sdkman/candidates/java/26 ~> 26.ea.7-open
    • thus enables sdk use java 26
#!/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);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment