/*
* Copyright (c) 2022, 2024, Oracle and/or its affiliates.
*
* This source code is licensed under the UPL license found in the
* LICENSE.txt file in the root directory of this source tree.
*/
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.List;
import java.util.StringJoiner;
import java.util.TreeMap;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* List Java Development Kit builds hosted at: {@code https://jdk.java.net}.
*
*
Example output:
*
*
{@code
* 17,17.0.2,linux,x64=https://[...]/GPL/openjdk-17.0.2_linux-x64_bin.tar.gz
* 17,latest,linux,x64=https://[...]/GPL/openjdk-17.0.2_linux-x64_bin.tar.gz
* ga,latest,linux,x64=https://[...]/GPL/openjdk-17.0.2_linux-x64_bin.tar.gz
* }
*
* Keys are composed of {@code RELEASE,VERSION,OS-NAME,OS-ARCH} with:
*
*
* - {@code RELEASE}: Either a release number or a name of an early-access project
*
- {@code VERSION}: Either a specific version or `latest`
*
- {@code OS-NAME}: An operating system name, usually one of: `linux`, `macos`, `windows`
*
- {@code OS-ARCH}: An operating system architecture, like: `aarch64`, `x64`, or `x64-musl`
*
*/
class ListOpenJavaDevelopmentKits {
/** Current General-Availability release number. */
static final String GA = System.getProperty("GA", "22");
/** Current Soon-Archived release number. */
static final String SA = System.getProperty("SA", "21");
/** Early-Access Releases, as comma separated names. */
static final String EA = System.getProperty("EA", "23,jextract,loom,valhalla");
/** Include archived releases flag. */
static final boolean ARCHIVES = Boolean.getBoolean("ARCHIVES");
/** Shared HTTP client instance. */
static final HttpClient HTTP_CLIENT =
HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NORMAL).build();
/** Regex-based pattern used to find a download URI in a HTML code line. */
static final Pattern URI_PATTERN =
Pattern.compile(
".+?href=\""
+ "(\\Qhttps://download.java.net/\\E.+?/openjdk-.+?_bin\\.(tar\\.gz|zip))"
+ "\".+?");
/** Regex-based pattern used to find all named groups of a download key. */
static final Pattern KEY_PATTERN =
Pattern.compile(".+?openjdk-(?.+?)_(?.+?)-(?.+?)_bin\\.(?.+?)");
/** Main entry-point. */
public static void main(String... args) {
if (args.length == 0) {
listGeneralAvailabilityRelease();
listSoonArchivedRelease();
listEarlyAccessReleases();
if (ARCHIVES) listArchivedReleases();
} else {
for (var name : args) {
var page = "https://jdk.java.net/" + name.toLowerCase() + "/";
var html = browse(page);
print(page, parse(html));
}
}
}
static void listGeneralAvailabilityRelease() {
var html = browse("https://jdk.java.net/" + GA + "/");
var directs = parse(html);
print("General-Availability Release", directs);
var aliases = alias(directs, ListOpenJavaDevelopmentKits::generateGeneralAvailabilityAliasKeys);
print("General-Availability Release (Alias)", aliases);
}
static List generateGeneralAvailabilityAliasKeys(String[] components) {
components[1] = "latest";
var alias1 = String.join(",", components);
components[0] = "ga";
var alias2 = String.join(",", components);
return List.of(alias1, alias2);
}
static void listSoonArchivedRelease() {
var html = browse("https://jdk.java.net/" + SA + "/");
var directs = parse(html);
print("Soon-Archived Release", directs);
var aliases = alias(directs, ListOpenJavaDevelopmentKits::generateSoonArchivedAliasKeys);
print("Soon-Archived Release (Alias)", aliases);
}
static List generateSoonArchivedAliasKeys(String[] components) {
components[1] = "latest";
var alias1 = String.join(",", components);
return List.of(alias1);
}
static void listEarlyAccessReleases() {
var names = List.of(EA.split(","));
if (names.isEmpty()) return;
var html =
names.stream()
.map(String::toLowerCase)
.map(name -> browse("https://jdk.java.net/" + name + "/"))
.collect(Collectors.joining());
var directs = parse(html);
print("Early-Access Releases", directs);
var aliases = alias(directs, ListOpenJavaDevelopmentKits::generateEarlyAccessAliasKeys);
print("Early-Access Releases (Alias)", aliases);
}
static List generateEarlyAccessAliasKeys(String[] components) {
var release = components[0];
var version = components[1];
try {
// extract named project or take version as-is
var from = version.indexOf('-');
var till = version.indexOf('+');
var project = from >= 0 && from < till ? version.substring(from + 1, till) : version;
components[0] = project;
components[1] = "latest";
var alias = String.join(",", components);
if (!project.equals("ea")) return List.of(alias);
components[0] = release; // 18-latest-...
return List.of(alias, String.join(",", components));
} catch (IndexOutOfBoundsException exception) {
System.err.println("Early-Access version without `-` and `+`: " + version);
return List.of();
}
}
static void listArchivedReleases() {
var html = browse("https://jdk.java.net/archive/");
var directs = parse(html);
print("Archived Releases", directs);
var aliases = alias(directs, ListOpenJavaDevelopmentKits::generateArchivedAliasKeys);
print("Archived Releases (Alias)", aliases);
}
static List generateArchivedAliasKeys(String[] components) {
if (components[0].equals(GA)) return List.of(); // "latest" is covered by GA
if (components[0].equals(SA)) return List.of(); // "latest" is covered by SA
components[1] = "latest";
var alias = String.join(",", components);
return List.of(alias);
}
static String browse(String uri) {
try {
var request = HttpRequest.newBuilder(URI.create(uri)).build();
return HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString()).body();
} catch (Exception exception) {
return exception.toString();
}
}
static TreeMap parse(String html) {
var map = new TreeMap();
for (var line : html.lines().toList()) {
var uriMatcher = URI_PATTERN.matcher(line);
if (uriMatcher.matches()) {
var uri = uriMatcher.group(1);
var keyMatcher = KEY_PATTERN.matcher(uri);
if (!keyMatcher.matches()) {
System.err.println("// no match -> " + uri);
continue;
}
var version = Runtime.Version.parse(keyMatcher.group("version"));
var os = keyMatcher.group("os");
var joiner =
new StringJoiner(",")
.add(Integer.toString(version.feature()))
.add(version.toString())
.add(os.equals("osx") ? "macos" : os)
.add(keyMatcher.group("arch"));
var key = joiner.toString();
map.put(key, uri);
}
}
return map;
}
static TreeMap alias(
TreeMap map, Function> generator) {
var alternates = new TreeMap();
for (var ga : map.entrySet()) {
var key = ga.getKey();
var uri = ga.getValue();
var elements = key.split(",");
generator.apply(elements).forEach(alias -> alternates.put(alias, uri));
}
return alternates;
}
static void print(String title, TreeMap map) {
System.out.println("#");
System.out.println("# " + title);
System.out.println("#");
map.forEach((key, uri) -> System.out.printf("%s=%s%n", key, uri));
}
}