From ce7966adf60fd3c5aff15ed97fdc574430b14eba Mon Sep 17 00:00:00 2001 From: Tako Schotanus Date: Mon, 9 Feb 2026 09:45:23 +0100 Subject: [PATCH 1/3] fix: fixed issues with caching Caching with the default remote access provider now actually works. --- build.gradle | 302 +++--- .../jdkinstallers/FoojayJdkInstaller.java | 739 ++++++++------- .../jdkinstallers/MetadataJdkInstaller.java | 887 +++++++++--------- .../jdkproviders/SdkmanJdkProvider.java | 100 +- .../devkitman/util/FileHttpCacheStorage.java | 179 ++-- .../devkitman/util/FunctionWithError.java | 15 + .../dev/jbang/devkitman/util/NetUtils.java | 268 +++--- .../devkitman/util/RemoteAccessProvider.java | 136 +-- .../java/dev/jbang/devkitman/BaseTest.java | 497 +++++----- .../jdkinstallers/FoojayJdkInstallerTest.java | 698 +++++++------- .../MetadataJdkInstallerTest.java | 588 ++++++------ .../jdkproviders/SdkmanJdkProviderTest.java | 136 +-- .../util/TestRemoteAccessProvider.java | 35 + 13 files changed, 2344 insertions(+), 2236 deletions(-) create mode 100644 src/main/java/dev/jbang/devkitman/util/FunctionWithError.java create mode 100644 src/test/java/dev/jbang/devkitman/util/TestRemoteAccessProvider.java diff --git a/build.gradle b/build.gradle index 72bb7e5..8ac8968 100644 --- a/build.gradle +++ b/build.gradle @@ -1,151 +1,151 @@ -plugins { - id 'java-library' - id 'io.toolebox.git-versioner' version '1.6.7' - id 'com.diffplug.spotless' version '7.2.1' - id 'maven-publish' - id 'org.jreleaser' version '1.21.0' -} - -group = 'dev.jbang' - -def javaVersion = System.getProperty('java.version') -def majorVersion = javaVersion.split('\\.')[0].toInteger() -if (majorVersion < 11) { - throw new GradleException(""" - ⚠️ This build requires Java 11 or newer but you're using Java ${javaVersion} - Please use JAVA_HOME with Java 11 or newer to run this build. - Current JAVA_HOME: ${System.getProperty('java.home')}. - If you have jbang installed, you can run it with: - eval \$(jbang jdk java-env 11+) - """.stripIndent()) -} - -javadoc { - options.encoding = 'UTF-8' - //remove this to see all the missing tags/parameters. - options.addStringOption('Xdoclint:none', '-quiet') -} - -repositories { - mavenCentral() -} - -java { - withJavadocJar() - withSourcesJar() -} - -compileJava { - options.encoding = 'UTF-8' - options.release = 8; -} - -versioner { - pattern { - pattern = "%M.%m.%p(.%c-SNAPSHOT)" - } - git { - authentication { - https { - token = project.hasProperty('github_token') ? getProperty('github_token') : "unknown_github_token" - } - } - } -} - -dependencies { - implementation 'org.apache.commons:commons-compress:1.28.0' - implementation 'org.apache.httpcomponents:httpclient:4.5.14' - implementation 'org.apache.httpcomponents:httpclient-cache:4.5.14' - implementation 'com.google.code.gson:gson:2.13.2' - implementation 'org.jspecify:jspecify:1.0.0' - - testImplementation platform('org.junit:junit-bom:6.0.1') - testImplementation 'org.junit.jupiter:junit-jupiter' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - testImplementation "org.hamcrest:hamcrest-library:3.0" - testImplementation "uk.org.webcompere:system-stubs-jupiter:2.1.8" - testImplementation "uk.org.webcompere:system-stubs-core:2.1.8" -} - -tasks.withType(AbstractArchiveTask) { - preserveFileTimestamps = false - reproducibleFileOrder = true -} - -spotless { - format 'misc', { - target '**/*.gradle', '**/*.md', '**/.gitignore' - targetExclude 'build/**/*', 'out/**/*' - trimTrailingWhitespace() - leadingSpacesToTabs() - endWithNewline() - } - java { - importOrder 'java', 'javax', 'org', 'com', 'dev.jbang', '' - removeUnusedImports() - eclipse().configFile "misc/eclipse_formatting_nowrap.xml" - targetExclude 'build/**/*' - } - format 'xml', { - targetExclude 'build/test-results', fileTree('.idea') - target '**/*.xml', '**/*.nuspec' - } -} - -test { - useJUnitPlatform() - jvmArgs = [ - "--add-opens", "java.base/java.lang=ALL-UNNAMED", - "--add-opens", "java.base/java.util=ALL-UNNAMED" - ] -} - -publishing { - publications { - maven(MavenPublication) { - groupId = 'dev.jbang' - artifactId = 'devkitman' - - from components.java - - pom { - name = 'JBang JDK Manager' - description = 'Library for managing JDK installations' - url = 'https://github.com/jbangdev/jbang-devkitman' - inceptionYear = '2025' - licenses { - license { - name = 'MIT' - url = 'https://github.com/jbangdev/jbang-devkitman/blob/main/LICENSE' - } - } - developers { - developer { - id = 'maxandersen' - name = 'Max Rydahl Andersen' - } - developer { - id = 'quintesse' - name = 'Tako Schotanus' - } - } - scm { - connection = 'scm:git:https://github.com/jbangdev/jbang-devkitman' - developerConnection = 'scm:git:https://github.com/jbangdev/jbang-devkitman' - url = 'http://github.com/jbangdev/jbang-devkitman' - } - } - } - } - - repositories { - maven { - url = layout.buildDirectory.dir('staging-deploy') - } - } -} - -jreleaser { - configFile = file('jreleaser.yml') -} +plugins { + id 'java-library' + id 'io.toolebox.git-versioner' version '1.6.7' + id 'com.diffplug.spotless' version '7.2.1' + id 'maven-publish' + id 'org.jreleaser' version '1.21.0' +} + +group = 'dev.jbang' + +def javaVersion = System.getProperty('java.version') +def majorVersion = javaVersion.split('\\.')[0].toInteger() +if (majorVersion < 11) { + throw new GradleException(""" + ⚠️ This build requires Java 11 or newer but you're using Java ${javaVersion} + Please use JAVA_HOME with Java 11 or newer to run this build. + Current JAVA_HOME: ${System.getProperty('java.home')}. + If you have jbang installed, you can run it with: + eval \$(jbang jdk java-env 11+) + """.stripIndent()) +} + +javadoc { + options.encoding = 'UTF-8' + //remove this to see all the missing tags/parameters. + options.addStringOption('Xdoclint:none', '-quiet') +} + +repositories { + mavenCentral() +} + +java { + withJavadocJar() + withSourcesJar() +} + +compileJava { + options.encoding = 'UTF-8' + options.release = 8; +} + +versioner { + pattern { + pattern = "%M.%m.%p(.%c-SNAPSHOT)" + } + git { + authentication { + https { + token = project.hasProperty('github_token') ? getProperty('github_token') : "unknown_github_token" + } + } + } +} + +dependencies { + implementation 'org.apache.commons:commons-compress:1.28.0' + implementation 'org.apache.httpcomponents.client5:httpclient5:5.6' + implementation 'org.apache.httpcomponents.client5:httpclient5-cache:5.6' + implementation 'com.google.code.gson:gson:2.13.2' + implementation 'org.jspecify:jspecify:1.0.0' + + testImplementation platform('org.junit:junit-bom:6.0.1') + testImplementation 'org.junit.jupiter:junit-jupiter' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testImplementation "org.hamcrest:hamcrest-library:3.0" + testImplementation "uk.org.webcompere:system-stubs-jupiter:2.1.8" + testImplementation "uk.org.webcompere:system-stubs-core:2.1.8" +} + +tasks.withType(AbstractArchiveTask) { + preserveFileTimestamps = false + reproducibleFileOrder = true +} + +spotless { + format 'misc', { + target '**/*.gradle', '**/*.md', '**/.gitignore' + targetExclude 'build/**/*', 'out/**/*' + trimTrailingWhitespace() + leadingSpacesToTabs() + endWithNewline() + } + java { + importOrder 'java', 'javax', 'org', 'com', 'dev.jbang', '' + removeUnusedImports() + eclipse().configFile "misc/eclipse_formatting_nowrap.xml" + targetExclude 'build/**/*' + } + format 'xml', { + targetExclude 'build/test-results', fileTree('.idea') + target '**/*.xml', '**/*.nuspec' + } +} + +test { + useJUnitPlatform() + jvmArgs = [ + "--add-opens", "java.base/java.lang=ALL-UNNAMED", + "--add-opens", "java.base/java.util=ALL-UNNAMED" + ] +} + +publishing { + publications { + maven(MavenPublication) { + groupId = 'dev.jbang' + artifactId = 'devkitman' + + from components.java + + pom { + name = 'JBang JDK Manager' + description = 'Library for managing JDK installations' + url = 'https://github.com/jbangdev/jbang-devkitman' + inceptionYear = '2025' + licenses { + license { + name = 'MIT' + url = 'https://github.com/jbangdev/jbang-devkitman/blob/main/LICENSE' + } + } + developers { + developer { + id = 'maxandersen' + name = 'Max Rydahl Andersen' + } + developer { + id = 'quintesse' + name = 'Tako Schotanus' + } + } + scm { + connection = 'scm:git:https://github.com/jbangdev/jbang-devkitman' + developerConnection = 'scm:git:https://github.com/jbangdev/jbang-devkitman' + url = 'http://github.com/jbangdev/jbang-devkitman' + } + } + } + } + + repositories { + maven { + url = layout.buildDirectory.dir('staging-deploy') + } + } +} + +jreleaser { + configFile = file('jreleaser.yml') +} diff --git a/src/main/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstaller.java b/src/main/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstaller.java index 0700593..17fb46e 100644 --- a/src/main/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstaller.java +++ b/src/main/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstaller.java @@ -1,372 +1,367 @@ -package dev.jbang.devkitman.jdkinstallers; - -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.nio.file.Path; -import java.util.*; -import java.util.function.Function; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import org.apache.http.impl.client.HttpClientBuilder; -import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; - -import dev.jbang.devkitman.Jdk; -import dev.jbang.devkitman.JdkInstaller; -import dev.jbang.devkitman.JdkInstallers; -import dev.jbang.devkitman.JdkProvider; -import dev.jbang.devkitman.util.*; - -/** - * JVM's main JDK installer that can download and install the JDKs provided by - * the Foojay Disco API. - */ -public class FoojayJdkInstaller implements JdkInstaller { - protected final JdkProvider jdkProvider; - protected final Function jdkId; - protected RemoteAccessProvider remoteAccessProvider = RemoteAccessProvider.createDefaultRemoteAccessProvider(); - protected String distro = DEFAULT_DISTRO; - - public static final String FOOJAY_JDK_VERSIONS_URL = "https://api.foojay.io/disco/v3.0/packages?"; - - public static final String DEFAULT_DISTRO = "temurin,aoj"; - - private static final Logger LOGGER = Logger.getLogger(FoojayJdkInstaller.class.getName()); - - public static class JdkResultLinks { - public String pkg_download_redirect; - } - - public static class JdkResult { - public String java_version; - public int major_version; - public String distribution; // temurin, aoj, liberica, zulu, etc. - public String release_status; // ga, ea - public String package_type; // jdk, jre - public boolean javafx_bundled; - public JdkResultLinks links; - } - - public static class VersionsResponse { - public List result; - } - - public FoojayJdkInstaller(@NonNull JdkProvider jdkProvider) { - this.jdkProvider = jdkProvider; - this.jdkId = jdk -> determineId(jdk) + "-" + jdkProvider.name(); - } - - public FoojayJdkInstaller(@NonNull JdkProvider jdkProvider, @NonNull Function jdkId) { - this.jdkProvider = jdkProvider; - this.jdkId = jdkId; - } - - public @NonNull FoojayJdkInstaller remoteAccessProvider(@NonNull RemoteAccessProvider remoteAccessProvider) { - this.remoteAccessProvider = remoteAccessProvider; - return this; - } - - public @NonNull FoojayJdkInstaller distro(String distro) { - this.distro = distro != null && !distro.isEmpty() ? distro : DEFAULT_DISTRO; - return this; - } - - @NonNull - @Override - public Stream listAvailable() { - try { - VersionsResponse res = readPackagesForList(); - return processPackages(res.result, majorVersionSort()).distinct(); - } catch (IOException e) { - LOGGER.log(Level.FINE, "Couldn't list available JDKs", e); - return Stream.empty(); - } - } - - private VersionsResponse readPackagesForList() throws IOException { - return readJsonFromUrl( - getVersionsUrl(0, true, OsUtils.getOS(), OsUtils.getArch(), distro, "ga,ea")); - } - - @Override - public Jdk.@Nullable AvailableJdk getAvailableByVersion(int version, boolean openVersion) { - int djv = jdkProvider.manager().defaultJavaVersion; - Comparator preferGaSort = (j1, j2) -> { - // Prefer versions equal to the default Java version - if (j1.major_version == djv && j2.major_version != djv) { - return -1; - } else if (j2.major_version == djv && j1.major_version != djv) { - return 1; - } - // Prefer GA releases over EA releases - if (!j1.release_status.equals(j2.release_status)) { - return j2.release_status.compareTo(j1.release_status); - } - // Prefer newer versions - return majorVersionSort().compare(j1, j2); - }; - try { - VersionsResponse res = readPackagesForVersion(version, openVersion); - return processPackages(res.result, preferGaSort) - .filter(Jdk.Predicates.forVersion(version, openVersion)) - .findFirst() - .orElse(null); - } catch (IOException e) { - LOGGER.log(Level.FINE, "Couldn't get available JDK by version", e); - return null; - } - } - - private VersionsResponse readPackagesForVersion(Integer minVersion, boolean openVersion) throws IOException { - VersionsResponse res = readJsonFromUrl( - getVersionsUrl(minVersion, openVersion, OsUtils.getOS(), OsUtils.getArch(), distro, "ga")); - if (res.result.isEmpty()) { - res = readJsonFromUrl( - getVersionsUrl(minVersion, openVersion, OsUtils.getOS(), OsUtils.getArch(), distro, "ea")); - } - return res; - } - - private Stream processPackages(List jdks, Comparator sortFunc) { - return filterEA(jdks) - .stream() - .sorted(sortFunc) - .map(jdk -> new AvailableFoojayJdk(jdkProvider, - jdkId.apply(jdk), jdk.java_version, - jdk.links.pkg_download_redirect, determineTags(jdk))); - } - - private @NonNull String determineId(@NonNull JdkResult jdk) { - String id = jdk.java_version + "-" + jdk.distribution; - if (Jdk.Default.Tags.Jre.name().equals(jdk.package_type)) { - id += "-jre"; - } - if (jdk.javafx_bundled) { - id += "-jfx"; - } - return id; - } - - private @NonNull Set determineTags(JdkResult jdk) { - Set tags = new HashSet<>(); - if (Jdk.Default.Tags.Ga.name().equalsIgnoreCase(jdk.release_status)) { - tags.add(Jdk.Default.Tags.Ga.name()); - } else if (Jdk.Default.Tags.Ea.name().equalsIgnoreCase(jdk.release_status)) { - tags.add(Jdk.Default.Tags.Ea.name()); - } - if (Jdk.Default.Tags.Jdk.name().equalsIgnoreCase(jdk.package_type)) { - tags.add(Jdk.Default.Tags.Jdk.name()); - } else if (Jdk.Default.Tags.Jre.name().equalsIgnoreCase(jdk.package_type)) { - tags.add(Jdk.Default.Tags.Jre.name()); - } - if (jdk.javafx_bundled) { - tags.add(Jdk.Default.Tags.Javafx.name()); - } - return tags; - } - - private VersionsResponse readJsonFromUrl(String url) throws IOException { - return remoteAccessProvider.resultFromUrl(url, is -> { - try (InputStream ignored = is) { - Gson parser = new GsonBuilder().create(); - return parser.fromJson(new InputStreamReader(is), VersionsResponse.class); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - } - - // Filter out any EA releases for which a GA with - // the same major version exists - private List filterEA(List jdks) { - Set GAs = jdks.stream() - .filter(jdk -> jdk.release_status.equals("ga")) - .map(jdk -> jdk.major_version) - .collect(Collectors.toSet()); - - JdkResult[] lastJdk = new JdkResult[] { null }; - return jdks.stream() - .filter( - jdk -> { - if (lastJdk[0] == null - || lastJdk[0].major_version != jdk.major_version - && (jdk.release_status.equals("ga") - || !GAs.contains(jdk.major_version))) { - lastJdk[0] = jdk; - return true; - } else { - return false; - } - }) - .collect(Collectors.toList()); - } - - private static final Comparator jdkResultVersionComparator = (o1, o2) -> VersionComparator.INSTANCE - .compare(o1.java_version, o2.java_version); - - private Comparator majorVersionSort() { - List ds = Arrays.asList(distro.split(",")); - Comparator jdkResultDistroComparator = Comparator.comparingInt(o -> ds.indexOf(o.distribution)); - return Comparator - .comparingInt((JdkResult jdk) -> -jdk.major_version) - .thenComparing(jdkResultDistroComparator) - .thenComparing(jdkResultVersionComparator.reversed()); - } - - @Override - public Jdk.@NonNull InstalledJdk install(Jdk.@NonNull AvailableJdk jdk, @NonNull Path jdkDir) { - if (!(jdk instanceof AvailableFoojayJdk)) { - throw new IllegalArgumentException( - "FoojayJdkInstaller can only install JDKs listed as available by itself"); - } - AvailableFoojayJdk foojayJdk = (AvailableFoojayJdk) jdk; - int version = jdkVersion(foojayJdk.id()); - LOGGER.log( - Level.INFO, - "Downloading JDK {0}. Be patient, this can take several minutes...", - version); - String url = foojayJdk.downloadUrl; - - try { - LOGGER.log(Level.FINE, "Downloading {0}", url); - Path jdkPkg = remoteAccessProvider.downloadFromUrl(url); - - LOGGER.log(Level.INFO, "Installing JDK {0}...", version); - JavaUtils.installJdk(jdkPkg, jdkDir); - - Jdk.InstalledJdk newJdk = jdkProvider.createJdk(foojayJdk.id(), jdkDir); - if (newJdk == null) { - throw new IllegalStateException("Cannot obtain version of recently installed JDK"); - } - return newJdk; - } catch (Exception e) { - String msg = "Required Java version not possible to download or install: " + version; - LOGGER.log(Level.FINE, msg); - throw new IllegalStateException( - "Unable to download or install JDK version " + version, e); - } - } - - @Override - public void uninstall(Jdk.@NonNull InstalledJdk jdk) { - JavaUtils.safeDeleteJdk(jdk.home()); - } - - private static String getVersionsUrl(int minVersion, boolean openVersion, OsUtils.OS os, OsUtils.Arch arch, - String distro, String status) { - return FOOJAY_JDK_VERSIONS_URL + getUrlParams(minVersion, openVersion, os, arch, distro, status); - } - - private static String getUrlParams(int version, boolean openVersion, OsUtils.OS os, OsUtils.Arch arch, - String distro, String status) { - Map params = new HashMap<>(); - if (version > 0) { - String v = String.valueOf(version); - if (openVersion) { - v += "..<999"; - } - params.put("version", v); - } - - if (distro == null) { - if (version == 0 || version == 8 || version == 11 || version >= 17) { - distro = "temurin"; - } else { - distro = "aoj"; - } - } - params.put("distro", distro); - - String archiveType; - if (os == OsUtils.OS.windows) { - archiveType = "zip"; - } else { - archiveType = "tar.gz"; - } - params.put("archive_type", archiveType); - - params.put("architecture", arch.name()); - params.put("package_type", "jdk"); - params.put("operating_system", os.name()); - - if (os == OsUtils.OS.windows) { - params.put("libc_type", "c_std_lib"); - } else if (os == OsUtils.OS.mac) { - params.put("libc_type", "libc"); - } else if (os == OsUtils.OS.alpine_linux) { - params.put("libc_type", "musl"); - } else { - params.put("libc_type", "glibc"); - } - - params.put("javafx_bundled", "false"); - params.put("latest", "available"); - params.put("release_status", status); - params.put("directly_downloadable", "true"); - - return urlEncodeUTF8(params); - } - - static String urlEncodeUTF8(Map map) { - StringBuilder sb = new StringBuilder(); - for (Map.Entry entry : map.entrySet()) { - if (sb.length() > 0) { - sb.append("&"); - } - sb.append( - String.format( - "%s=%s", - urlEncodeUTF8(entry.getKey().toString()), - urlEncodeUTF8(entry.getValue().toString()))); - } - return sb.toString(); - } - - static String urlEncodeUTF8(String s) { - try { - return URLEncoder.encode(s, "UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new IllegalStateException(e); - } - } - - private static int jdkVersion(String jdk) { - return JavaUtils.parseJavaVersion(jdk); - } - - static class AvailableFoojayJdk extends Jdk.AvailableJdk.Default { - public final String downloadUrl; - - AvailableFoojayJdk(@NonNull JdkProvider provider, @NonNull String id, @NonNull String version, - @NonNull String downloadUrl, @NonNull Set tags) { - super(provider, id, version, tags); - this.downloadUrl = downloadUrl; - } - } - - public static class Discovery implements JdkInstallers.Discovery { - @Override - public @NonNull String name() { - return "foojay"; - } - - @Override - public @NonNull JdkInstaller create(Config config) { - FoojayJdkInstaller installer = new FoojayJdkInstaller(config.jdkProvider()); - installer.distro(config.properties().getOrDefault("distro", null)); - HttpClientBuilder httpClientBuilder = NetUtils.createCachingHttpClientBuilder(config.cachePath()); - RemoteAccessProvider rap = RemoteAccessProvider.createDefaultRemoteAccessProvider(httpClientBuilder); - installer.remoteAccessProvider(rap); - return installer; - } - } -} +package dev.jbang.devkitman.jdkinstallers; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.file.Path; +import java.util.*; +import java.util.function.Function; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import dev.jbang.devkitman.Jdk; +import dev.jbang.devkitman.JdkInstaller; +import dev.jbang.devkitman.JdkInstallers; +import dev.jbang.devkitman.JdkProvider; +import dev.jbang.devkitman.util.*; + +/** + * JVM's main JDK installer that can download and install the JDKs provided by + * the Foojay Disco API. + */ +public class FoojayJdkInstaller implements JdkInstaller { + protected final JdkProvider jdkProvider; + protected final Function jdkId; + protected RemoteAccessProvider remoteAccessProvider; + protected String distro = DEFAULT_DISTRO; + + public static final String FOOJAY_JDK_VERSIONS_URL = "https://api.foojay.io/disco/v3.0/packages?"; + + public static final String DEFAULT_DISTRO = "temurin,aoj"; + + private static final Logger LOGGER = Logger.getLogger(FoojayJdkInstaller.class.getName()); + + public static class JdkResultLinks { + public String pkg_download_redirect; + } + + public static class JdkResult { + public String java_version; + public int major_version; + public String distribution; // temurin, aoj, liberica, zulu, etc. + public String release_status; // ga, ea + public String package_type; // jdk, jre + public boolean javafx_bundled; + public JdkResultLinks links; + } + + public static class VersionsResponse { + public List result; + } + + public FoojayJdkInstaller(@NonNull JdkProvider jdkProvider) { + this.jdkProvider = jdkProvider; + this.jdkId = jdk -> determineId(jdk) + "-" + jdkProvider.name(); + } + + public FoojayJdkInstaller(@NonNull JdkProvider jdkProvider, @NonNull Function jdkId) { + this.jdkProvider = jdkProvider; + this.jdkId = jdkId; + } + + protected @NonNull RemoteAccessProvider remoteAccessProvider() { + if (remoteAccessProvider == null) { + remoteAccessProvider = RemoteAccessProvider.createDefaultRemoteAccessProvider(); + } + return remoteAccessProvider; + } + + public @NonNull FoojayJdkInstaller remoteAccessProvider(@NonNull RemoteAccessProvider remoteAccessProvider) { + this.remoteAccessProvider = remoteAccessProvider; + return this; + } + + public @NonNull FoojayJdkInstaller distro(String distro) { + this.distro = distro != null && !distro.isEmpty() ? distro : DEFAULT_DISTRO; + return this; + } + + @NonNull + @Override + public Stream listAvailable() { + try { + VersionsResponse res = readPackagesForList(); + return processPackages(res.result, majorVersionSort()).distinct(); + } catch (IOException e) { + LOGGER.log(Level.FINE, "Couldn't list available JDKs", e); + return Stream.empty(); + } + } + + private VersionsResponse readPackagesForList() throws IOException { + return readJsonFromUrl( + getVersionsUrl(0, true, OsUtils.getOS(), OsUtils.getArch(), distro, "ga,ea")); + } + + @Override + public Jdk.@Nullable AvailableJdk getAvailableByVersion(int version, boolean openVersion) { + int djv = jdkProvider.manager().defaultJavaVersion; + Comparator preferGaSort = (j1, j2) -> { + // Prefer versions equal to the default Java version + if (j1.major_version == djv && j2.major_version != djv) { + return -1; + } else if (j2.major_version == djv && j1.major_version != djv) { + return 1; + } + // Prefer GA releases over EA releases + if (!j1.release_status.equals(j2.release_status)) { + return j2.release_status.compareTo(j1.release_status); + } + // Prefer newer versions + return majorVersionSort().compare(j1, j2); + }; + try { + VersionsResponse res = readPackagesForVersion(version, openVersion); + return processPackages(res.result, preferGaSort) + .filter(Jdk.Predicates.forVersion(version, openVersion)) + .findFirst() + .orElse(null); + } catch (IOException e) { + LOGGER.log(Level.FINE, "Couldn't get available JDK by version", e); + return null; + } + } + + private VersionsResponse readPackagesForVersion(Integer minVersion, boolean openVersion) throws IOException { + VersionsResponse res = readJsonFromUrl( + getVersionsUrl(minVersion, openVersion, OsUtils.getOS(), OsUtils.getArch(), distro, "ga")); + if (res.result.isEmpty()) { + res = readJsonFromUrl( + getVersionsUrl(minVersion, openVersion, OsUtils.getOS(), OsUtils.getArch(), distro, "ea")); + } + return res; + } + + private Stream processPackages(List jdks, Comparator sortFunc) { + return filterEA(jdks) + .stream() + .sorted(sortFunc) + .map(jdk -> new AvailableFoojayJdk(jdkProvider, + jdkId.apply(jdk), jdk.java_version, + jdk.links.pkg_download_redirect, determineTags(jdk))); + } + + private @NonNull String determineId(@NonNull JdkResult jdk) { + String id = jdk.java_version + "-" + jdk.distribution; + if (Jdk.Default.Tags.Jre.name().equals(jdk.package_type)) { + id += "-jre"; + } + if (jdk.javafx_bundled) { + id += "-jfx"; + } + return id; + } + + private @NonNull Set determineTags(JdkResult jdk) { + Set tags = new HashSet<>(); + if (Jdk.Default.Tags.Ga.name().equalsIgnoreCase(jdk.release_status)) { + tags.add(Jdk.Default.Tags.Ga.name()); + } else if (Jdk.Default.Tags.Ea.name().equalsIgnoreCase(jdk.release_status)) { + tags.add(Jdk.Default.Tags.Ea.name()); + } + if (Jdk.Default.Tags.Jdk.name().equalsIgnoreCase(jdk.package_type)) { + tags.add(Jdk.Default.Tags.Jdk.name()); + } else if (Jdk.Default.Tags.Jre.name().equalsIgnoreCase(jdk.package_type)) { + tags.add(Jdk.Default.Tags.Jre.name()); + } + if (jdk.javafx_bundled) { + tags.add(Jdk.Default.Tags.Javafx.name()); + } + return tags; + } + + private VersionsResponse readJsonFromUrl(String url) throws IOException { + return RemoteAccessProvider.readJsonFromUrl(remoteAccessProvider(), url, VersionsResponse.class); + } + + // Filter out any EA releases for which a GA with + // the same major version exists + private List filterEA(List jdks) { + Set GAs = jdks.stream() + .filter(jdk -> jdk.release_status.equals("ga")) + .map(jdk -> jdk.major_version) + .collect(Collectors.toSet()); + + JdkResult[] lastJdk = new JdkResult[] { null }; + return jdks.stream() + .filter( + jdk -> { + if (lastJdk[0] == null + || lastJdk[0].major_version != jdk.major_version + && (jdk.release_status.equals("ga") + || !GAs.contains(jdk.major_version))) { + lastJdk[0] = jdk; + return true; + } else { + return false; + } + }) + .collect(Collectors.toList()); + } + + private static final Comparator jdkResultVersionComparator = (o1, o2) -> VersionComparator.INSTANCE + .compare(o1.java_version, o2.java_version); + + private Comparator majorVersionSort() { + List ds = Arrays.asList(distro.split(",")); + Comparator jdkResultDistroComparator = Comparator.comparingInt(o -> ds.indexOf(o.distribution)); + return Comparator + .comparingInt((JdkResult jdk) -> -jdk.major_version) + .thenComparing(jdkResultDistroComparator) + .thenComparing(jdkResultVersionComparator.reversed()); + } + + @Override + public Jdk.@NonNull InstalledJdk install(Jdk.@NonNull AvailableJdk jdk, @NonNull Path jdkDir) { + if (!(jdk instanceof AvailableFoojayJdk)) { + throw new IllegalArgumentException( + "FoojayJdkInstaller can only install JDKs listed as available by itself"); + } + AvailableFoojayJdk foojayJdk = (AvailableFoojayJdk) jdk; + int version = jdkVersion(foojayJdk.id()); + LOGGER.log( + Level.INFO, + "Downloading JDK {0}. Be patient, this can take several minutes...", + version); + String url = foojayJdk.downloadUrl; + + try { + LOGGER.log(Level.FINE, "Downloading {0}", url); + Path jdkPkg = remoteAccessProvider().downloadFromUrl(url); + + LOGGER.log(Level.INFO, "Installing JDK {0}...", version); + JavaUtils.installJdk(jdkPkg, jdkDir); + + Jdk.InstalledJdk newJdk = jdkProvider.createJdk(foojayJdk.id(), jdkDir); + if (newJdk == null) { + throw new IllegalStateException("Cannot obtain version of recently installed JDK"); + } + return newJdk; + } catch (Exception e) { + String msg = "Required Java version not possible to download or install: " + version; + LOGGER.log(Level.FINE, msg); + throw new IllegalStateException( + "Unable to download or install JDK version " + version, e); + } + } + + @Override + public void uninstall(Jdk.@NonNull InstalledJdk jdk) { + JavaUtils.safeDeleteJdk(jdk.home()); + } + + private static String getVersionsUrl(int minVersion, boolean openVersion, OsUtils.OS os, OsUtils.Arch arch, + String distro, String status) { + return FOOJAY_JDK_VERSIONS_URL + getUrlParams(minVersion, openVersion, os, arch, distro, status); + } + + private static String getUrlParams(int version, boolean openVersion, OsUtils.OS os, OsUtils.Arch arch, + String distro, String status) { + Map params = new HashMap<>(); + if (version > 0) { + String v = String.valueOf(version); + if (openVersion) { + v += "..<999"; + } + params.put("version", v); + } + + if (distro == null) { + if (version == 0 || version == 8 || version == 11 || version >= 17) { + distro = "temurin"; + } else { + distro = "aoj"; + } + } + params.put("distro", distro); + + String archiveType; + if (os == OsUtils.OS.windows) { + archiveType = "zip"; + } else { + archiveType = "tar.gz"; + } + params.put("archive_type", archiveType); + + params.put("architecture", arch.name()); + params.put("package_type", "jdk"); + params.put("operating_system", os.name()); + + if (os == OsUtils.OS.windows) { + params.put("libc_type", "c_std_lib"); + } else if (os == OsUtils.OS.mac) { + params.put("libc_type", "libc"); + } else if (os == OsUtils.OS.alpine_linux) { + params.put("libc_type", "musl"); + } else { + params.put("libc_type", "glibc"); + } + + params.put("javafx_bundled", "false"); + params.put("latest", "available"); + params.put("release_status", status); + params.put("directly_downloadable", "true"); + + return urlEncodeUTF8(params); + } + + static String urlEncodeUTF8(Map map) { + StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : map.entrySet()) { + if (sb.length() > 0) { + sb.append("&"); + } + sb.append( + String.format( + "%s=%s", + urlEncodeUTF8(entry.getKey().toString()), + urlEncodeUTF8(entry.getValue().toString()))); + } + return sb.toString(); + } + + static String urlEncodeUTF8(String s) { + try { + return URLEncoder.encode(s, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException(e); + } + } + + private static int jdkVersion(String jdk) { + return JavaUtils.parseJavaVersion(jdk); + } + + static class AvailableFoojayJdk extends Jdk.AvailableJdk.Default { + public final String downloadUrl; + + AvailableFoojayJdk(@NonNull JdkProvider provider, @NonNull String id, @NonNull String version, + @NonNull String downloadUrl, @NonNull Set tags) { + super(provider, id, version, tags); + this.downloadUrl = downloadUrl; + } + } + + public static class Discovery implements JdkInstallers.Discovery { + @Override + public @NonNull String name() { + return "foojay"; + } + + @Override + public @NonNull JdkInstaller create(Config config) { + FoojayJdkInstaller installer = new FoojayJdkInstaller(config.jdkProvider()); + installer.distro(config.properties().getOrDefault("distro", null)); + HttpClientBuilder httpClientBuilder = NetUtils.createCachingHttpClientBuilder(config.cachePath()); + RemoteAccessProvider rap = RemoteAccessProvider.createDefaultRemoteAccessProvider(httpClientBuilder); + installer.remoteAccessProvider(rap); + return installer; + } + } +} diff --git a/src/main/java/dev/jbang/devkitman/jdkinstallers/MetadataJdkInstaller.java b/src/main/java/dev/jbang/devkitman/jdkinstallers/MetadataJdkInstaller.java index 69ef656..404e864 100644 --- a/src/main/java/dev/jbang/devkitman/jdkinstallers/MetadataJdkInstaller.java +++ b/src/main/java/dev/jbang/devkitman/jdkinstallers/MetadataJdkInstaller.java @@ -1,447 +1,440 @@ -package dev.jbang.devkitman.jdkinstallers; - -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.net.URI; -import java.nio.file.Path; -import java.util.*; -import java.util.function.Function; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import org.apache.http.impl.client.HttpClientBuilder; -import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; - -import dev.jbang.devkitman.Jdk; -import dev.jbang.devkitman.JdkInstaller; -import dev.jbang.devkitman.JdkInstallers; -import dev.jbang.devkitman.JdkProvider; -import dev.jbang.devkitman.util.*; - -/** - * JDK installer that downloads and installs JDKs using the Java Metadata API - * from https://joschi.github.io/java-metadata/ - */ -public class MetadataJdkInstaller implements JdkInstaller { - protected final @NonNull JdkProvider jdkProvider; - protected final Function jdkId; - protected @NonNull RemoteAccessProvider remoteAccessProvider = RemoteAccessProvider - .createDefaultRemoteAccessProvider(); - protected @NonNull String distro = DEFAULT_DISTRO; - protected String jvmImpl = DEFAULT_JVM_IMPL; - - public static final String METADATA_BASE_URL = "https://joschi.github.io/java-metadata/metadata/"; - public static final String DEFAULT_DISTRO = "temurin,adoptopenjdk"; - public static final String DEFAULT_JVM_IMPL = "hotspot"; - - private static final Logger LOGGER = Logger.getLogger(MetadataJdkInstaller.class.getName()); - - /** - * Represents a single metadata entry from the Java Metadata API - */ - public static class MetadataResult { - public String vendor; - public String filename; - public String file_type; - public String release_type; // "ga" or "ea" - public String version; - public String java_version; - public String jvm_impl; // "hotspot", "openj9", "graalvm" - public String os; // "linux", "macosx", "windows", "solaris", "aix" - public String architecture; // "x86_64", "i686", "aarch64", etc. - public String image_type; // "jdk" or "jre" - public List features; - public String url; - public String md5; - public String md5_file; - public String sha1; - public String sha1_file; - public String sha256; - public String sha256_file; - public String sha512; - public String sha512_file; - public Integer size; - } - - public MetadataJdkInstaller(@NonNull JdkProvider jdkProvider) { - this.jdkProvider = jdkProvider; - this.jdkId = jdk -> determineId(jdk) + "-" + jdkProvider.name(); - } - - public MetadataJdkInstaller(@NonNull JdkProvider jdkProvider, @NonNull Function jdkId) { - this.jdkProvider = jdkProvider; - this.jdkId = jdkId; - } - - public @NonNull MetadataJdkInstaller remoteAccessProvider(@NonNull RemoteAccessProvider remoteAccessProvider) { - this.remoteAccessProvider = remoteAccessProvider; - return this; - } - - public @NonNull MetadataJdkInstaller distro(@Nullable String distro) { - this.distro = distro != null && !distro.isEmpty() ? distro : DEFAULT_DISTRO; - return this; - } - - public @NonNull MetadataJdkInstaller jvmImpl(@Nullable String jvmImpl) { - this.jvmImpl = jvmImpl; - return this; - } - - @NonNull - @Override - public Stream listAvailable() { - try { - List results = readMetadataForList(); - return processMetadata(results, majorVersionSort()).distinct(); - } catch (IOException e) { - LOGGER.log(Level.FINE, "Couldn't list available JDKs", e); - return Stream.empty(); - } - } - - private List readMetadataForList() throws IOException { - List allResults = new ArrayList<>(); - IOException lastException = null; - - String[] distros = distro.split(","); - // Query for GA releases first - for (String d : distros) { - try { - List results = readJsonFromUrl( - getMetadataUrl("ga", OsUtils.getOS(), OsUtils.getArch(), "jdk", jvmImpl, d.trim())); - allResults.addAll(results); - } catch (IOException e) { - lastException = e; - } - } - // And for EA releases second - for (String d : distros) { - try { - List results = readJsonFromUrl( - getMetadataUrl("ea", OsUtils.getOS(), OsUtils.getArch(), "jdk", jvmImpl, d.trim())); - allResults.addAll(results); - } catch (IOException e) { - lastException = e; - } - } - - // If we have no results at all and had at least one exception, throw the last - // one - if (allResults.isEmpty() && lastException != null) { - throw lastException; - } - - return allResults; - } - - @Override - public Jdk.@Nullable AvailableJdk getAvailableByVersion(int version, boolean openVersion) { - int djv = jdkProvider.manager().defaultJavaVersion; - Comparator preferGaSort = (j1, j2) -> { - int v1 = extractMajorVersion(j1.java_version); - int v2 = extractMajorVersion(j2.java_version); - - // Prefer versions equal to the default Java version - if (v1 == djv && v2 != djv) { - return -1; - } else if (v2 == djv && v1 != djv) { - return 1; - } - // Prefer GA releases over EA releases - if (!j1.release_type.equals(j2.release_type)) { - return j2.release_type.compareTo(j1.release_type); - } - // Prefer newer versions - return majorVersionSort().compare(j1, j2); - }; - - try { - List results = readMetadataForVersion(version, openVersion); - return processMetadata(results, preferGaSort) - .filter(Jdk.Predicates.forVersion(version, openVersion)) - .findFirst() - .orElse(null); - } catch (IOException e) { - LOGGER.log(Level.FINE, "Couldn't get available JDK by version", e); - return null; - } - } - - private List readMetadataForVersion(int version, boolean openVersion) throws IOException { - String[] distros = distro.split(","); - // Try GA first for all selected distros, return the first that has results - for (String d : distros) { - List results = readMetadataForVersionAndDistro(version, openVersion, "ga", d.trim()); - if (!results.isEmpty()) { - return results; - } - } - // Try EA if no GA found - for (String d : distros) { - List results = readMetadataForVersionAndDistro(version, openVersion, "ea", d.trim()); - if (!results.isEmpty()) { - return results; - } - } - return Collections.emptyList(); - } - - private List readMetadataForVersionAndDistro(int version, boolean openVersion, String releaseType, - String distro) throws IOException { - List gaResults = readJsonFromUrl( - getMetadataUrl(releaseType, OsUtils.getOS(), OsUtils.getArch(), "jdk", jvmImpl, distro)); - return filterByVersion(gaResults, version, openVersion); - } - - private List filterByVersion(List results, int version, boolean openVersion) { - return results.stream() - .filter(r -> { - int majorVersion = extractMajorVersion(r.java_version); - if (openVersion) { - return majorVersion >= version; - } else { - return majorVersion == version; - } - }) - .collect(Collectors.toList()); - } - - private Stream processMetadata(List jdks, Comparator sortFunc) { - return filterEA(jdks) - .stream() - .sorted(sortFunc) - .map(jdk -> new AvailableMetadataJdk(jdkProvider, - jdkId.apply(jdk), jdk.java_version, - jdk.url, determineTags(jdk))); - } - - private @NonNull String determineId(@NonNull MetadataResult jdk) { - String id = jdk.java_version + "-" + jdk.vendor; - if ("jre".equals(jdk.image_type)) { - id += "-jre"; - } - if (jdk.features != null && jdk.features.contains("javafx")) { - id += "-jfx"; - } - if (!"hotspot".equals(jdk.jvm_impl)) { - id += "-" + jdk.jvm_impl; - } - return id; - } - - private @NonNull Set determineTags(MetadataResult jdk) { - Set tags = new HashSet<>(); - if ("ga".equalsIgnoreCase(jdk.release_type)) { - tags.add(Jdk.Default.Tags.Ga.name()); - } else if ("ea".equalsIgnoreCase(jdk.release_type)) { - tags.add(Jdk.Default.Tags.Ea.name()); - } - if ("jdk".equalsIgnoreCase(jdk.image_type)) { - tags.add(Jdk.Default.Tags.Jdk.name()); - } else if ("jre".equalsIgnoreCase(jdk.image_type)) { - tags.add(Jdk.Default.Tags.Jre.name()); - } - if (jdk.features != null && jdk.features.contains("javafx")) { - tags.add(Jdk.Default.Tags.Javafx.name()); - } - return tags; - } - - private List readJsonFromUrl(String url) throws IOException { - return remoteAccessProvider.resultFromUrl(url, is -> { - try (InputStream ignored = is) { - Gson parser = new GsonBuilder().create(); - MetadataResult[] results = parser.fromJson(new InputStreamReader(is), MetadataResult[].class); - return Arrays.asList(results); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - } - - // Filter out any EA releases for which a GA with the same major version exists - private List filterEA(List jdks) { - Set GAs = jdks.stream() - .filter(jdk -> "ga".equals(jdk.release_type)) - .map(jdk -> extractMajorVersion(jdk.java_version)) - .collect(Collectors.toSet()); - - MetadataResult[] lastJdk = new MetadataResult[] { null }; - return jdks.stream() - .filter(jdk -> { - int majorVersion = extractMajorVersion(jdk.java_version); - if (lastJdk[0] == null - || extractMajorVersion(lastJdk[0].java_version) != majorVersion - && ("ga".equals(jdk.release_type) - || !GAs.contains(majorVersion))) { - lastJdk[0] = jdk; - return true; - } else { - return false; - } - }) - .collect(Collectors.toList()); - } - - private static final Comparator metadataResultVersionComparator = (o1, - o2) -> VersionComparator.INSTANCE.compare(o1.java_version, o2.java_version); - - private Comparator majorVersionSort() { - return Comparator - .comparingInt((MetadataResult jdk) -> -extractMajorVersion(jdk.java_version)) - .thenComparing(metadataResultVersionComparator.reversed()); - } - - @Override - public Jdk.@NonNull InstalledJdk install(Jdk.@NonNull AvailableJdk jdk, @NonNull Path jdkDir) { - if (!(jdk instanceof AvailableMetadataJdk)) { - throw new IllegalArgumentException( - "MetadataJdkInstaller can only install JDKs listed as available by itself"); - } - AvailableMetadataJdk metadataJdk = (AvailableMetadataJdk) jdk; - int version = jdkVersion(metadataJdk.id()); - LOGGER.log( - Level.INFO, - "Downloading JDK {0}. Be patient, this can take several minutes...", - version); - String url = metadataJdk.downloadUrl; - - try { - LOGGER.log(Level.FINE, "Downloading {0}", url); - Path jdkPkg = remoteAccessProvider.downloadFromUrl(url); - - LOGGER.log(Level.INFO, "Installing JDK {0}...", version); - JavaUtils.installJdk(jdkPkg, jdkDir); - - Jdk.InstalledJdk newJdk = jdkProvider.createJdk(metadataJdk.id(), jdkDir); - if (newJdk == null) { - throw new IllegalStateException("Cannot obtain version of recently installed JDK"); - } - return newJdk; - } catch (Exception e) { - String msg = "Required Java version not possible to download or install: " + version; - LOGGER.log(Level.FINE, msg); - throw new IllegalStateException( - "Unable to download or install JDK version " + version, e); - } - } - - @Override - public void uninstall(Jdk.@NonNull InstalledJdk jdk) { - JavaUtils.safeDeleteJdk(jdk.home()); - } - - /** - * Constructs the metadata API URL for the given parameters Format: - * /metadata/{release_type}/{os}/{arch}/{image_type}/{jvm_impl}/{vendor}.json - */ - private static String getMetadataUrl(String releaseType, OsUtils.OS os, OsUtils.Arch arch, - String imageType, String jvmImpl, String vendor) { - String osName = mapOsToMetadataName(os); - String archName = mapArchToMetadataName(arch); - if (jvmImpl == null || jvmImpl.isEmpty()) { - if (vendor.contains("graalvm") || vendor.equals("mandrel")) { - jvmImpl = "graalvm"; - } else { - jvmImpl = DEFAULT_JVM_IMPL; - } - } - URI uri = URI.create(METADATA_BASE_URL + releaseType + "/" + osName + "/" + archName + "/" - + imageType + "/" + jvmImpl + "/" + vendor + ".json"); - return uri.toString(); - } - - /** - * Maps OsUtils.OS enum to the metadata API os name - */ - private static String mapOsToMetadataName(OsUtils.OS os) { - switch (os) { - case linux: - case alpine_linux: - return "linux"; - case mac: - return "macosx"; - case windows: - return "windows"; - case aix: - return "aix"; - default: - return "linux"; - } - } - - /** - * Maps OsUtils.Arch enum to the metadata API architecture name - */ - private static String mapArchToMetadataName(OsUtils.Arch arch) { - switch (arch) { - case x64: - return "x86_64"; - case x32: - return "i686"; - case aarch64: - case arm64: - return "aarch64"; - case arm: - return "arm32"; - case ppc64: - return "ppc64"; - case ppc64le: - return "ppc64le"; - case s390x: - return "s390x"; - case riscv64: - return "riscv64"; - default: - return "x86_64"; - } - } - - /** - * Extracts the major version from a Java version string - */ - private static int extractMajorVersion(String javaVersion) { - return JavaUtils.parseJavaVersion(javaVersion); - } - - private static int jdkVersion(String jdk) { - return JavaUtils.parseJavaVersion(jdk); - } - - static class AvailableMetadataJdk extends Jdk.AvailableJdk.Default { - public final String downloadUrl; - - AvailableMetadataJdk(@NonNull JdkProvider provider, @NonNull String id, @NonNull String version, - @NonNull String downloadUrl, @NonNull Set tags) { - super(provider, id, version, tags); - this.downloadUrl = downloadUrl; - } - } - - public static class Discovery implements JdkInstallers.Discovery { - @Override - public @NonNull String name() { - return "metadata"; - } - - @Override - public @NonNull JdkInstaller create(Config config) { - MetadataJdkInstaller installer = new MetadataJdkInstaller(config.jdkProvider()); - installer - .distro(config.properties().getOrDefault("distro", null)) - .jvmImpl(config.properties().getOrDefault("impl", null)); - HttpClientBuilder httpClientBuilder = NetUtils.createCachingHttpClientBuilder(config.cachePath()); - RemoteAccessProvider rap = RemoteAccessProvider.createDefaultRemoteAccessProvider(httpClientBuilder); - installer.remoteAccessProvider(rap); - return installer; - } - } -} +package dev.jbang.devkitman.jdkinstallers; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.Path; +import java.util.*; +import java.util.function.Function; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import dev.jbang.devkitman.Jdk; +import dev.jbang.devkitman.JdkInstaller; +import dev.jbang.devkitman.JdkInstallers; +import dev.jbang.devkitman.JdkProvider; +import dev.jbang.devkitman.util.*; + +/** + * JDK installer that downloads and installs JDKs using the Java Metadata API + * from https://joschi.github.io/java-metadata/ + */ +public class MetadataJdkInstaller implements JdkInstaller { + protected final @NonNull JdkProvider jdkProvider; + protected final Function jdkId; + protected RemoteAccessProvider remoteAccessProvider; + protected @NonNull String distro = DEFAULT_DISTRO; + protected String jvmImpl = DEFAULT_JVM_IMPL; + + public static final String METADATA_BASE_URL = "https://joschi.github.io/java-metadata/metadata/"; + public static final String DEFAULT_DISTRO = "temurin,adoptopenjdk"; + public static final String DEFAULT_JVM_IMPL = "hotspot"; + + private static final Logger LOGGER = Logger.getLogger(MetadataJdkInstaller.class.getName()); + + /** + * Represents a single metadata entry from the Java Metadata API + */ + public static class MetadataResult { + public String vendor; + public String filename; + public String file_type; + public String release_type; // "ga" or "ea" + public String version; + public String java_version; + public String jvm_impl; // "hotspot", "openj9", "graalvm" + public String os; // "linux", "macosx", "windows", "solaris", "aix" + public String architecture; // "x86_64", "i686", "aarch64", etc. + public String image_type; // "jdk" or "jre" + public List features; + public String url; + public String md5; + public String md5_file; + public String sha1; + public String sha1_file; + public String sha256; + public String sha256_file; + public String sha512; + public String sha512_file; + public Integer size; + } + + public MetadataJdkInstaller(@NonNull JdkProvider jdkProvider) { + this.jdkProvider = jdkProvider; + this.jdkId = jdk -> determineId(jdk) + "-" + jdkProvider.name(); + } + + public MetadataJdkInstaller(@NonNull JdkProvider jdkProvider, @NonNull Function jdkId) { + this.jdkProvider = jdkProvider; + this.jdkId = jdkId; + } + + protected @NonNull RemoteAccessProvider remoteAccessProvider() { + if (remoteAccessProvider == null) { + remoteAccessProvider = RemoteAccessProvider.createDefaultRemoteAccessProvider(); + } + return remoteAccessProvider; + } + + public @NonNull MetadataJdkInstaller remoteAccessProvider(@NonNull RemoteAccessProvider remoteAccessProvider) { + this.remoteAccessProvider = remoteAccessProvider; + return this; + } + + public @NonNull MetadataJdkInstaller distro(@Nullable String distro) { + this.distro = distro != null && !distro.isEmpty() ? distro : DEFAULT_DISTRO; + return this; + } + + public @NonNull MetadataJdkInstaller jvmImpl(@Nullable String jvmImpl) { + this.jvmImpl = jvmImpl; + return this; + } + + @NonNull + @Override + public Stream listAvailable() { + try { + List results = readMetadataForList(); + return processMetadata(results, majorVersionSort()).distinct(); + } catch (IOException e) { + LOGGER.log(Level.FINE, "Couldn't list available JDKs", e); + return Stream.empty(); + } + } + + private List readMetadataForList() throws IOException { + List allResults = new ArrayList<>(); + IOException lastException = null; + + String[] distros = distro.split(","); + // Query for GA releases first + for (String d : distros) { + try { + List results = readJsonFromUrl( + getMetadataUrl("ga", OsUtils.getOS(), OsUtils.getArch(), "jdk", jvmImpl, d.trim())); + allResults.addAll(results); + } catch (IOException e) { + lastException = e; + } + } + // And for EA releases second + for (String d : distros) { + try { + List results = readJsonFromUrl( + getMetadataUrl("ea", OsUtils.getOS(), OsUtils.getArch(), "jdk", jvmImpl, d.trim())); + allResults.addAll(results); + } catch (IOException e) { + lastException = e; + } + } + + // If we have no results at all and had at least one exception, throw the last + // one + if (allResults.isEmpty() && lastException != null) { + throw lastException; + } + + return allResults; + } + + @Override + public Jdk.@Nullable AvailableJdk getAvailableByVersion(int version, boolean openVersion) { + int djv = jdkProvider.manager().defaultJavaVersion; + Comparator preferGaSort = (j1, j2) -> { + int v1 = extractMajorVersion(j1.java_version); + int v2 = extractMajorVersion(j2.java_version); + + // Prefer versions equal to the default Java version + if (v1 == djv && v2 != djv) { + return -1; + } else if (v2 == djv && v1 != djv) { + return 1; + } + // Prefer GA releases over EA releases + if (!j1.release_type.equals(j2.release_type)) { + return j2.release_type.compareTo(j1.release_type); + } + // Prefer newer versions + return majorVersionSort().compare(j1, j2); + }; + + try { + List results = readMetadataForVersion(version, openVersion); + return processMetadata(results, preferGaSort) + .filter(Jdk.Predicates.forVersion(version, openVersion)) + .findFirst() + .orElse(null); + } catch (IOException e) { + LOGGER.log(Level.FINE, "Couldn't get available JDK by version", e); + return null; + } + } + + private List readMetadataForVersion(int version, boolean openVersion) throws IOException { + String[] distros = distro.split(","); + // Try GA first for all selected distros, return the first that has results + for (String d : distros) { + List results = readMetadataForVersionAndDistro(version, openVersion, "ga", d.trim()); + if (!results.isEmpty()) { + return results; + } + } + // Try EA if no GA found + for (String d : distros) { + List results = readMetadataForVersionAndDistro(version, openVersion, "ea", d.trim()); + if (!results.isEmpty()) { + return results; + } + } + return Collections.emptyList(); + } + + private List readMetadataForVersionAndDistro(int version, boolean openVersion, String releaseType, + String distro) throws IOException { + List gaResults = readJsonFromUrl( + getMetadataUrl(releaseType, OsUtils.getOS(), OsUtils.getArch(), "jdk", jvmImpl, distro)); + return filterByVersion(gaResults, version, openVersion); + } + + private List filterByVersion(List results, int version, boolean openVersion) { + return results.stream() + .filter(r -> { + int majorVersion = extractMajorVersion(r.java_version); + if (openVersion) { + return majorVersion >= version; + } else { + return majorVersion == version; + } + }) + .collect(Collectors.toList()); + } + + private Stream processMetadata(List jdks, Comparator sortFunc) { + return filterEA(jdks) + .stream() + .sorted(sortFunc) + .map(jdk -> new AvailableMetadataJdk(jdkProvider, + jdkId.apply(jdk), jdk.java_version, + jdk.url, determineTags(jdk))); + } + + private @NonNull String determineId(@NonNull MetadataResult jdk) { + String id = jdk.java_version + "-" + jdk.vendor; + if ("jre".equals(jdk.image_type)) { + id += "-jre"; + } + if (jdk.features != null && jdk.features.contains("javafx")) { + id += "-jfx"; + } + if (!"hotspot".equals(jdk.jvm_impl)) { + id += "-" + jdk.jvm_impl; + } + return id; + } + + private @NonNull Set determineTags(MetadataResult jdk) { + Set tags = new HashSet<>(); + if ("ga".equalsIgnoreCase(jdk.release_type)) { + tags.add(Jdk.Default.Tags.Ga.name()); + } else if ("ea".equalsIgnoreCase(jdk.release_type)) { + tags.add(Jdk.Default.Tags.Ea.name()); + } + if ("jdk".equalsIgnoreCase(jdk.image_type)) { + tags.add(Jdk.Default.Tags.Jdk.name()); + } else if ("jre".equalsIgnoreCase(jdk.image_type)) { + tags.add(Jdk.Default.Tags.Jre.name()); + } + if (jdk.features != null && jdk.features.contains("javafx")) { + tags.add(Jdk.Default.Tags.Javafx.name()); + } + return tags; + } + + private List readJsonFromUrl(String url) throws IOException { + return Arrays.asList(RemoteAccessProvider.readJsonFromUrl(remoteAccessProvider(), url, MetadataResult[].class)); + } + + // Filter out any EA releases for which a GA with the same major version exists + private List filterEA(List jdks) { + Set GAs = jdks.stream() + .filter(jdk -> "ga".equals(jdk.release_type)) + .map(jdk -> extractMajorVersion(jdk.java_version)) + .collect(Collectors.toSet()); + + MetadataResult[] lastJdk = new MetadataResult[] { null }; + return jdks.stream() + .filter(jdk -> { + int majorVersion = extractMajorVersion(jdk.java_version); + if (lastJdk[0] == null + || extractMajorVersion(lastJdk[0].java_version) != majorVersion + && ("ga".equals(jdk.release_type) + || !GAs.contains(majorVersion))) { + lastJdk[0] = jdk; + return true; + } else { + return false; + } + }) + .collect(Collectors.toList()); + } + + private static final Comparator metadataResultVersionComparator = (o1, + o2) -> VersionComparator.INSTANCE.compare(o1.java_version, o2.java_version); + + private Comparator majorVersionSort() { + return Comparator + .comparingInt((MetadataResult jdk) -> -extractMajorVersion(jdk.java_version)) + .thenComparing(metadataResultVersionComparator.reversed()); + } + + @Override + public Jdk.@NonNull InstalledJdk install(Jdk.@NonNull AvailableJdk jdk, @NonNull Path jdkDir) { + if (!(jdk instanceof AvailableMetadataJdk)) { + throw new IllegalArgumentException( + "MetadataJdkInstaller can only install JDKs listed as available by itself"); + } + AvailableMetadataJdk metadataJdk = (AvailableMetadataJdk) jdk; + int version = jdkVersion(metadataJdk.id()); + LOGGER.log( + Level.INFO, + "Downloading JDK {0}. Be patient, this can take several minutes...", + version); + String url = metadataJdk.downloadUrl; + + try { + LOGGER.log(Level.FINE, "Downloading {0}", url); + Path jdkPkg = remoteAccessProvider().downloadFromUrl(url); + + LOGGER.log(Level.INFO, "Installing JDK {0}...", version); + JavaUtils.installJdk(jdkPkg, jdkDir); + + Jdk.InstalledJdk newJdk = jdkProvider.createJdk(metadataJdk.id(), jdkDir); + if (newJdk == null) { + throw new IllegalStateException("Cannot obtain version of recently installed JDK"); + } + return newJdk; + } catch (Exception e) { + String msg = "Required Java version not possible to download or install: " + version; + LOGGER.log(Level.FINE, msg); + throw new IllegalStateException( + "Unable to download or install JDK version " + version, e); + } + } + + @Override + public void uninstall(Jdk.@NonNull InstalledJdk jdk) { + JavaUtils.safeDeleteJdk(jdk.home()); + } + + /** + * Constructs the metadata API URL for the given parameters Format: + * /metadata/{release_type}/{os}/{arch}/{image_type}/{jvm_impl}/{vendor}.json + */ + private static String getMetadataUrl(String releaseType, OsUtils.OS os, OsUtils.Arch arch, + String imageType, String jvmImpl, String vendor) { + String osName = mapOsToMetadataName(os); + String archName = mapArchToMetadataName(arch); + if (jvmImpl == null || jvmImpl.isEmpty()) { + if (vendor.contains("graalvm") || vendor.equals("mandrel")) { + jvmImpl = "graalvm"; + } else { + jvmImpl = DEFAULT_JVM_IMPL; + } + } + URI uri = URI.create(METADATA_BASE_URL + releaseType + "/" + osName + "/" + archName + "/" + + imageType + "/" + jvmImpl + "/" + vendor + ".json"); + return uri.toString(); + } + + /** + * Maps OsUtils.OS enum to the metadata API os name + */ + private static String mapOsToMetadataName(OsUtils.OS os) { + switch (os) { + case linux: + case alpine_linux: + return "linux"; + case mac: + return "macosx"; + case windows: + return "windows"; + case aix: + return "aix"; + default: + return "linux"; + } + } + + /** + * Maps OsUtils.Arch enum to the metadata API architecture name + */ + private static String mapArchToMetadataName(OsUtils.Arch arch) { + switch (arch) { + case x64: + return "x86_64"; + case x32: + return "i686"; + case aarch64: + case arm64: + return "aarch64"; + case arm: + return "arm32"; + case ppc64: + return "ppc64"; + case ppc64le: + return "ppc64le"; + case s390x: + return "s390x"; + case riscv64: + return "riscv64"; + default: + return "x86_64"; + } + } + + /** + * Extracts the major version from a Java version string + */ + private static int extractMajorVersion(String javaVersion) { + return JavaUtils.parseJavaVersion(javaVersion); + } + + private static int jdkVersion(String jdk) { + return JavaUtils.parseJavaVersion(jdk); + } + + static class AvailableMetadataJdk extends Jdk.AvailableJdk.Default { + public final String downloadUrl; + + AvailableMetadataJdk(@NonNull JdkProvider provider, @NonNull String id, @NonNull String version, + @NonNull String downloadUrl, @NonNull Set tags) { + super(provider, id, version, tags); + this.downloadUrl = downloadUrl; + } + } + + public static class Discovery implements JdkInstallers.Discovery { + @Override + public @NonNull String name() { + return "metadata"; + } + + @Override + public @NonNull JdkInstaller create(Config config) { + MetadataJdkInstaller installer = new MetadataJdkInstaller(config.jdkProvider()); + installer + .distro(config.properties().getOrDefault("distro", null)) + .jvmImpl(config.properties().getOrDefault("impl", null)); + HttpClientBuilder httpClientBuilder = NetUtils.createCachingHttpClientBuilder(config.cachePath()); + RemoteAccessProvider rap = RemoteAccessProvider.createDefaultRemoteAccessProvider(httpClientBuilder); + installer.remoteAccessProvider(rap); + return installer; + } + } +} diff --git a/src/main/java/dev/jbang/devkitman/jdkproviders/SdkmanJdkProvider.java b/src/main/java/dev/jbang/devkitman/jdkproviders/SdkmanJdkProvider.java index fe421eb..312ebdb 100644 --- a/src/main/java/dev/jbang/devkitman/jdkproviders/SdkmanJdkProvider.java +++ b/src/main/java/dev/jbang/devkitman/jdkproviders/SdkmanJdkProvider.java @@ -1,50 +1,50 @@ -package dev.jbang.devkitman.jdkproviders; - -import java.nio.file.Path; -import java.nio.file.Paths; - -import org.jspecify.annotations.NonNull; - -import dev.jbang.devkitman.JdkDiscovery; -import dev.jbang.devkitman.JdkProvider; -import dev.jbang.devkitman.util.FileUtils; -import dev.jbang.devkitman.util.JavaUtils; - -/** - * This JDK provider detects any JDKs that have been installed using the SDKMAN - * package manager. - */ -public class SdkmanJdkProvider extends BaseFoldersJdkProvider { - private static final Path JDKS_ROOT = Paths.get(".sdkman", "candidates", "java"); - - public SdkmanJdkProvider() { - super(Paths.get(System.getProperty("user.home")).resolve(JDKS_ROOT)); - } - - @Override - public @NonNull String description() { - return "The JDKs installed using the SDKMAN package manager."; - } - - @Override - protected boolean acceptFolder(@NonNull Path jdkFolder) { - return jdkFolder.startsWith(jdksRoot) - && !FileUtils.isSameFolderLink(jdkFolder) - && JavaUtils.hasJavacCmd(jdkFolder); - } - - public static class Discovery implements JdkDiscovery { - public static final String PROVIDER_ID = "sdkman"; - - @Override - @NonNull - public String name() { - return PROVIDER_ID; - } - - @Override - public JdkProvider create(@NonNull Config config) { - return new SdkmanJdkProvider(); - } - } -} +package dev.jbang.devkitman.jdkproviders; + +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.jspecify.annotations.NonNull; + +import dev.jbang.devkitman.JdkDiscovery; +import dev.jbang.devkitman.JdkProvider; +import dev.jbang.devkitman.util.FileUtils; +import dev.jbang.devkitman.util.JavaUtils; + +/** + * This JDK provider detects any JDKs that have been installed using the SDKMAN + * package manager. + */ +public class SdkmanJdkProvider extends BaseFoldersJdkProvider { + private static final Path JDKS_ROOT = Paths.get(".sdkman", "candidates", "java"); + + public SdkmanJdkProvider() { + super(Paths.get(System.getProperty("user.home")).resolve(JDKS_ROOT)); + } + + @Override + public @NonNull String description() { + return "The JDKs installed using the SDKMAN package manager."; + } + + @Override + protected boolean acceptFolder(@NonNull Path jdkFolder) { + return jdkFolder.startsWith(jdksRoot) + && !FileUtils.isSameFolderLink(jdkFolder) + && JavaUtils.hasJavacCmd(jdkFolder); + } + + public static class Discovery implements JdkDiscovery { + public static final String PROVIDER_ID = "sdkman"; + + @Override + @NonNull + public String name() { + return PROVIDER_ID; + } + + @Override + public JdkProvider create(@NonNull Config config) { + return new SdkmanJdkProvider(); + } + } +} diff --git a/src/main/java/dev/jbang/devkitman/util/FileHttpCacheStorage.java b/src/main/java/dev/jbang/devkitman/util/FileHttpCacheStorage.java index 2670a10..a1fca85 100644 --- a/src/main/java/dev/jbang/devkitman/util/FileHttpCacheStorage.java +++ b/src/main/java/dev/jbang/devkitman/util/FileHttpCacheStorage.java @@ -1,74 +1,105 @@ -package dev.jbang.devkitman.util; - -import java.io.*; -import java.nio.file.Files; -import java.nio.file.Path; - -import org.apache.http.client.cache.HttpCacheEntry; -import org.apache.http.client.cache.HttpCacheStorage; -import org.apache.http.client.cache.HttpCacheUpdateCallback; -import org.apache.http.client.cache.HttpCacheUpdateException; -import org.apache.http.impl.client.cache.DefaultHttpCacheEntrySerializer; - -public class FileHttpCacheStorage implements HttpCacheStorage { - - private final Path cacheDir; - private final DefaultHttpCacheEntrySerializer serializer; - - public FileHttpCacheStorage(Path cacheDir) { - this.cacheDir = cacheDir; - this.serializer = new DefaultHttpCacheEntrySerializer(); - try { - Files.createDirectories(cacheDir); - } catch (IOException e) { - throw new RuntimeException("Failed to create cache directory", e); - } - } - - @Override - public synchronized void putEntry(String key, HttpCacheEntry entry) throws IOException { - Path filePath = cacheDir.resolve(encodeKey(key)); - try (OutputStream os = Files.newOutputStream(filePath); - BufferedOutputStream bos = new BufferedOutputStream(os)) { - serializer.writeTo(entry, bos); - } - } - - @Override - public synchronized HttpCacheEntry getEntry(String key) throws IOException { - Path filePath = cacheDir.resolve(encodeKey(key)); - if (Files.exists(filePath)) { - try (InputStream is = Files.newInputStream(filePath); - BufferedInputStream bis = new BufferedInputStream(is)) { - return serializer.readFrom(bis); - } - } - return null; - } - - @Override - public synchronized void removeEntry(String key) throws IOException { - Path filePath = cacheDir.resolve(encodeKey(key)); - Files.deleteIfExists(filePath); - } - - @Override - public synchronized void updateEntry(String key, HttpCacheUpdateCallback callback) - throws IOException, HttpCacheUpdateException { - Path filePath = cacheDir.resolve(encodeKey(key)); - HttpCacheEntry existingEntry = null; - if (Files.exists(filePath)) { - try (InputStream is = Files.newInputStream(filePath); - BufferedInputStream bis = new BufferedInputStream(is)) { - existingEntry = serializer.readFrom(bis); - } - } - HttpCacheEntry updatedEntry = callback.update(existingEntry); - putEntry(key, updatedEntry); - } - - private String encodeKey(String key) { - // You can use more sophisticated encoding if necessary - return key.replaceAll("[^a-zA-Z0-9-_]", "_"); - } -} +package dev.jbang.devkitman.util; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import org.apache.hc.client5.http.cache.HttpCacheCASOperation; +import org.apache.hc.client5.http.cache.HttpCacheEntry; +import org.apache.hc.client5.http.cache.HttpCacheStorage; +import org.apache.hc.client5.http.cache.ResourceIOException; + +public class FileHttpCacheStorage implements HttpCacheStorage { + + private final Path cacheDir; + + public FileHttpCacheStorage(Path cacheDir) { + this.cacheDir = cacheDir; + } + + @Override + public synchronized void putEntry(String key, HttpCacheEntry entry) throws ResourceIOException { + try { + Files.createDirectories(cacheDir); + } catch (IOException e) { + throw new RuntimeException("Failed to create cache directory", e); + } + Path filePath = cacheDir.resolve(encodeKey(key)); + try (ObjectOutputStream oos = new ObjectOutputStream( + new BufferedOutputStream(Files.newOutputStream(filePath)))) { + oos.writeObject(entry); + } catch (IOException e) { + throw new ResourceIOException("Failed to write cache entry", e); + } + } + + @Override + public synchronized HttpCacheEntry getEntry(String key) throws ResourceIOException { + Path filePath = cacheDir.resolve(encodeKey(key)); + if (!Files.exists(filePath)) { + return null; + } + try (ObjectInputStream ois = new ObjectInputStream( + new BufferedInputStream(Files.newInputStream(filePath)))) { + return (HttpCacheEntry) ois.readObject(); + } catch (IOException | ClassNotFoundException e) { + throw new ResourceIOException("Failed to read cache entry", e); + } + } + + @Override + public synchronized void removeEntry(String key) { + Path filePath = cacheDir.resolve(encodeKey(key)); + try { + Files.deleteIfExists(filePath); + } catch (IOException e) { + // Ignore errors on removal + } + } + + @Override + public synchronized void updateEntry(String key, HttpCacheCASOperation operation) throws ResourceIOException { + HttpCacheEntry existingEntry = getEntry(key); + HttpCacheEntry updatedEntry = operation.execute(existingEntry); + if (updatedEntry != null) { + putEntry(key, updatedEntry); + } + } + + @Override + public synchronized Map getEntries(Collection keys) throws ResourceIOException { + Map result = new HashMap<>(); + for (String key : keys) { + HttpCacheEntry entry = getEntry(key); + if (entry != null) { + result.put(key, entry); + } + } + return result; + } + + private String encodeKey(String key) { + int p = key.indexOf("https://"); + if (p == -1) { + p = key.indexOf("http://"); + } + if (p != -1) { + String hap = key.substring(p); + p = hap.indexOf("?"); + if (p != -1) { + hap = hap.substring(0, p); + } + String encoded = hap.replaceAll("[^a-zA-Z0-9-_]", "_"); + if (encoded.length() > 100) { + encoded = encoded.substring(0, 100); + } + encoded = encoded + "_" + Integer.toHexString(key.hashCode()) + ".cache"; + return encoded; + } else { + return Integer.toHexString(key.hashCode()) + ".cache"; + } + } +} diff --git a/src/main/java/dev/jbang/devkitman/util/FunctionWithError.java b/src/main/java/dev/jbang/devkitman/util/FunctionWithError.java new file mode 100644 index 0000000..ba1c407 --- /dev/null +++ b/src/main/java/dev/jbang/devkitman/util/FunctionWithError.java @@ -0,0 +1,15 @@ +package dev.jbang.devkitman.util; + +import java.io.IOException; + +@FunctionalInterface +public interface FunctionWithError { + OUT apply(IN in) throws IOException; + + default FunctionWithError andThen(FunctionWithError next) { + return in -> { + OUT intermediate = this.apply(in); + return next.apply(intermediate); + }; + } +} diff --git a/src/main/java/dev/jbang/devkitman/util/NetUtils.java b/src/main/java/dev/jbang/devkitman/util/NetUtils.java index 7c555f5..595cd41 100644 --- a/src/main/java/dev/jbang/devkitman/util/NetUtils.java +++ b/src/main/java/dev/jbang/devkitman/util/NetUtils.java @@ -1,126 +1,142 @@ -package dev.jbang.devkitman.util; - -import java.io.IOException; -import java.io.InputStream; -import java.io.UncheckedIOException; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.function.Function; - -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.client.config.RequestConfig; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.entity.ContentType; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; -import org.apache.http.impl.client.cache.CacheConfig; -import org.apache.http.impl.client.cache.CachingHttpClientBuilder; -import org.jspecify.annotations.NonNull; - -public class NetUtils { - - public static final RequestConfig DEFAULT_REQUEST_CONFIG = RequestConfig.custom() - .setConnectionRequestTimeout(10000) - .setConnectTimeout(10000) - .setSocketTimeout(30000) - .build(); - - public static Path downloadFromUrl(String url) throws IOException { - HttpClientBuilder builder = createDefaultHttpClientBuilder(); - return downloadFromUrl(builder, url); - } - - public static Path downloadFromUrl(HttpClientBuilder builder, String url) throws IOException { - return requestUrl(builder, url, NetUtils::handleDownloadResult); - } - - public static T resultFromUrl(String url, Function streamToObject) - throws IOException { - HttpClientBuilder builder = createDefaultHttpClientBuilder(); - return resultFromUrl(builder, url, streamToObject); - } - - public static T resultFromUrl( - HttpClientBuilder builder, String url, Function streamToObject) - throws IOException { - return requestUrl( - builder, - url, - mimetypeChecker("application/json") - .andThen(NetUtils::responseStreamer) - .andThen(streamToObject)); - } - - public static HttpClientBuilder createDefaultHttpClientBuilder() { - return createCachingHttpClientBuilder(Paths.get("http-cache")); - } - - public static HttpClientBuilder createCachingHttpClientBuilder(@NonNull Path cacheDir) { - CacheConfig cacheConfig = CacheConfig.custom().setMaxCacheEntries(1000).build(); - - FileHttpCacheStorage cacheStorage = new FileHttpCacheStorage(cacheDir); - - return CachingHttpClientBuilder.create() - .setCacheConfig(cacheConfig) - .setHttpCacheStorage(cacheStorage) - .setDefaultRequestConfig(DEFAULT_REQUEST_CONFIG); - } - - public static T requestUrl( - HttpClientBuilder builder, String url, Function responseHandler) - throws IOException { - try (CloseableHttpClient httpClient = builder.build()) { - HttpGet httpGet = new HttpGet(url); - try (CloseableHttpResponse response = httpClient.execute(httpGet)) { - int responseCode = response.getStatusLine().getStatusCode(); - if (responseCode != 200) { - throw new IOException( - "Failed to read from URL: " - + url - + ", response code: #" - + responseCode); - } - HttpEntity entity = response.getEntity(); - if (entity == null) { - throw new IOException("Failed to read from URL: " + url + ", no content"); - } - return responseHandler.apply(response); - } - } catch (UncheckedIOException e) { - throw new IOException("Failed to read from URL: " + url + ", " + e.getMessage(), e); - } - } - - private static Function mimetypeChecker(String expectedMimeType) { - return response -> { - String mimeType = ContentType.getOrDefault(response.getEntity()).getMimeType(); - if (expectedMimeType != null && !mimeType.equals(expectedMimeType)) { - throw new RuntimeException("Unexpected MIME type: " + mimeType); - } - return response; - }; - } - - private static InputStream responseStreamer(HttpResponse response) { - try { - HttpEntity entity = response.getEntity(); - return entity.getContent(); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - - private static Path handleDownloadResult(HttpResponse response) { - try { - HttpEntity entity = response.getEntity(); - try (InputStream is = entity.getContent()) { - // TODO implement - return null; - } - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } -} +package dev.jbang.devkitman.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; + +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.cache.CacheConfig; +import org.apache.hc.client5.http.impl.cache.CachingHttpClients; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.apache.hc.core5.http.ClassicHttpResponse; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.util.Timeout; +import org.jspecify.annotations.NonNull; + +public class NetUtils { + + public static final RequestConfig DEFAULT_REQUEST_CONFIG = RequestConfig.custom() + .setConnectionRequestTimeout(Timeout.ofMilliseconds(10000)) + .setConnectTimeout(Timeout.ofMilliseconds(10000)) + .setResponseTimeout(Timeout.ofMilliseconds(30000)) + .build(); + + public static Path downloadFromUrl(String url) throws IOException { + HttpClientBuilder builder = createDefaultHttpClientBuilder(); + return downloadFromUrl(builder, url); + } + + public static Path downloadFromUrl(HttpClientBuilder builder, String url) throws IOException { + return requestUrl(builder, url, NetUtils::handleDownloadResult); + } + + public static T resultFromUrl(String url, FunctionWithError streamToObject) + throws IOException { + HttpClientBuilder builder = createDefaultHttpClientBuilder(); + return resultFromUrl(builder, url, streamToObject); + } + + public static T resultFromUrl( + HttpClientBuilder builder, String url, FunctionWithError streamToObject) + throws IOException { + return requestUrl( + builder, + url, + mimetypeChecker("application/json", "text/plain") + .andThen(NetUtils::responseStreamer) + .andThen(is -> streamToObject.apply(is))); + } + + public static HttpClientBuilder createDefaultHttpClientBuilder() { + return createCachingHttpClientBuilder(Paths.get("http-cache")); + } + + public static HttpClientBuilder createCachingHttpClientBuilder(@NonNull Path cacheDir) { + CacheConfig cacheConfig = CacheConfig.custom() + .setMaxCacheEntries(1000) + .setSharedCache(false) + .build(); + + FileHttpCacheStorage cacheStorage = new FileHttpCacheStorage(cacheDir); + + return CachingHttpClients.custom() + .setCacheConfig(cacheConfig) + .setHttpCacheStorage(cacheStorage) + .addResponseInterceptorFirst((response, entity, context) -> { + // Force cache headers on all 200 OK responses to make them cacheable + if (response.getCode() == 200) { + response.setHeader("Cache-Control", "max-age=3600, public"); + if (!response.containsHeader("Date")) { + response.setHeader("Date", java.time.Instant.now().toString()); + } + } + }) + .setDefaultRequestConfig(DEFAULT_REQUEST_CONFIG); + } + + public static T requestUrl( + HttpClientBuilder builder, String url, FunctionWithError responseHandler) + throws IOException { + try (CloseableHttpClient httpClient = builder.build()) { + HttpGet httpGet = new HttpGet(url); + return httpClient.execute(httpGet, response -> { + int responseCode = response.getCode(); + if (responseCode != 200) { + throw new IOException( + "Failed to read from URL: " + + url + + ", response code: #" + + responseCode); + } + HttpEntity entity = response.getEntity(); + if (entity == null) { + throw new IOException("Failed to read from URL: " + url + ", no content"); + } + return responseHandler.apply(response); + }); + } catch (UncheckedIOException e) { + throw new IOException("Failed to read from URL: " + url + ", " + e.getMessage(), e); + } + } + + private static FunctionWithError mimetypeChecker( + String... expectedMimeTypes) { + return response -> { + ContentType contentType = ContentType.parse(response.getEntity().getContentType()); + String mimeType = contentType != null ? contentType.getMimeType() : "application/octet-stream"; + if (expectedMimeTypes != null && + expectedMimeTypes.length != 0 && + !Arrays.asList(expectedMimeTypes).contains(mimeType)) { + throw new RuntimeException("Unexpected MIME type: " + mimeType); + } + return response; + }; + } + + private static InputStream responseStreamer(ClassicHttpResponse response) { + try { + HttpEntity entity = response.getEntity(); + return entity.getContent(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private static Path handleDownloadResult(ClassicHttpResponse response) { + try { + HttpEntity entity = response.getEntity(); + try (InputStream is = entity.getContent()) { + // TODO implement + return null; + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/src/main/java/dev/jbang/devkitman/util/RemoteAccessProvider.java b/src/main/java/dev/jbang/devkitman/util/RemoteAccessProvider.java index 811efa7..e7736fe 100644 --- a/src/main/java/dev/jbang/devkitman/util/RemoteAccessProvider.java +++ b/src/main/java/dev/jbang/devkitman/util/RemoteAccessProvider.java @@ -1,57 +1,79 @@ -package dev.jbang.devkitman.util; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.function.Function; - -import org.apache.http.impl.client.HttpClientBuilder; - -public interface RemoteAccessProvider { - - Path downloadFromUrl(String url) throws IOException; - - default T resultFromUrl(String url, Function streamToObject) - throws IOException { - Path file = downloadFromUrl(url); - try (InputStream is = Files.newInputStream(file)) { - return streamToObject.apply(is); - } - } - - static RemoteAccessProvider createDefaultRemoteAccessProvider() { - return new DefaultRemoteAccessProvider(); - } - - static RemoteAccessProvider createDefaultRemoteAccessProvider(HttpClientBuilder clientBuilder) { - if (clientBuilder != null) { - return new DefaultRemoteAccessProvider(clientBuilder); - } else { - return new DefaultRemoteAccessProvider(); - } - } - - class DefaultRemoteAccessProvider implements RemoteAccessProvider { - private final HttpClientBuilder clientBuilder; - - public DefaultRemoteAccessProvider() { - this.clientBuilder = NetUtils.createDefaultHttpClientBuilder(); - } - - public DefaultRemoteAccessProvider(HttpClientBuilder clientBuilder) { - this.clientBuilder = clientBuilder; - } - - @Override - public Path downloadFromUrl(String url) throws IOException { - return NetUtils.downloadFromUrl(clientBuilder, url); - } - - @Override - public T resultFromUrl(String url, Function streamToObject) - throws IOException { - return NetUtils.resultFromUrl(clientBuilder, url, streamToObject); - } - } -} +package dev.jbang.devkitman.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +public interface RemoteAccessProvider { + + Path downloadFromUrl(String url) throws IOException; + + default T resultFromUrl(String url, FunctionWithError streamToObject) + throws IOException { + Path file = downloadFromUrl(url); + try (InputStream is = Files.newInputStream(file)) { + return streamToObject.apply(is); + } + } + + static T readJsonFromUrl(RemoteAccessProvider rap, String url, Class klass) throws IOException { + return rap.resultFromUrl(url, is -> { + try (InputStream ignored = is) { + Gson parser = new GsonBuilder().create(); + return parser.fromJson(new InputStreamReader(is), klass); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + + static RemoteAccessProvider createDefaultRemoteAccessProvider() { + return new DefaultRemoteAccessProvider(); + } + + static RemoteAccessProvider createDefaultRemoteAccessProvider(Path cacheDir) { + return new DefaultRemoteAccessProvider(cacheDir); + } + + static RemoteAccessProvider createDefaultRemoteAccessProvider(HttpClientBuilder clientBuilder) { + if (clientBuilder != null) { + return new DefaultRemoteAccessProvider(clientBuilder); + } else { + return new DefaultRemoteAccessProvider(); + } + } + + class DefaultRemoteAccessProvider implements RemoteAccessProvider { + private final HttpClientBuilder clientBuilder; + + public DefaultRemoteAccessProvider() { + this.clientBuilder = NetUtils.createDefaultHttpClientBuilder(); + } + + public DefaultRemoteAccessProvider(Path cacheDir) { + this.clientBuilder = NetUtils.createCachingHttpClientBuilder(cacheDir); + } + + public DefaultRemoteAccessProvider(HttpClientBuilder clientBuilder) { + this.clientBuilder = clientBuilder; + } + + @Override + public Path downloadFromUrl(String url) throws IOException { + return NetUtils.downloadFromUrl(clientBuilder, url); + } + + @Override + public T resultFromUrl(String url, FunctionWithError streamToObject) + throws IOException { + return NetUtils.resultFromUrl(clientBuilder, url, streamToObject); + } + } +} diff --git a/src/test/java/dev/jbang/devkitman/BaseTest.java b/src/test/java/dev/jbang/devkitman/BaseTest.java index 6f799cb..bdd347c 100644 --- a/src/test/java/dev/jbang/devkitman/BaseTest.java +++ b/src/test/java/dev/jbang/devkitman/BaseTest.java @@ -1,248 +1,249 @@ -package dev.jbang.devkitman; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Arrays; -import java.util.List; -import java.util.function.BiConsumer; -import java.util.function.Function; -import java.util.logging.*; -import java.util.stream.Collectors; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.io.TempDir; - -import dev.jbang.devkitman.jdkinstallers.FoojayJdkInstaller; -import dev.jbang.devkitman.jdkinstallers.MetadataJdkInstaller; -import dev.jbang.devkitman.jdkproviders.JBangJdkProvider; -import dev.jbang.devkitman.jdkproviders.MockJdkProvider; -import dev.jbang.devkitman.util.FileUtils; -import dev.jbang.devkitman.util.JavaUtils; -import dev.jbang.devkitman.util.RemoteAccessProvider; - -import uk.org.webcompere.systemstubs.environment.EnvironmentVariables; -import uk.org.webcompere.systemstubs.jupiter.SystemStub; -import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension; - -@ExtendWith(SystemStubsExtension.class) -public class BaseTest { - protected JdkDiscovery.Config config; - - @SystemStub - public final EnvironmentVariables environmentVariables = new EnvironmentVariables(); - - private static Path testJdkFile; - - @BeforeAll - protected static void initAll(@TempDir Path tempPath) throws IOException { - // Force the logging level to FINE - Logger root = LogManager.getLogManager().getLogger(""); - root.setLevel(Level.FINE); - Handler consoleHandler = null; - for (Handler handler : root.getHandlers()) { - if (handler instanceof ConsoleHandler) { - consoleHandler = handler; - break; - } - } - if (consoleHandler == null) { - consoleHandler = new ConsoleHandler(); - root.addHandler(new ConsoleHandler()); - } - consoleHandler.setLevel(Level.FINE); - - testJdkFile = tempPath.resolve("jdk-12.zip"); - Files.copy( - BaseTest.class.getResourceAsStream("/jdk-12.zip"), - testJdkFile, - java.nio.file.StandardCopyOption.REPLACE_EXISTING); - } - - @BeforeEach - protected void initEnv(@TempDir Path tempPath) throws IOException { - System.setProperty("user.home", tempPath.resolve("home").toString()); - config = new JdkDiscovery.Config(tempPath.resolve("jdks"), null, null); - } - - protected JdkManager jdkManager() { - return jdkManager("default", "linked", "jbang"); - } - - protected JdkManager jdkManager(String... providerNames) { - List providers = JdkProviders.instance() - .parseNames(config, providerNames) - .stream() - .map(p -> { - if (p instanceof JBangJdkProvider) { - return createJbangProvider(); - } else { - return p; - } - }) - .collect(Collectors.toList()); - - return JdkManager.builder() - .providers(providers) - .build(); - } - - protected JdkManager mockJdkManager(int... versions) { - String[] vs = Arrays.stream(versions) - .mapToObj(v -> v + ".0.7") - .toArray(String[]::new); - return mockJdkManager(vs); - } - - protected JdkManager mockJdkManager(String... versions) { - return mockJdkManager(this::createDummyJdk, versions); - } - - protected JdkManager mockJdkManager(Function mockJdk, String... versions) { - return JdkManager.builder() - .providers(new MockJdkProvider(config.installPath(), mockJdk, versions)) - .build(); - } - - protected Path createMockJdk(int jdkVersion) { - return createMockJdk(jdkVersion, this::initMockJdkDir); - } - - protected Path createMockJdk(String jdkVersion) { - Path jdkPath = config.installPath().resolve(jdkVersion + "-distro-jbang"); - return createMockJdk(jdkPath, jdkVersion, this::initMockJdkDir); - } - - protected Path createDummyJdk(String jdkVersion) { - Path jdkPath = config.installPath().resolve(jdkVersion + "-distro-dummy"); - return createMockJdk(jdkPath, jdkVersion, this::initMockJdkDir); - } - - protected Path createMockJdk(String jdkId, String jdkVersion) { - Path jdkPath = config.installPath().resolve(jdkId); - return createMockJdk(jdkPath, jdkVersion, this::initMockJdkDir); - } - - protected Path createMockJdkRuntime(int jdkVersion) { - return createMockJdk(jdkVersion, this::initMockJdkDirRuntime); - } - - protected Path createMockJdk(int jdkVersion, BiConsumer init) { - Path jdkPath = config.installPath().resolve(jdkVersion + ".0.7-distro-jbang"); - return createMockJdk(jdkPath, jdkVersion + ".0.7", init); - } - - protected Path createMockJdk(Path jdkPath, String jdkVersion, BiConsumer init) { - init.accept(jdkPath, jdkVersion); - Path link = config.installPath().resolve("default"); - if (!Files.exists(link)) { - FileUtils.createLink(link, jdkPath); - } - int v = JavaUtils.parseJavaVersion(jdkVersion); - Path vlink = config.installPath().resolve(String.valueOf(v)); - if (!Files.exists(vlink)) { - FileUtils.createLink(vlink, jdkPath); - } - return jdkPath; - } - - protected Path createMockJdkExt(int jdkVersion) { - Path jdkPath = config.cachePath().resolve("jdk" + jdkVersion); - FileUtils.mkdirs(jdkPath); - initMockJdkDir(jdkPath, jdkVersion + ".0.7"); - return jdkPath; - } - - protected void initMockJdkDirRuntime(Path jdkPath, String version) { - initMockJdkDir(jdkPath, version, "JAVA_RUNTIME_VERSION", true, false, false, false); - } - - protected void initMockJdkDir(Path jdkPath, String version) { - initMockJdkDir(jdkPath, version, "JAVA_VERSION", true, false, false, false); - } - - protected void initMockJdkDir(Path jdkPath, String version, String key, boolean isJdk, boolean isGraalVM, - boolean hasNativeCmd, boolean hasJavaFX) { - try { - Path jdkBinPath = jdkPath.resolve("bin"); - Files.createDirectories(jdkBinPath); - String releaseText = ""; - String rawJavaVersion = key + "=\"" + version + "\"\n"; - releaseText += rawJavaVersion; - Path release = jdkPath.resolve("release"); - Path javaPath = jdkBinPath.resolve("java"); - writeString(javaPath, "dummy"); - javaPath.toFile().setExecutable(true, true); - writeString(jdkBinPath.resolve("java.exe"), "dummy"); - if (isJdk) { - Path javacPath = jdkBinPath.resolve("javac"); - writeString(javacPath, "dummy"); - javacPath.toFile().setExecutable(true, true); - writeString(jdkBinPath.resolve("javac.exe"), "dummy"); - if (isGraalVM) { - String rawGraalVMVersion = "GRAALVM_VERSION=\"" + version + "\"\n"; - releaseText += rawGraalVMVersion; - if (hasNativeCmd) { - Path nativePath = jdkBinPath.resolve("native-image"); - writeString(nativePath, "dummy"); - nativePath.toFile().setExecutable(true, true); - writeString(jdkBinPath.resolve("native-image.exe"), "dummy"); - } - } - } - if (hasJavaFX) { - Path jdkLibPath = jdkPath.resolve("lib"); - Files.createDirectories(jdkLibPath); - String rawJavaFXVersion = "javafx.version=" + version; - Path jfxprops = jdkLibPath.resolve("javafx.properties"); - writeString(jfxprops, rawJavaFXVersion); - } - writeString(release, releaseText); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - protected JBangJdkProvider createJbangProvider() { - RemoteAccessProvider rap = new RemoteAccessProvider() { - @Override - public Path downloadFromUrl(String url) throws IOException { - if (url.startsWith("https://api.foojay.io/disco/v3.0/ids/") && url.endsWith("/redirect")) { - return testJdkFile; - } else if (url.startsWith("https://github.com/adoptium/") && url.endsWith(".zip")) { - return testJdkFile; - } - throw new IOException("Unexpected URL: " + url); - } - - @Override - public T resultFromUrl( - String url, Function streamToObject) - throws IOException { - if (url.startsWith(FoojayJdkInstaller.FOOJAY_JDK_VERSIONS_URL)) { - return streamToObject.apply( - getClass().getResourceAsStream("/testFoojayInstall.json")); - } else if (url.startsWith(MetadataJdkInstaller.METADATA_BASE_URL)) { - return streamToObject.apply( - getClass().getResourceAsStream("/testMetadataInstall.json")); - } - throw new IOException("Unexpected URL: " + url); - } - }; - - JBangJdkProvider jbang = new JBangJdkProvider(config.installPath()); - FoojayJdkInstaller installer = new FoojayJdkInstaller(jbang) - .distro("jbang") - .remoteAccessProvider(rap); - installer.remoteAccessProvider(rap); - jbang.installer(installer); - return jbang; - } - - protected void writeString(Path toPath, String scriptText) throws IOException { - Files.write(toPath, scriptText.getBytes()); - } -} +package dev.jbang.devkitman; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.logging.*; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; + +import dev.jbang.devkitman.jdkinstallers.FoojayJdkInstaller; +import dev.jbang.devkitman.jdkinstallers.MetadataJdkInstaller; +import dev.jbang.devkitman.jdkproviders.JBangJdkProvider; +import dev.jbang.devkitman.jdkproviders.MockJdkProvider; +import dev.jbang.devkitman.util.FileUtils; +import dev.jbang.devkitman.util.FunctionWithError; +import dev.jbang.devkitman.util.JavaUtils; +import dev.jbang.devkitman.util.RemoteAccessProvider; + +import uk.org.webcompere.systemstubs.environment.EnvironmentVariables; +import uk.org.webcompere.systemstubs.jupiter.SystemStub; +import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension; + +@ExtendWith(SystemStubsExtension.class) +public class BaseTest { + protected JdkDiscovery.Config config; + + @SystemStub + public final EnvironmentVariables environmentVariables = new EnvironmentVariables(); + + private static Path testJdkFile; + + @BeforeAll + protected static void initAll(@TempDir Path tempPath) throws IOException { + // Force the logging level to FINE + Logger root = LogManager.getLogManager().getLogger(""); + root.setLevel(Level.FINE); + Handler consoleHandler = null; + for (Handler handler : root.getHandlers()) { + if (handler instanceof ConsoleHandler) { + consoleHandler = handler; + break; + } + } + if (consoleHandler == null) { + consoleHandler = new ConsoleHandler(); + root.addHandler(new ConsoleHandler()); + } + consoleHandler.setLevel(Level.FINE); + + testJdkFile = tempPath.resolve("jdk-12.zip"); + Files.copy( + BaseTest.class.getResourceAsStream("/jdk-12.zip"), + testJdkFile, + java.nio.file.StandardCopyOption.REPLACE_EXISTING); + } + + @BeforeEach + protected void initEnv(@TempDir Path tempPath) throws IOException { + System.setProperty("user.home", tempPath.resolve("home").toString()); + config = new JdkDiscovery.Config(tempPath.resolve("jdks"), null, null); + } + + protected JdkManager jdkManager() { + return jdkManager("default", "linked", "jbang"); + } + + protected JdkManager jdkManager(String... providerNames) { + List providers = JdkProviders.instance() + .parseNames(config, providerNames) + .stream() + .map(p -> { + if (p instanceof JBangJdkProvider) { + return createJbangProvider(); + } else { + return p; + } + }) + .collect(Collectors.toList()); + + return JdkManager.builder() + .providers(providers) + .build(); + } + + protected JdkManager mockJdkManager(int... versions) { + String[] vs = Arrays.stream(versions) + .mapToObj(v -> v + ".0.7") + .toArray(String[]::new); + return mockJdkManager(vs); + } + + protected JdkManager mockJdkManager(String... versions) { + return mockJdkManager(this::createDummyJdk, versions); + } + + protected JdkManager mockJdkManager(Function mockJdk, String... versions) { + return JdkManager.builder() + .providers(new MockJdkProvider(config.installPath(), mockJdk, versions)) + .build(); + } + + protected Path createMockJdk(int jdkVersion) { + return createMockJdk(jdkVersion, this::initMockJdkDir); + } + + protected Path createMockJdk(String jdkVersion) { + Path jdkPath = config.installPath().resolve(jdkVersion + "-distro-jbang"); + return createMockJdk(jdkPath, jdkVersion, this::initMockJdkDir); + } + + protected Path createDummyJdk(String jdkVersion) { + Path jdkPath = config.installPath().resolve(jdkVersion + "-distro-dummy"); + return createMockJdk(jdkPath, jdkVersion, this::initMockJdkDir); + } + + protected Path createMockJdk(String jdkId, String jdkVersion) { + Path jdkPath = config.installPath().resolve(jdkId); + return createMockJdk(jdkPath, jdkVersion, this::initMockJdkDir); + } + + protected Path createMockJdkRuntime(int jdkVersion) { + return createMockJdk(jdkVersion, this::initMockJdkDirRuntime); + } + + protected Path createMockJdk(int jdkVersion, BiConsumer init) { + Path jdkPath = config.installPath().resolve(jdkVersion + ".0.7-distro-jbang"); + return createMockJdk(jdkPath, jdkVersion + ".0.7", init); + } + + protected Path createMockJdk(Path jdkPath, String jdkVersion, BiConsumer init) { + init.accept(jdkPath, jdkVersion); + Path link = config.installPath().resolve("default"); + if (!Files.exists(link)) { + FileUtils.createLink(link, jdkPath); + } + int v = JavaUtils.parseJavaVersion(jdkVersion); + Path vlink = config.installPath().resolve(String.valueOf(v)); + if (!Files.exists(vlink)) { + FileUtils.createLink(vlink, jdkPath); + } + return jdkPath; + } + + protected Path createMockJdkExt(int jdkVersion) { + Path jdkPath = config.cachePath().resolve("jdk" + jdkVersion); + FileUtils.mkdirs(jdkPath); + initMockJdkDir(jdkPath, jdkVersion + ".0.7"); + return jdkPath; + } + + protected void initMockJdkDirRuntime(Path jdkPath, String version) { + initMockJdkDir(jdkPath, version, "JAVA_RUNTIME_VERSION", true, false, false, false); + } + + protected void initMockJdkDir(Path jdkPath, String version) { + initMockJdkDir(jdkPath, version, "JAVA_VERSION", true, false, false, false); + } + + protected void initMockJdkDir(Path jdkPath, String version, String key, boolean isJdk, boolean isGraalVM, + boolean hasNativeCmd, boolean hasJavaFX) { + try { + Path jdkBinPath = jdkPath.resolve("bin"); + Files.createDirectories(jdkBinPath); + String releaseText = ""; + String rawJavaVersion = key + "=\"" + version + "\"\n"; + releaseText += rawJavaVersion; + Path release = jdkPath.resolve("release"); + Path javaPath = jdkBinPath.resolve("java"); + writeString(javaPath, "dummy"); + javaPath.toFile().setExecutable(true, true); + writeString(jdkBinPath.resolve("java.exe"), "dummy"); + if (isJdk) { + Path javacPath = jdkBinPath.resolve("javac"); + writeString(javacPath, "dummy"); + javacPath.toFile().setExecutable(true, true); + writeString(jdkBinPath.resolve("javac.exe"), "dummy"); + if (isGraalVM) { + String rawGraalVMVersion = "GRAALVM_VERSION=\"" + version + "\"\n"; + releaseText += rawGraalVMVersion; + if (hasNativeCmd) { + Path nativePath = jdkBinPath.resolve("native-image"); + writeString(nativePath, "dummy"); + nativePath.toFile().setExecutable(true, true); + writeString(jdkBinPath.resolve("native-image.exe"), "dummy"); + } + } + } + if (hasJavaFX) { + Path jdkLibPath = jdkPath.resolve("lib"); + Files.createDirectories(jdkLibPath); + String rawJavaFXVersion = "javafx.version=" + version; + Path jfxprops = jdkLibPath.resolve("javafx.properties"); + writeString(jfxprops, rawJavaFXVersion); + } + writeString(release, releaseText); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + protected JBangJdkProvider createJbangProvider() { + RemoteAccessProvider rap = new RemoteAccessProvider() { + @Override + public Path downloadFromUrl(String url) throws IOException { + if (url.startsWith("https://api.foojay.io/disco/v3.0/ids/") && url.endsWith("/redirect")) { + return testJdkFile; + } else if (url.startsWith("https://github.com/adoptium/") && url.endsWith(".zip")) { + return testJdkFile; + } + throw new IOException("Unexpected URL: " + url); + } + + @Override + public T resultFromUrl( + String url, FunctionWithError streamToObject) + throws IOException { + if (url.startsWith(FoojayJdkInstaller.FOOJAY_JDK_VERSIONS_URL)) { + return streamToObject.apply( + getClass().getResourceAsStream("/testFoojayInstall.json")); + } else if (url.startsWith(MetadataJdkInstaller.METADATA_BASE_URL)) { + return streamToObject.apply( + getClass().getResourceAsStream("/testMetadataInstall.json")); + } + throw new IOException("Unexpected URL: " + url); + } + }; + + JBangJdkProvider jbang = new JBangJdkProvider(config.installPath()); + FoojayJdkInstaller installer = new FoojayJdkInstaller(jbang) + .distro("jbang") + .remoteAccessProvider(rap); + installer.remoteAccessProvider(rap); + jbang.installer(installer); + return jbang; + } + + protected void writeString(Path toPath, String scriptText) throws IOException { + Files.write(toPath, scriptText.getBytes()); + } +} diff --git a/src/test/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstallerTest.java b/src/test/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstallerTest.java index fac8bdd..a86cc40 100644 --- a/src/test/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstallerTest.java +++ b/src/test/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstallerTest.java @@ -1,349 +1,349 @@ -package dev.jbang.devkitman.jdkinstallers; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; -import static org.junit.jupiter.api.Assertions.*; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; -import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import dev.jbang.devkitman.BaseTest; -import dev.jbang.devkitman.Jdk; -import dev.jbang.devkitman.JdkManager; -import dev.jbang.devkitman.jdkproviders.JBangJdkProvider; -import dev.jbang.devkitman.util.RemoteAccessProvider; - -public class FoojayJdkInstallerTest extends BaseTest { - - private FoojayJdkInstaller installer; - private JBangJdkProvider provider; - private Path testJdkFile; - - @BeforeEach - @Override - protected void initEnv(@TempDir Path tempPath) throws IOException { - super.initEnv(tempPath); - - // Copy test JDK file for installation tests - testJdkFile = tempPath.resolve("jdk-12.zip"); - Files.copy( - getClass().getResourceAsStream("/jdk-12.zip"), - testJdkFile, - java.nio.file.StandardCopyOption.REPLACE_EXISTING); - - RemoteAccessProvider rap = createRemoteAccessProvider(); - provider = new JBangJdkProvider(config.installPath()); - installer = new FoojayJdkInstaller(provider) - .distro("jbang") - .remoteAccessProvider(rap); - provider.installer(installer); - - // Create a manager so the provider has access to defaultJavaVersion - JdkManager manager = JdkManager.builder() - .providers(provider) - .build(); - } - - private RemoteAccessProvider createRemoteAccessProvider() { - return new RemoteAccessProvider() { - @Override - public Path downloadFromUrl(String url) throws IOException { - // Verify URL format for Foojay API - if (!url.startsWith("https://api.foojay.io/disco/v3.0/ids/") || !url.endsWith("/redirect")) { - throw new IOException("Unexpected URL: " + url); - } - return testJdkFile; - } - - @Override - public T resultFromUrl(String url, Function streamToObject) - throws IOException { - // Verify the URL format matches expected Foojay API pattern - if (!url.startsWith(FoojayJdkInstaller.FOOJAY_JDK_VERSIONS_URL)) { - throw new IOException("Unexpected URL: " + url); - } - // Return our test Foojay JSON for all requests - return streamToObject.apply( - getClass().getResourceAsStream("/testFoojayInstall.json")); - } - }; - } - - @Test - public void testListAvailable() { - List jdks = installer.listAvailable().collect(Collectors.toList()); - - // Should have JDKs from test data - assertThat(jdks, is(not(empty()))); - - // Verify we have expected major versions from testInstall.json - List versions = jdks.stream() - .map(jdk -> Integer.parseInt(jdk.version().split("[.\\-+]")[0])) - .distinct() - .sorted() - .collect(Collectors.toList()); - - // testInstall.json contains versions 11-25 - assertThat(versions, hasItems(11, 17, 21, 23)); - } - - @Test - public void testListAvailableOrderedByVersionDescending() { - List jdks = installer.listAvailable().collect(Collectors.toList()); - - assertThat(jdks.size(), greaterThan(0)); - - // First JDK should be the highest version in our test data - String firstVersion = jdks.get(0).version(); - int firstMajor = Integer.parseInt(firstVersion.split("[.\\-+]")[0]); - - // Verify it's one of the higher versions - assertThat(firstMajor, greaterThanOrEqualTo(23)); - } - - @Test - public void testGetAvailableByVersionExact() { - Jdk.AvailableJdk jdk21 = installer.getAvailableByVersion(21, false); - - assertThat(jdk21, is(notNullValue())); - assertThat(jdk21.version(), startsWith("21.")); - assertTrue(jdk21.tags().contains(Jdk.Default.Tags.Ga.name())); - assertTrue(jdk21.tags().contains(Jdk.Default.Tags.Jdk.name())); - } - - @Test - public void testGetAvailableByVersionOpen() { - // Request version 17+, should return newest available - Jdk.AvailableJdk jdk = installer.getAvailableByVersion(17, true); - - assertThat(jdk, is(notNullValue())); - String version = jdk.version(); - int major = Integer.parseInt(version.split("[.\\-+]")[0]); - assertThat(major, greaterThanOrEqualTo(17)); - } - - @Test - public void testGetAvailableByVersionNotFound() { - // Request version that doesn't exist in our test data - Jdk.AvailableJdk jdk = installer.getAvailableByVersion(6, false); - - assertThat(jdk, is(nullValue())); - } - - @Test - public void testDetermineIdIncludesDistro() { - List jdks = installer.listAvailable().collect(Collectors.toList()); - - assertThat(jdks, is(not(empty()))); - - // All IDs should contain a distribution name - for (Jdk.AvailableJdk jdk : jdks) { - // The ID should contain either temurin or aoj or other distro names - assertThat(jdk.id(), anyOf( - containsString("temurin"), - containsString("aoj"), - containsString("liberica"), - containsString("zulu"))); - } - } - - @Test - public void testDetermineIdForJre() { - // Note: testInstall.json might not contain JRE entries - // This test verifies the logic would work if JRE was present - List jdks = installer.listAvailable().collect(Collectors.toList()); - - Jdk.AvailableJdk jre = jdks.stream() - .filter(j -> j.tags().contains(Jdk.Default.Tags.Jre.name())) - .findFirst() - .orElse(null); - - if (jre != null) { - assertThat(jre.id(), containsString("-jre")); - } - } - - @Test - public void testDetermineTagsForGa() { - Jdk.AvailableJdk jdk17 = installer.getAvailableByVersion(17, false); - - assertThat(jdk17, is(notNullValue())); - Set tags = jdk17.tags(); - - assertThat(tags, hasItem(Jdk.Default.Tags.Ga.name())); - assertThat(tags, hasItem(Jdk.Default.Tags.Jdk.name())); - } - - @Test - public void testDetermineTagsForEa() { - // testInstall.json contains EA versions - List jdks = installer.listAvailable().collect(Collectors.toList()); - - Jdk.AvailableJdk ea = jdks.stream() - .filter(j -> j.tags().contains(Jdk.Default.Tags.Ea.name())) - .findFirst() - .orElse(null); - - if (ea != null) { - Set tags = ea.tags(); - assertThat(tags, hasItem(Jdk.Default.Tags.Ea.name())); - } - } - - @Test - public void testDetermineTagsForJdk() { - List jdks = installer.listAvailable().collect(Collectors.toList()); - - // Most entries should be JDK - long jdkCount = jdks.stream() - .filter(j -> j.tags().contains(Jdk.Default.Tags.Jdk.name())) - .count(); - - assertThat(jdkCount, greaterThan(0L)); - } - - @Test - public void testInstallJdk() throws IOException { - Jdk.AvailableJdk jdk17 = installer.getAvailableByVersion(17, false); - assertThat(jdk17, is(notNullValue())); - - Path installDir = config.installPath().resolve("test-jdk-17"); - Files.createDirectories(installDir.getParent()); - - Jdk.InstalledJdk installed = installer.install(jdk17, installDir); - - assertThat(installed, is(notNullValue())); - assertThat(Files.exists(installDir), is(true)); - assertThat(Files.isDirectory(installDir), is(true)); - } - - @Test - public void testInstallJdkInvalidType() { - // Create a mock JDK from a different installer type - Jdk.AvailableJdk mockJdk = new Jdk.AvailableJdk.Default( - provider, - "mock-jdk-21", - "21.0.0", - Set.of(Jdk.Default.Tags.Ga.name())); - - Path installDir = config.installPath().resolve("test-jdk-mock"); - - IllegalArgumentException exception = assertThrows( - IllegalArgumentException.class, - () -> installer.install(mockJdk, installDir)); - - assertThat(exception.getMessage(), containsString("FoojayJdkInstaller can only install")); - } - - @Test - public void testUninstallJdk() throws IOException { - // Create a mock installed JDK - Path jdkPath = config.installPath().resolve("test-jdk-uninstall"); - initMockJdkDir(jdkPath, "17.0.13"); - - Jdk.InstalledJdk jdk = new Jdk.InstalledJdk.Default( - provider, - "17.0.13-temurin-jbang", - jdkPath, - "17.0.13", - Set.of(Jdk.Default.Tags.Ga.name())); - - assertThat(Files.exists(jdkPath), is(true)); - - installer.uninstall(jdk); - - // JDK directory should be removed - assertThat(Files.exists(jdkPath), is(false)); - } - - @Test - public void testFilterEAWhenGAExists() { - // If we have both EA and GA for same version, EA should be filtered out - List jdks = installer.listAvailable().collect(Collectors.toList()); - - // Count how many times each major version appears - List majorVersions = jdks.stream() - .map(jdk -> Integer.parseInt(jdk.version().split("[.\\-+]")[0])) - .collect(Collectors.toList()); - - // Each major version should appear at most once (after filtering) - long uniqueVersions = majorVersions.stream().distinct().count(); - assertThat((long) majorVersions.size(), greaterThanOrEqualTo(uniqueVersions)); - } - - @Test - public void testAvailableFoojayJdkCreation() { - List jdks = installer.listAvailable().collect(Collectors.toList()); - - assertThat(jdks, is(not(empty()))); - - // Verify that the JDKs are of the correct type - for (Jdk.AvailableJdk jdk : jdks) { - assertThat(jdk, is(instanceOf(FoojayJdkInstaller.AvailableFoojayJdk.class))); - - // Verify essential properties - assertThat(jdk.id(), is(not(emptyString()))); - assertThat(jdk.version(), is(not(emptyString()))); - assertThat(jdk.tags(), is(not(empty()))); - assertThat(jdk.provider(), is(provider)); - } - } - - @Test - public void testDistroSortingOrder() { - // Test that JDKs are sorted by distribution preference - List jdks = installer.listAvailable().collect(Collectors.toList()); - - assertThat(jdks, is(not(empty()))); - - // When filtering by same major version, the preferred distro should come first - // This is based on the distro configuration (default: "temurin,aoj") - List majorVersions = jdks.stream() - .map(jdk -> Integer.parseInt(jdk.version().split("[.\\-+]")[0])) - .distinct() - .collect(Collectors.toList()); - - // Just verify we have multiple versions - assertThat(majorVersions.size(), greaterThan(0)); - } - - @Test - public void testJavaFXBundled() { - // Test JavaFX bundled detection - List jdks = installer.listAvailable().collect(Collectors.toList()); - - // Check if any JDKs have JavaFX tag (depends on test data) - long javafxCount = jdks.stream() - .filter(j -> j.tags().contains(Jdk.Default.Tags.Javafx.name())) - .count(); - - // This is just verifying the tag system works, count might be 0 - assertThat(javafxCount, greaterThanOrEqualTo(0L)); - } - - @Test - public void testMajorVersionExtraction() { - List jdks = installer.listAvailable().collect(Collectors.toList()); - - assertThat(jdks, is(not(empty()))); - - // Verify all JDKs have parseable major versions - for (Jdk.AvailableJdk jdk : jdks) { - String version = jdk.version(); - int major = Integer.parseInt(version.split("[.\\-+]")[0]); - - // Major version should be reasonable (8-30 range as of 2026) - assertThat(major, allOf(greaterThanOrEqualTo(8), lessThanOrEqualTo(30))); - } - } -} +package dev.jbang.devkitman.jdkinstallers; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import dev.jbang.devkitman.BaseTest; +import dev.jbang.devkitman.Jdk; +import dev.jbang.devkitman.JdkManager; +import dev.jbang.devkitman.jdkproviders.JBangJdkProvider; +import dev.jbang.devkitman.util.FunctionWithError; +import dev.jbang.devkitman.util.RemoteAccessProvider; + +public class FoojayJdkInstallerTest extends BaseTest { + + private FoojayJdkInstaller installer; + private JBangJdkProvider provider; + private Path testJdkFile; + + @BeforeEach + @Override + protected void initEnv(@TempDir Path tempPath) throws IOException { + super.initEnv(tempPath); + + // Copy test JDK file for installation tests + testJdkFile = tempPath.resolve("jdk-12.zip"); + Files.copy( + getClass().getResourceAsStream("/jdk-12.zip"), + testJdkFile, + java.nio.file.StandardCopyOption.REPLACE_EXISTING); + + RemoteAccessProvider rap = createRemoteAccessProvider(); + provider = new JBangJdkProvider(config.installPath()); + installer = new FoojayJdkInstaller(provider) + .distro("jbang") + .remoteAccessProvider(rap); + provider.installer(installer); + + // Create a manager so the provider has access to defaultJavaVersion + JdkManager manager = JdkManager.builder() + .providers(provider) + .build(); + } + + private RemoteAccessProvider createRemoteAccessProvider() { + return new RemoteAccessProvider() { + @Override + public Path downloadFromUrl(String url) throws IOException { + // Verify URL format for Foojay API + if (!url.startsWith("https://api.foojay.io/disco/v3.0/ids/") || !url.endsWith("/redirect")) { + throw new IOException("Unexpected URL: " + url); + } + return testJdkFile; + } + + @Override + public T resultFromUrl(String url, FunctionWithError streamToObject) + throws IOException { + // Verify the URL format matches expected Foojay API pattern + if (!url.startsWith(FoojayJdkInstaller.FOOJAY_JDK_VERSIONS_URL)) { + throw new IOException("Unexpected URL: " + url); + } + // Return our test Foojay JSON for all requests + return streamToObject.apply( + getClass().getResourceAsStream("/testFoojayInstall.json")); + } + }; + } + + @Test + public void testListAvailable() { + List jdks = installer.listAvailable().collect(Collectors.toList()); + + // Should have JDKs from test data + assertThat(jdks, is(not(empty()))); + + // Verify we have expected major versions from testInstall.json + List versions = jdks.stream() + .map(jdk -> Integer.parseInt(jdk.version().split("[.\\-+]")[0])) + .distinct() + .sorted() + .collect(Collectors.toList()); + + // testInstall.json contains versions 11-25 + assertThat(versions, hasItems(11, 17, 21, 23)); + } + + @Test + public void testListAvailableOrderedByVersionDescending() { + List jdks = installer.listAvailable().collect(Collectors.toList()); + + assertThat(jdks.size(), greaterThan(0)); + + // First JDK should be the highest version in our test data + String firstVersion = jdks.get(0).version(); + int firstMajor = Integer.parseInt(firstVersion.split("[.\\-+]")[0]); + + // Verify it's one of the higher versions + assertThat(firstMajor, greaterThanOrEqualTo(23)); + } + + @Test + public void testGetAvailableByVersionExact() { + Jdk.AvailableJdk jdk21 = installer.getAvailableByVersion(21, false); + + assertThat(jdk21, is(notNullValue())); + assertThat(jdk21.version(), startsWith("21.")); + assertTrue(jdk21.tags().contains(Jdk.Default.Tags.Ga.name())); + assertTrue(jdk21.tags().contains(Jdk.Default.Tags.Jdk.name())); + } + + @Test + public void testGetAvailableByVersionOpen() { + // Request version 17+, should return newest available + Jdk.AvailableJdk jdk = installer.getAvailableByVersion(17, true); + + assertThat(jdk, is(notNullValue())); + String version = jdk.version(); + int major = Integer.parseInt(version.split("[.\\-+]")[0]); + assertThat(major, greaterThanOrEqualTo(17)); + } + + @Test + public void testGetAvailableByVersionNotFound() { + // Request version that doesn't exist in our test data + Jdk.AvailableJdk jdk = installer.getAvailableByVersion(6, false); + + assertThat(jdk, is(nullValue())); + } + + @Test + public void testDetermineIdIncludesDistro() { + List jdks = installer.listAvailable().collect(Collectors.toList()); + + assertThat(jdks, is(not(empty()))); + + // All IDs should contain a distribution name + for (Jdk.AvailableJdk jdk : jdks) { + // The ID should contain either temurin or aoj or other distro names + assertThat(jdk.id(), anyOf( + containsString("temurin"), + containsString("aoj"), + containsString("liberica"), + containsString("zulu"))); + } + } + + @Test + public void testDetermineIdForJre() { + // Note: testInstall.json might not contain JRE entries + // This test verifies the logic would work if JRE was present + List jdks = installer.listAvailable().collect(Collectors.toList()); + + Jdk.AvailableJdk jre = jdks.stream() + .filter(j -> j.tags().contains(Jdk.Default.Tags.Jre.name())) + .findFirst() + .orElse(null); + + if (jre != null) { + assertThat(jre.id(), containsString("-jre")); + } + } + + @Test + public void testDetermineTagsForGa() { + Jdk.AvailableJdk jdk17 = installer.getAvailableByVersion(17, false); + + assertThat(jdk17, is(notNullValue())); + Set tags = jdk17.tags(); + + assertThat(tags, hasItem(Jdk.Default.Tags.Ga.name())); + assertThat(tags, hasItem(Jdk.Default.Tags.Jdk.name())); + } + + @Test + public void testDetermineTagsForEa() { + // testInstall.json contains EA versions + List jdks = installer.listAvailable().collect(Collectors.toList()); + + Jdk.AvailableJdk ea = jdks.stream() + .filter(j -> j.tags().contains(Jdk.Default.Tags.Ea.name())) + .findFirst() + .orElse(null); + + if (ea != null) { + Set tags = ea.tags(); + assertThat(tags, hasItem(Jdk.Default.Tags.Ea.name())); + } + } + + @Test + public void testDetermineTagsForJdk() { + List jdks = installer.listAvailable().collect(Collectors.toList()); + + // Most entries should be JDK + long jdkCount = jdks.stream() + .filter(j -> j.tags().contains(Jdk.Default.Tags.Jdk.name())) + .count(); + + assertThat(jdkCount, greaterThan(0L)); + } + + @Test + public void testInstallJdk() throws IOException { + Jdk.AvailableJdk jdk17 = installer.getAvailableByVersion(17, false); + assertThat(jdk17, is(notNullValue())); + + Path installDir = config.installPath().resolve("test-jdk-17"); + Files.createDirectories(installDir.getParent()); + + Jdk.InstalledJdk installed = installer.install(jdk17, installDir); + + assertThat(installed, is(notNullValue())); + assertThat(Files.exists(installDir), is(true)); + assertThat(Files.isDirectory(installDir), is(true)); + } + + @Test + public void testInstallJdkInvalidType() { + // Create a mock JDK from a different installer type + Jdk.AvailableJdk mockJdk = new Jdk.AvailableJdk.Default( + provider, + "mock-jdk-21", + "21.0.0", + Set.of(Jdk.Default.Tags.Ga.name())); + + Path installDir = config.installPath().resolve("test-jdk-mock"); + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> installer.install(mockJdk, installDir)); + + assertThat(exception.getMessage(), containsString("FoojayJdkInstaller can only install")); + } + + @Test + public void testUninstallJdk() throws IOException { + // Create a mock installed JDK + Path jdkPath = config.installPath().resolve("test-jdk-uninstall"); + initMockJdkDir(jdkPath, "17.0.13"); + + Jdk.InstalledJdk jdk = new Jdk.InstalledJdk.Default( + provider, + "17.0.13-temurin-jbang", + jdkPath, + "17.0.13", + Set.of(Jdk.Default.Tags.Ga.name())); + + assertThat(Files.exists(jdkPath), is(true)); + + installer.uninstall(jdk); + + // JDK directory should be removed + assertThat(Files.exists(jdkPath), is(false)); + } + + @Test + public void testFilterEAWhenGAExists() { + // If we have both EA and GA for same version, EA should be filtered out + List jdks = installer.listAvailable().collect(Collectors.toList()); + + // Count how many times each major version appears + List majorVersions = jdks.stream() + .map(jdk -> Integer.parseInt(jdk.version().split("[.\\-+]")[0])) + .collect(Collectors.toList()); + + // Each major version should appear at most once (after filtering) + long uniqueVersions = majorVersions.stream().distinct().count(); + assertThat((long) majorVersions.size(), greaterThanOrEqualTo(uniqueVersions)); + } + + @Test + public void testAvailableFoojayJdkCreation() { + List jdks = installer.listAvailable().collect(Collectors.toList()); + + assertThat(jdks, is(not(empty()))); + + // Verify that the JDKs are of the correct type + for (Jdk.AvailableJdk jdk : jdks) { + assertThat(jdk, is(instanceOf(FoojayJdkInstaller.AvailableFoojayJdk.class))); + + // Verify essential properties + assertThat(jdk.id(), is(not(emptyString()))); + assertThat(jdk.version(), is(not(emptyString()))); + assertThat(jdk.tags(), is(not(empty()))); + assertThat(jdk.provider(), is(provider)); + } + } + + @Test + public void testDistroSortingOrder() { + // Test that JDKs are sorted by distribution preference + List jdks = installer.listAvailable().collect(Collectors.toList()); + + assertThat(jdks, is(not(empty()))); + + // When filtering by same major version, the preferred distro should come first + // This is based on the distro configuration (default: "temurin,aoj") + List majorVersions = jdks.stream() + .map(jdk -> Integer.parseInt(jdk.version().split("[.\\-+]")[0])) + .distinct() + .collect(Collectors.toList()); + + // Just verify we have multiple versions + assertThat(majorVersions.size(), greaterThan(0)); + } + + @Test + public void testJavaFXBundled() { + // Test JavaFX bundled detection + List jdks = installer.listAvailable().collect(Collectors.toList()); + + // Check if any JDKs have JavaFX tag (depends on test data) + long javafxCount = jdks.stream() + .filter(j -> j.tags().contains(Jdk.Default.Tags.Javafx.name())) + .count(); + + // This is just verifying the tag system works, count might be 0 + assertThat(javafxCount, greaterThanOrEqualTo(0L)); + } + + @Test + public void testMajorVersionExtraction() { + List jdks = installer.listAvailable().collect(Collectors.toList()); + + assertThat(jdks, is(not(empty()))); + + // Verify all JDKs have parseable major versions + for (Jdk.AvailableJdk jdk : jdks) { + String version = jdk.version(); + int major = Integer.parseInt(version.split("[.\\-+]")[0]); + + // Major version should be reasonable (8-30 range as of 2026) + assertThat(major, allOf(greaterThanOrEqualTo(8), lessThanOrEqualTo(30))); + } + } +} diff --git a/src/test/java/dev/jbang/devkitman/jdkinstallers/MetadataJdkInstallerTest.java b/src/test/java/dev/jbang/devkitman/jdkinstallers/MetadataJdkInstallerTest.java index aae7d28..c9b5158 100644 --- a/src/test/java/dev/jbang/devkitman/jdkinstallers/MetadataJdkInstallerTest.java +++ b/src/test/java/dev/jbang/devkitman/jdkinstallers/MetadataJdkInstallerTest.java @@ -1,294 +1,294 @@ -package dev.jbang.devkitman.jdkinstallers; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; -import static org.junit.jupiter.api.Assertions.*; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; -import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import dev.jbang.devkitman.BaseTest; -import dev.jbang.devkitman.Jdk; -import dev.jbang.devkitman.JdkManager; -import dev.jbang.devkitman.jdkproviders.JBangJdkProvider; -import dev.jbang.devkitman.util.RemoteAccessProvider; - -public class MetadataJdkInstallerTest extends BaseTest { - - private MetadataJdkInstaller installer; - private JBangJdkProvider provider; - private Path testJdkFile; - - @BeforeEach - @Override - protected void initEnv(@TempDir Path tempPath) throws IOException { - super.initEnv(tempPath); - - // Copy test JDK file for installation tests - testJdkFile = tempPath.resolve("jdk-12.zip"); - Files.copy( - getClass().getResourceAsStream("/jdk-12.zip"), - testJdkFile, - java.nio.file.StandardCopyOption.REPLACE_EXISTING); - - RemoteAccessProvider rap = createRemoteAccessProvider(); - provider = new JBangJdkProvider(config.installPath()); - installer = new MetadataJdkInstaller(provider) - .distro("temurin") - .jvmImpl("hotspot") - .remoteAccessProvider(rap); - provider.installer(installer); - - // Create a manager so the provider has access to defaultJavaVersion - JdkManager manager = JdkManager.builder() - .providers(provider) - .build(); - } - - private RemoteAccessProvider createRemoteAccessProvider() { - return new RemoteAccessProvider() { - @Override - public Path downloadFromUrl(String url) throws IOException { - // Return our test JDK file for any download request - return testJdkFile; - } - - @Override - public T resultFromUrl(String url, Function streamToObject) - throws IOException { - // Verify the URL format matches expected metadata API pattern - if (!url.startsWith(MetadataJdkInstaller.METADATA_BASE_URL)) { - throw new IOException("Unexpected URL: " + url); - } - // Return our test metadata JSON for all requests - return streamToObject.apply( - getClass().getResourceAsStream("/testMetadataInstall.json")); - } - }; - } - - @Test - public void testListAvailable() { - List jdks = installer.listAvailable().collect(Collectors.toList()); - - // Should have JDKs from test data (excluding duplicate major versions and - // filtered EAs) - assertThat(jdks, is(not(empty()))); - - // Verify we have expected major versions (23, 21, 17, 11) - // 24 is EA and should be filtered if 24 GA exists (it doesn't in our test data) - List versions = jdks.stream() - .map(jdk -> Integer.parseInt(jdk.version().split("[.\\-+]")[0])) - .distinct() - .sorted() - .collect(Collectors.toList()); - - assertThat(versions, hasItems(11, 17, 21, 23)); - } - - @Test - public void testListAvailableOrderedByVersionDescending() { - List jdks = installer.listAvailable().collect(Collectors.toList()); - - assertThat(jdks.size(), greaterThan(0)); - - // First JDK should be the highest version (24 EA in our test data) - String firstVersion = jdks.get(0).version(); - int firstMajor = Integer.parseInt(firstVersion.split("[.\\-+]")[0]); - assertThat(firstMajor, is(24)); - } - - @Test - public void testGetAvailableByVersionExact() { - Jdk.AvailableJdk jdk21 = installer.getAvailableByVersion(21, false); - - assertThat(jdk21, is(notNullValue())); - assertThat(jdk21.version(), startsWith("21.")); - assertTrue(jdk21.tags().contains(Jdk.Default.Tags.Ga.name())); - assertTrue(jdk21.tags().contains(Jdk.Default.Tags.Jdk.name())); - } - - @Test - public void testGetAvailableByVersionOpen() { - // Request version 17+, should return newest available (23 in our test data) - Jdk.AvailableJdk jdk = installer.getAvailableByVersion(17, true); - - assertThat(jdk, is(notNullValue())); - String version = jdk.version(); - int major = Integer.parseInt(version.split("[.\\-+]")[0]); - assertThat(major, greaterThanOrEqualTo(17)); - } - - @Test - public void testGetAvailableByVersionNotFound() { - // Request version that doesn't exist in our test data - Jdk.AvailableJdk jdk = installer.getAvailableByVersion(8, false); - - assertThat(jdk, is(nullValue())); - } - - @Test - public void testDetermineIdIncludesDistro() { - List jdks = installer.listAvailable().collect(Collectors.toList()); - - assertThat(jdks, is(not(empty()))); - - // All IDs should contain the vendor name - for (Jdk.AvailableJdk jdk : jdks) { - assertThat(jdk.id(), containsString("temurin")); - } - } - - @Test - public void testDetermineIdForJre() { - // Find the JRE in our test data (11.0.25+9 JRE) - List jdks = installer.listAvailable().collect(Collectors.toList()); - - Jdk.AvailableJdk jre = jdks.stream() - .filter(j -> j.tags().contains(Jdk.Default.Tags.Jre.name())) - .findFirst() - .orElse(null); - - assertThat(jre, is(notNullValue())); - assertThat(jre.id(), containsString("-jre")); - } - - @Test - public void testDetermineTagsForGa() { - Jdk.AvailableJdk jdk17 = installer.getAvailableByVersion(17, false); - - assertThat(jdk17, is(notNullValue())); - Set tags = jdk17.tags(); - - assertThat(tags, hasItem(Jdk.Default.Tags.Ga.name())); - assertThat(tags, hasItem(Jdk.Default.Tags.Jdk.name())); - } - - @Test - public void testDetermineTagsForEa() { - // Our test data has version 24 as EA - List jdks = installer.listAvailable().collect(Collectors.toList()); - - Jdk.AvailableJdk ea = jdks.stream() - .filter(j -> j.version().contains("24")) - .findFirst() - .orElse(null); - - if (ea != null) { - Set tags = ea.tags(); - assertThat(tags, hasItem(Jdk.Default.Tags.Ea.name())); - } - } - - @Test - public void testDetermineTagsForJre() { - List jdks = installer.listAvailable().collect(Collectors.toList()); - - Jdk.AvailableJdk jre = jdks.stream() - .filter(j -> j.tags().contains(Jdk.Default.Tags.Jre.name())) - .findFirst() - .orElse(null); - - assertThat(jre, is(notNullValue())); - assertThat(jre.tags(), hasItem(Jdk.Default.Tags.Jre.name())); - assertThat(jre.tags(), not(hasItem(Jdk.Default.Tags.Jdk.name()))); - } - - @Test - public void testInstallJdk() throws IOException { - Jdk.AvailableJdk jdk17 = installer.getAvailableByVersion(17, false); - assertThat(jdk17, is(notNullValue())); - - Path installDir = config.installPath().resolve("test-jdk-17"); - Files.createDirectories(installDir.getParent()); - - Jdk.InstalledJdk installed = installer.install(jdk17, installDir); - - assertThat(installed, is(notNullValue())); - assertThat(Files.exists(installDir), is(true)); - assertThat(Files.isDirectory(installDir), is(true)); - } - - @Test - public void testInstallJdkInvalidType() { - // Create a mock JDK from a different installer type - Jdk.AvailableJdk mockJdk = new Jdk.AvailableJdk.Default( - provider, - "mock-jdk-21", - "21.0.0", - Set.of(Jdk.Default.Tags.Ga.name())); - - Path installDir = config.installPath().resolve("test-jdk-mock"); - - IllegalArgumentException exception = assertThrows( - IllegalArgumentException.class, - () -> installer.install(mockJdk, installDir)); - - assertThat(exception.getMessage(), containsString("MetadataJdkInstaller can only install")); - } - - @Test - public void testUninstallJdk() throws IOException { - // Create a mock installed JDK - Path jdkPath = config.installPath().resolve("test-jdk-uninstall"); - initMockJdkDir(jdkPath, "17.0.13"); - - Jdk.InstalledJdk jdk = new Jdk.InstalledJdk.Default( - provider, - "17.0.13-temurin-jbang", - jdkPath, - "17.0.13", - Set.of(Jdk.Default.Tags.Ga.name())); - - assertThat(Files.exists(jdkPath), is(true)); - - installer.uninstall(jdk); - - // JDK directory should be removed - assertThat(Files.exists(jdkPath), is(false)); - } - - @Test - public void testFilterEAWhenGAExists() { - // In our test data, if we had both EA and GA for same version, - // EA should be filtered out - List jdks = installer.listAvailable().collect(Collectors.toList()); - - // Count how many times each major version appears - List majorVersions = jdks.stream() - .map(jdk -> Integer.parseInt(jdk.version().split("[.\\-+]")[0])) - .collect(Collectors.toList()); - - // Each major version should appear at most once (after filtering) - long uniqueVersions = majorVersions.stream().distinct().count(); - assertThat((long) majorVersions.size(), greaterThanOrEqualTo(uniqueVersions)); - } - - @Test - public void testAvailableMetadataJdkCreation() { - List jdks = installer.listAvailable().collect(Collectors.toList()); - - assertThat(jdks, is(not(empty()))); - - // Verify that the JDKs are of the correct type - for (Jdk.AvailableJdk jdk : jdks) { - assertThat(jdk, is(instanceOf(MetadataJdkInstaller.AvailableMetadataJdk.class))); - - // Verify essential properties - assertThat(jdk.id(), is(not(emptyString()))); - assertThat(jdk.version(), is(not(emptyString()))); - assertThat(jdk.tags(), is(not(empty()))); - assertThat(jdk.provider(), is(provider)); - } - } -} +package dev.jbang.devkitman.jdkinstallers; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import dev.jbang.devkitman.BaseTest; +import dev.jbang.devkitman.Jdk; +import dev.jbang.devkitman.JdkManager; +import dev.jbang.devkitman.jdkproviders.JBangJdkProvider; +import dev.jbang.devkitman.util.FunctionWithError; +import dev.jbang.devkitman.util.RemoteAccessProvider; + +public class MetadataJdkInstallerTest extends BaseTest { + + private MetadataJdkInstaller installer; + private JBangJdkProvider provider; + private Path testJdkFile; + + @BeforeEach + @Override + protected void initEnv(@TempDir Path tempPath) throws IOException { + super.initEnv(tempPath); + + // Copy test JDK file for installation tests + testJdkFile = tempPath.resolve("jdk-12.zip"); + Files.copy( + getClass().getResourceAsStream("/jdk-12.zip"), + testJdkFile, + java.nio.file.StandardCopyOption.REPLACE_EXISTING); + + RemoteAccessProvider rap = createRemoteAccessProvider(); + provider = new JBangJdkProvider(config.installPath()); + installer = new MetadataJdkInstaller(provider) + .distro("temurin") + .jvmImpl("hotspot") + .remoteAccessProvider(rap); + provider.installer(installer); + + // Create a manager so the provider has access to defaultJavaVersion + JdkManager manager = JdkManager.builder() + .providers(provider) + .build(); + } + + private RemoteAccessProvider createRemoteAccessProvider() { + return new RemoteAccessProvider() { + @Override + public Path downloadFromUrl(String url) throws IOException { + // Return our test JDK file for any download request + return testJdkFile; + } + + @Override + public T resultFromUrl(String url, FunctionWithError streamToObject) + throws IOException { + // Verify the URL format matches expected metadata API pattern + if (!url.startsWith(MetadataJdkInstaller.METADATA_BASE_URL)) { + throw new IOException("Unexpected URL: " + url); + } + // Return our test metadata JSON for all requests + return streamToObject.apply( + getClass().getResourceAsStream("/testMetadataInstall.json")); + } + }; + } + + @Test + public void testListAvailable() { + List jdks = installer.listAvailable().collect(Collectors.toList()); + + // Should have JDKs from test data (excluding duplicate major versions and + // filtered EAs) + assertThat(jdks, is(not(empty()))); + + // Verify we have expected major versions (23, 21, 17, 11) + // 24 is EA and should be filtered if 24 GA exists (it doesn't in our test data) + List versions = jdks.stream() + .map(jdk -> Integer.parseInt(jdk.version().split("[.\\-+]")[0])) + .distinct() + .sorted() + .collect(Collectors.toList()); + + assertThat(versions, hasItems(11, 17, 21, 23)); + } + + @Test + public void testListAvailableOrderedByVersionDescending() { + List jdks = installer.listAvailable().collect(Collectors.toList()); + + assertThat(jdks.size(), greaterThan(0)); + + // First JDK should be the highest version (24 EA in our test data) + String firstVersion = jdks.get(0).version(); + int firstMajor = Integer.parseInt(firstVersion.split("[.\\-+]")[0]); + assertThat(firstMajor, is(24)); + } + + @Test + public void testGetAvailableByVersionExact() { + Jdk.AvailableJdk jdk21 = installer.getAvailableByVersion(21, false); + + assertThat(jdk21, is(notNullValue())); + assertThat(jdk21.version(), startsWith("21.")); + assertTrue(jdk21.tags().contains(Jdk.Default.Tags.Ga.name())); + assertTrue(jdk21.tags().contains(Jdk.Default.Tags.Jdk.name())); + } + + @Test + public void testGetAvailableByVersionOpen() { + // Request version 17+, should return newest available (23 in our test data) + Jdk.AvailableJdk jdk = installer.getAvailableByVersion(17, true); + + assertThat(jdk, is(notNullValue())); + String version = jdk.version(); + int major = Integer.parseInt(version.split("[.\\-+]")[0]); + assertThat(major, greaterThanOrEqualTo(17)); + } + + @Test + public void testGetAvailableByVersionNotFound() { + // Request version that doesn't exist in our test data + Jdk.AvailableJdk jdk = installer.getAvailableByVersion(8, false); + + assertThat(jdk, is(nullValue())); + } + + @Test + public void testDetermineIdIncludesDistro() { + List jdks = installer.listAvailable().collect(Collectors.toList()); + + assertThat(jdks, is(not(empty()))); + + // All IDs should contain the vendor name + for (Jdk.AvailableJdk jdk : jdks) { + assertThat(jdk.id(), containsString("temurin")); + } + } + + @Test + public void testDetermineIdForJre() { + // Find the JRE in our test data (11.0.25+9 JRE) + List jdks = installer.listAvailable().collect(Collectors.toList()); + + Jdk.AvailableJdk jre = jdks.stream() + .filter(j -> j.tags().contains(Jdk.Default.Tags.Jre.name())) + .findFirst() + .orElse(null); + + assertThat(jre, is(notNullValue())); + assertThat(jre.id(), containsString("-jre")); + } + + @Test + public void testDetermineTagsForGa() { + Jdk.AvailableJdk jdk17 = installer.getAvailableByVersion(17, false); + + assertThat(jdk17, is(notNullValue())); + Set tags = jdk17.tags(); + + assertThat(tags, hasItem(Jdk.Default.Tags.Ga.name())); + assertThat(tags, hasItem(Jdk.Default.Tags.Jdk.name())); + } + + @Test + public void testDetermineTagsForEa() { + // Our test data has version 24 as EA + List jdks = installer.listAvailable().collect(Collectors.toList()); + + Jdk.AvailableJdk ea = jdks.stream() + .filter(j -> j.version().contains("24")) + .findFirst() + .orElse(null); + + if (ea != null) { + Set tags = ea.tags(); + assertThat(tags, hasItem(Jdk.Default.Tags.Ea.name())); + } + } + + @Test + public void testDetermineTagsForJre() { + List jdks = installer.listAvailable().collect(Collectors.toList()); + + Jdk.AvailableJdk jre = jdks.stream() + .filter(j -> j.tags().contains(Jdk.Default.Tags.Jre.name())) + .findFirst() + .orElse(null); + + assertThat(jre, is(notNullValue())); + assertThat(jre.tags(), hasItem(Jdk.Default.Tags.Jre.name())); + assertThat(jre.tags(), not(hasItem(Jdk.Default.Tags.Jdk.name()))); + } + + @Test + public void testInstallJdk() throws IOException { + Jdk.AvailableJdk jdk17 = installer.getAvailableByVersion(17, false); + assertThat(jdk17, is(notNullValue())); + + Path installDir = config.installPath().resolve("test-jdk-17"); + Files.createDirectories(installDir.getParent()); + + Jdk.InstalledJdk installed = installer.install(jdk17, installDir); + + assertThat(installed, is(notNullValue())); + assertThat(Files.exists(installDir), is(true)); + assertThat(Files.isDirectory(installDir), is(true)); + } + + @Test + public void testInstallJdkInvalidType() { + // Create a mock JDK from a different installer type + Jdk.AvailableJdk mockJdk = new Jdk.AvailableJdk.Default( + provider, + "mock-jdk-21", + "21.0.0", + Set.of(Jdk.Default.Tags.Ga.name())); + + Path installDir = config.installPath().resolve("test-jdk-mock"); + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> installer.install(mockJdk, installDir)); + + assertThat(exception.getMessage(), containsString("MetadataJdkInstaller can only install")); + } + + @Test + public void testUninstallJdk() throws IOException { + // Create a mock installed JDK + Path jdkPath = config.installPath().resolve("test-jdk-uninstall"); + initMockJdkDir(jdkPath, "17.0.13"); + + Jdk.InstalledJdk jdk = new Jdk.InstalledJdk.Default( + provider, + "17.0.13-temurin-jbang", + jdkPath, + "17.0.13", + Set.of(Jdk.Default.Tags.Ga.name())); + + assertThat(Files.exists(jdkPath), is(true)); + + installer.uninstall(jdk); + + // JDK directory should be removed + assertThat(Files.exists(jdkPath), is(false)); + } + + @Test + public void testFilterEAWhenGAExists() { + // In our test data, if we had both EA and GA for same version, + // EA should be filtered out + List jdks = installer.listAvailable().collect(Collectors.toList()); + + // Count how many times each major version appears + List majorVersions = jdks.stream() + .map(jdk -> Integer.parseInt(jdk.version().split("[.\\-+]")[0])) + .collect(Collectors.toList()); + + // Each major version should appear at most once (after filtering) + long uniqueVersions = majorVersions.stream().distinct().count(); + assertThat((long) majorVersions.size(), greaterThanOrEqualTo(uniqueVersions)); + } + + @Test + public void testAvailableMetadataJdkCreation() { + List jdks = installer.listAvailable().collect(Collectors.toList()); + + assertThat(jdks, is(not(empty()))); + + // Verify that the JDKs are of the correct type + for (Jdk.AvailableJdk jdk : jdks) { + assertThat(jdk, is(instanceOf(MetadataJdkInstaller.AvailableMetadataJdk.class))); + + // Verify essential properties + assertThat(jdk.id(), is(not(emptyString()))); + assertThat(jdk.version(), is(not(emptyString()))); + assertThat(jdk.tags(), is(not(empty()))); + assertThat(jdk.provider(), is(provider)); + } + } +} diff --git a/src/test/java/dev/jbang/devkitman/jdkproviders/SdkmanJdkProviderTest.java b/src/test/java/dev/jbang/devkitman/jdkproviders/SdkmanJdkProviderTest.java index 7cb9e42..c825148 100644 --- a/src/test/java/dev/jbang/devkitman/jdkproviders/SdkmanJdkProviderTest.java +++ b/src/test/java/dev/jbang/devkitman/jdkproviders/SdkmanJdkProviderTest.java @@ -1,68 +1,68 @@ -package dev.jbang.devkitman.jdkproviders; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.hasSize; -import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.Matchers.notNullValue; - -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.List; -import java.util.stream.Collectors; - -import org.hamcrest.Matchers; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; - -import dev.jbang.devkitman.BaseTest; -import dev.jbang.devkitman.Jdk; -import dev.jbang.devkitman.util.FileUtils; - -public class SdkmanJdkProviderTest extends BaseTest { - @ParameterizedTest - @CsvSource({ - "25.0.1-tem,25.0.1,25+", - "21-tem,21.0.7,21+", - "25.0.2-zulu,25.0.2,25+", - "25.0.2.fx-zulu,25.0.2,25+", - "21.0.10-librca,21.0.10,21+", - "25-graal,25,25+", - "26.ea.13-graal,26.ea.13,26+", - "22.1.0.1.r17-gln,22.1.0.1.r17,22+" - }) - void testSdkmanProviderFindsInstalledJdkByVersionPattern( - String folderName, String releaseVersion, String requestedVersion) { - Path jdkHome = installSdkmanJdk(folderName, releaseVersion); - Jdk.InstalledJdk jdk = jdkManager("sdkman").getInstalledJdk(requestedVersion); - - assertThat(jdk, notNullValue()); - assertThat(jdk.provider(), instanceOf(SdkmanJdkProvider.class)); - assertThat(jdk.home(), Matchers.is(jdkHome)); - assertThat(jdk.id(), Matchers.is(folderName)); - } - - @Test - void testSdkmanProviderIgnoresCurrentSymlink() { - installSdkmanJdk("25.0.1-tem", "25.0.1"); - Path jdkHome = sdkmanJdksRoot().resolve("25.0.1-tem"); - FileUtils.createLink(sdkmanJdksRoot().resolve("current"), jdkHome); - - List ids = jdkManager("sdkman").listInstalledJdks() - .stream() - .map(Jdk::id) - .collect(Collectors.toList()); - assertThat(ids, hasSize(1)); - assertThat(ids, Matchers.contains("25.0.1-tem")); - } - - private Path sdkmanJdksRoot() { - return Paths.get(System.getProperty("user.home")).resolve(".sdkman/candidates/java"); - } - - private Path installSdkmanJdk(String folderName, String releaseVersion) { - Path jdkHome = sdkmanJdksRoot().resolve(folderName); - initMockJdkDir(jdkHome, releaseVersion); - return jdkHome; - } -} +package dev.jbang.devkitman.jdkproviders; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.notNullValue; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Collectors; + +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import dev.jbang.devkitman.BaseTest; +import dev.jbang.devkitman.Jdk; +import dev.jbang.devkitman.util.FileUtils; + +public class SdkmanJdkProviderTest extends BaseTest { + @ParameterizedTest + @CsvSource({ + "25.0.1-tem,25.0.1,25+", + "21-tem,21.0.7,21+", + "25.0.2-zulu,25.0.2,25+", + "25.0.2.fx-zulu,25.0.2,25+", + "21.0.10-librca,21.0.10,21+", + "25-graal,25,25+", + "26.ea.13-graal,26.ea.13,26+", + "22.1.0.1.r17-gln,22.1.0.1.r17,22+" + }) + void testSdkmanProviderFindsInstalledJdkByVersionPattern( + String folderName, String releaseVersion, String requestedVersion) { + Path jdkHome = installSdkmanJdk(folderName, releaseVersion); + Jdk.InstalledJdk jdk = jdkManager("sdkman").getInstalledJdk(requestedVersion); + + assertThat(jdk, notNullValue()); + assertThat(jdk.provider(), instanceOf(SdkmanJdkProvider.class)); + assertThat(jdk.home(), Matchers.is(jdkHome)); + assertThat(jdk.id(), Matchers.is(folderName)); + } + + @Test + void testSdkmanProviderIgnoresCurrentSymlink() { + installSdkmanJdk("25.0.1-tem", "25.0.1"); + Path jdkHome = sdkmanJdksRoot().resolve("25.0.1-tem"); + FileUtils.createLink(sdkmanJdksRoot().resolve("current"), jdkHome); + + List ids = jdkManager("sdkman").listInstalledJdks() + .stream() + .map(Jdk::id) + .collect(Collectors.toList()); + assertThat(ids, hasSize(1)); + assertThat(ids, Matchers.contains("25.0.1-tem")); + } + + private Path sdkmanJdksRoot() { + return Paths.get(System.getProperty("user.home")).resolve(".sdkman/candidates/java"); + } + + private Path installSdkmanJdk(String folderName, String releaseVersion) { + Path jdkHome = sdkmanJdksRoot().resolve(folderName); + initMockJdkDir(jdkHome, releaseVersion); + return jdkHome; + } +} diff --git a/src/test/java/dev/jbang/devkitman/util/TestRemoteAccessProvider.java b/src/test/java/dev/jbang/devkitman/util/TestRemoteAccessProvider.java new file mode 100644 index 0000000..ada8d24 --- /dev/null +++ b/src/test/java/dev/jbang/devkitman/util/TestRemoteAccessProvider.java @@ -0,0 +1,35 @@ +package dev.jbang.devkitman.util; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import dev.jbang.devkitman.BaseTest; + +public class TestRemoteAccessProvider extends BaseTest { + + @Test + void testDefaultReadJsonFromUrl(@TempDir Path cacheDir) throws IOException { + RemoteAccessProvider rap = RemoteAccessProvider.createDefaultRemoteAccessProvider(cacheDir); + String url = "https://raw.githubusercontent.com/jbangdev/jbang-devkitman/refs/heads/main/renovate.json"; + Object json = RemoteAccessProvider.readJsonFromUrl(rap, url, Object.class); + assertThat(json, instanceOf(Map.class)); + } + + @Test + void testDefaultCache(@TempDir Path cacheDir) throws IOException { + RemoteAccessProvider rap = RemoteAccessProvider.createDefaultRemoteAccessProvider(cacheDir); + String url = "https://raw.githubusercontent.com/jbangdev/jbang-devkitman/refs/heads/main/renovate.json"; + Object json = RemoteAccessProvider.readJsonFromUrl(rap, url, Object.class); + assertThat(json, instanceOf(Map.class)); + assertThat(Files.exists(cacheDir), is(true)); + assertThat(Files.list(cacheDir).count(), greaterThan(0L)); + } +} From be341774fb49ff856fbf0cb0fedf929376779d92 Mon Sep 17 00:00:00 2001 From: Tako Schotanus Date: Wed, 4 Mar 2026 12:40:23 +0100 Subject: [PATCH 2/3] build: make the http client dependency optional --- build.gradle | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 8ac8968..da8196d 100644 --- a/build.gradle +++ b/build.gradle @@ -55,11 +55,16 @@ versioner { dependencies { implementation 'org.apache.commons:commons-compress:1.28.0' - implementation 'org.apache.httpcomponents.client5:httpclient5:5.6' - implementation 'org.apache.httpcomponents.client5:httpclient5-cache:5.6' implementation 'com.google.code.gson:gson:2.13.2' implementation 'org.jspecify:jspecify:1.0.0' + implementation('org.apache.httpcomponents.client5:httpclient5:5.6') { + // Optional: Only needed if using DefaultRemoteAccessProvider + } + implementation('org.apache.httpcomponents.client5:httpclient5-cache:5.6') { + // Optional: Only needed if using DefaultRemoteAccessProvider + } + testImplementation platform('org.junit:junit-bom:6.0.1') testImplementation 'org.junit.jupiter:junit-jupiter' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' @@ -135,6 +140,19 @@ publishing { developerConnection = 'scm:git:https://github.com/jbangdev/jbang-devkitman' url = 'http://github.com/jbangdev/jbang-devkitman' } + + // Mark Apache HttpClient dependencies as optional + withXml { + def dependenciesNode = asNode().dependencies[0] + dependenciesNode.dependency.each { dep -> + def groupId = dep.groupId[0].text() + def artifactId = dep.artifactId[0].text() + if (groupId == 'org.apache.httpcomponents.client5' && + (artifactId == 'httpclient5' || artifactId == 'httpclient5-cache')) { + dep.appendNode('optional', true) + } + } + } } } } From d8ec8ecd70923850e3ffed2381c61a741967924e Mon Sep 17 00:00:00 2001 From: Tako Schotanus Date: Wed, 4 Mar 2026 14:15:26 +0100 Subject: [PATCH 3/3] chore: line endings --- build.gradle | 338 +++---- .../jdkinstallers/FoojayJdkInstaller.java | 734 +++++++-------- .../jdkinstallers/MetadataJdkInstaller.java | 880 +++++++++--------- .../jdkproviders/SdkmanJdkProvider.java | 100 +- .../devkitman/util/FileHttpCacheStorage.java | 210 ++--- .../devkitman/util/FunctionWithError.java | 30 +- .../dev/jbang/devkitman/util/NetUtils.java | 284 +++--- .../devkitman/util/RemoteAccessProvider.java | 158 ++-- .../java/dev/jbang/devkitman/BaseTest.java | 498 +++++----- .../jdkinstallers/FoojayJdkInstallerTest.java | 698 +++++++------- .../MetadataJdkInstallerTest.java | 588 ++++++------ .../jdkproviders/SdkmanJdkProviderTest.java | 136 +-- .../util/TestRemoteAccessProvider.java | 70 +- 13 files changed, 2362 insertions(+), 2362 deletions(-) diff --git a/build.gradle b/build.gradle index da8196d..d0a2558 100644 --- a/build.gradle +++ b/build.gradle @@ -1,169 +1,169 @@ -plugins { - id 'java-library' - id 'io.toolebox.git-versioner' version '1.6.7' - id 'com.diffplug.spotless' version '7.2.1' - id 'maven-publish' - id 'org.jreleaser' version '1.21.0' -} - -group = 'dev.jbang' - -def javaVersion = System.getProperty('java.version') -def majorVersion = javaVersion.split('\\.')[0].toInteger() -if (majorVersion < 11) { - throw new GradleException(""" - ⚠️ This build requires Java 11 or newer but you're using Java ${javaVersion} - Please use JAVA_HOME with Java 11 or newer to run this build. - Current JAVA_HOME: ${System.getProperty('java.home')}. - If you have jbang installed, you can run it with: - eval \$(jbang jdk java-env 11+) - """.stripIndent()) -} - -javadoc { - options.encoding = 'UTF-8' - //remove this to see all the missing tags/parameters. - options.addStringOption('Xdoclint:none', '-quiet') -} - -repositories { - mavenCentral() -} - -java { - withJavadocJar() - withSourcesJar() -} - -compileJava { - options.encoding = 'UTF-8' - options.release = 8; -} - -versioner { - pattern { - pattern = "%M.%m.%p(.%c-SNAPSHOT)" - } - git { - authentication { - https { - token = project.hasProperty('github_token') ? getProperty('github_token') : "unknown_github_token" - } - } - } -} - -dependencies { - implementation 'org.apache.commons:commons-compress:1.28.0' - implementation 'com.google.code.gson:gson:2.13.2' - implementation 'org.jspecify:jspecify:1.0.0' - - implementation('org.apache.httpcomponents.client5:httpclient5:5.6') { - // Optional: Only needed if using DefaultRemoteAccessProvider - } - implementation('org.apache.httpcomponents.client5:httpclient5-cache:5.6') { - // Optional: Only needed if using DefaultRemoteAccessProvider - } - - testImplementation platform('org.junit:junit-bom:6.0.1') - testImplementation 'org.junit.jupiter:junit-jupiter' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - testImplementation "org.hamcrest:hamcrest-library:3.0" - testImplementation "uk.org.webcompere:system-stubs-jupiter:2.1.8" - testImplementation "uk.org.webcompere:system-stubs-core:2.1.8" -} - -tasks.withType(AbstractArchiveTask) { - preserveFileTimestamps = false - reproducibleFileOrder = true -} - -spotless { - format 'misc', { - target '**/*.gradle', '**/*.md', '**/.gitignore' - targetExclude 'build/**/*', 'out/**/*' - trimTrailingWhitespace() - leadingSpacesToTabs() - endWithNewline() - } - java { - importOrder 'java', 'javax', 'org', 'com', 'dev.jbang', '' - removeUnusedImports() - eclipse().configFile "misc/eclipse_formatting_nowrap.xml" - targetExclude 'build/**/*' - } - format 'xml', { - targetExclude 'build/test-results', fileTree('.idea') - target '**/*.xml', '**/*.nuspec' - } -} - -test { - useJUnitPlatform() - jvmArgs = [ - "--add-opens", "java.base/java.lang=ALL-UNNAMED", - "--add-opens", "java.base/java.util=ALL-UNNAMED" - ] -} - -publishing { - publications { - maven(MavenPublication) { - groupId = 'dev.jbang' - artifactId = 'devkitman' - - from components.java - - pom { - name = 'JBang JDK Manager' - description = 'Library for managing JDK installations' - url = 'https://github.com/jbangdev/jbang-devkitman' - inceptionYear = '2025' - licenses { - license { - name = 'MIT' - url = 'https://github.com/jbangdev/jbang-devkitman/blob/main/LICENSE' - } - } - developers { - developer { - id = 'maxandersen' - name = 'Max Rydahl Andersen' - } - developer { - id = 'quintesse' - name = 'Tako Schotanus' - } - } - scm { - connection = 'scm:git:https://github.com/jbangdev/jbang-devkitman' - developerConnection = 'scm:git:https://github.com/jbangdev/jbang-devkitman' - url = 'http://github.com/jbangdev/jbang-devkitman' - } - - // Mark Apache HttpClient dependencies as optional - withXml { - def dependenciesNode = asNode().dependencies[0] - dependenciesNode.dependency.each { dep -> - def groupId = dep.groupId[0].text() - def artifactId = dep.artifactId[0].text() - if (groupId == 'org.apache.httpcomponents.client5' && - (artifactId == 'httpclient5' || artifactId == 'httpclient5-cache')) { - dep.appendNode('optional', true) - } - } - } - } - } - } - - repositories { - maven { - url = layout.buildDirectory.dir('staging-deploy') - } - } -} - -jreleaser { - configFile = file('jreleaser.yml') -} +plugins { + id 'java-library' + id 'io.toolebox.git-versioner' version '1.6.7' + id 'com.diffplug.spotless' version '7.2.1' + id 'maven-publish' + id 'org.jreleaser' version '1.21.0' +} + +group = 'dev.jbang' + +def javaVersion = System.getProperty('java.version') +def majorVersion = javaVersion.split('\\.')[0].toInteger() +if (majorVersion < 11) { + throw new GradleException(""" + ⚠️ This build requires Java 11 or newer but you're using Java ${javaVersion} + Please use JAVA_HOME with Java 11 or newer to run this build. + Current JAVA_HOME: ${System.getProperty('java.home')}. + If you have jbang installed, you can run it with: + eval \$(jbang jdk java-env 11+) + """.stripIndent()) +} + +javadoc { + options.encoding = 'UTF-8' + //remove this to see all the missing tags/parameters. + options.addStringOption('Xdoclint:none', '-quiet') +} + +repositories { + mavenCentral() +} + +java { + withJavadocJar() + withSourcesJar() +} + +compileJava { + options.encoding = 'UTF-8' + options.release = 8; +} + +versioner { + pattern { + pattern = "%M.%m.%p(.%c-SNAPSHOT)" + } + git { + authentication { + https { + token = project.hasProperty('github_token') ? getProperty('github_token') : "unknown_github_token" + } + } + } +} + +dependencies { + implementation 'org.apache.commons:commons-compress:1.28.0' + implementation 'com.google.code.gson:gson:2.13.2' + implementation 'org.jspecify:jspecify:1.0.0' + + implementation('org.apache.httpcomponents.client5:httpclient5:5.6') { + // Optional: Only needed if using DefaultRemoteAccessProvider + } + implementation('org.apache.httpcomponents.client5:httpclient5-cache:5.6') { + // Optional: Only needed if using DefaultRemoteAccessProvider + } + + testImplementation platform('org.junit:junit-bom:6.0.1') + testImplementation 'org.junit.jupiter:junit-jupiter' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testImplementation "org.hamcrest:hamcrest-library:3.0" + testImplementation "uk.org.webcompere:system-stubs-jupiter:2.1.8" + testImplementation "uk.org.webcompere:system-stubs-core:2.1.8" +} + +tasks.withType(AbstractArchiveTask) { + preserveFileTimestamps = false + reproducibleFileOrder = true +} + +spotless { + format 'misc', { + target '**/*.gradle', '**/*.md', '**/.gitignore' + targetExclude 'build/**/*', 'out/**/*' + trimTrailingWhitespace() + leadingSpacesToTabs() + endWithNewline() + } + java { + importOrder 'java', 'javax', 'org', 'com', 'dev.jbang', '' + removeUnusedImports() + eclipse().configFile "misc/eclipse_formatting_nowrap.xml" + targetExclude 'build/**/*' + } + format 'xml', { + targetExclude 'build/test-results', fileTree('.idea') + target '**/*.xml', '**/*.nuspec' + } +} + +test { + useJUnitPlatform() + jvmArgs = [ + "--add-opens", "java.base/java.lang=ALL-UNNAMED", + "--add-opens", "java.base/java.util=ALL-UNNAMED" + ] +} + +publishing { + publications { + maven(MavenPublication) { + groupId = 'dev.jbang' + artifactId = 'devkitman' + + from components.java + + pom { + name = 'JBang JDK Manager' + description = 'Library for managing JDK installations' + url = 'https://github.com/jbangdev/jbang-devkitman' + inceptionYear = '2025' + licenses { + license { + name = 'MIT' + url = 'https://github.com/jbangdev/jbang-devkitman/blob/main/LICENSE' + } + } + developers { + developer { + id = 'maxandersen' + name = 'Max Rydahl Andersen' + } + developer { + id = 'quintesse' + name = 'Tako Schotanus' + } + } + scm { + connection = 'scm:git:https://github.com/jbangdev/jbang-devkitman' + developerConnection = 'scm:git:https://github.com/jbangdev/jbang-devkitman' + url = 'http://github.com/jbangdev/jbang-devkitman' + } + + // Mark Apache HttpClient dependencies as optional + withXml { + def dependenciesNode = asNode().dependencies[0] + dependenciesNode.dependency.each { dep -> + def groupId = dep.groupId[0].text() + def artifactId = dep.artifactId[0].text() + if (groupId == 'org.apache.httpcomponents.client5' && + (artifactId == 'httpclient5' || artifactId == 'httpclient5-cache')) { + dep.appendNode('optional', true) + } + } + } + } + } + } + + repositories { + maven { + url = layout.buildDirectory.dir('staging-deploy') + } + } +} + +jreleaser { + configFile = file('jreleaser.yml') +} diff --git a/src/main/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstaller.java b/src/main/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstaller.java index 17fb46e..58f6e4b 100644 --- a/src/main/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstaller.java +++ b/src/main/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstaller.java @@ -1,367 +1,367 @@ -package dev.jbang.devkitman.jdkinstallers; - -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.nio.file.Path; -import java.util.*; -import java.util.function.Function; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; -import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; - -import dev.jbang.devkitman.Jdk; -import dev.jbang.devkitman.JdkInstaller; -import dev.jbang.devkitman.JdkInstallers; -import dev.jbang.devkitman.JdkProvider; -import dev.jbang.devkitman.util.*; - -/** - * JVM's main JDK installer that can download and install the JDKs provided by - * the Foojay Disco API. - */ -public class FoojayJdkInstaller implements JdkInstaller { - protected final JdkProvider jdkProvider; - protected final Function jdkId; - protected RemoteAccessProvider remoteAccessProvider; - protected String distro = DEFAULT_DISTRO; - - public static final String FOOJAY_JDK_VERSIONS_URL = "https://api.foojay.io/disco/v3.0/packages?"; - - public static final String DEFAULT_DISTRO = "temurin,aoj"; - - private static final Logger LOGGER = Logger.getLogger(FoojayJdkInstaller.class.getName()); - - public static class JdkResultLinks { - public String pkg_download_redirect; - } - - public static class JdkResult { - public String java_version; - public int major_version; - public String distribution; // temurin, aoj, liberica, zulu, etc. - public String release_status; // ga, ea - public String package_type; // jdk, jre - public boolean javafx_bundled; - public JdkResultLinks links; - } - - public static class VersionsResponse { - public List result; - } - - public FoojayJdkInstaller(@NonNull JdkProvider jdkProvider) { - this.jdkProvider = jdkProvider; - this.jdkId = jdk -> determineId(jdk) + "-" + jdkProvider.name(); - } - - public FoojayJdkInstaller(@NonNull JdkProvider jdkProvider, @NonNull Function jdkId) { - this.jdkProvider = jdkProvider; - this.jdkId = jdkId; - } - - protected @NonNull RemoteAccessProvider remoteAccessProvider() { - if (remoteAccessProvider == null) { - remoteAccessProvider = RemoteAccessProvider.createDefaultRemoteAccessProvider(); - } - return remoteAccessProvider; - } - - public @NonNull FoojayJdkInstaller remoteAccessProvider(@NonNull RemoteAccessProvider remoteAccessProvider) { - this.remoteAccessProvider = remoteAccessProvider; - return this; - } - - public @NonNull FoojayJdkInstaller distro(String distro) { - this.distro = distro != null && !distro.isEmpty() ? distro : DEFAULT_DISTRO; - return this; - } - - @NonNull - @Override - public Stream listAvailable() { - try { - VersionsResponse res = readPackagesForList(); - return processPackages(res.result, majorVersionSort()).distinct(); - } catch (IOException e) { - LOGGER.log(Level.FINE, "Couldn't list available JDKs", e); - return Stream.empty(); - } - } - - private VersionsResponse readPackagesForList() throws IOException { - return readJsonFromUrl( - getVersionsUrl(0, true, OsUtils.getOS(), OsUtils.getArch(), distro, "ga,ea")); - } - - @Override - public Jdk.@Nullable AvailableJdk getAvailableByVersion(int version, boolean openVersion) { - int djv = jdkProvider.manager().defaultJavaVersion; - Comparator preferGaSort = (j1, j2) -> { - // Prefer versions equal to the default Java version - if (j1.major_version == djv && j2.major_version != djv) { - return -1; - } else if (j2.major_version == djv && j1.major_version != djv) { - return 1; - } - // Prefer GA releases over EA releases - if (!j1.release_status.equals(j2.release_status)) { - return j2.release_status.compareTo(j1.release_status); - } - // Prefer newer versions - return majorVersionSort().compare(j1, j2); - }; - try { - VersionsResponse res = readPackagesForVersion(version, openVersion); - return processPackages(res.result, preferGaSort) - .filter(Jdk.Predicates.forVersion(version, openVersion)) - .findFirst() - .orElse(null); - } catch (IOException e) { - LOGGER.log(Level.FINE, "Couldn't get available JDK by version", e); - return null; - } - } - - private VersionsResponse readPackagesForVersion(Integer minVersion, boolean openVersion) throws IOException { - VersionsResponse res = readJsonFromUrl( - getVersionsUrl(minVersion, openVersion, OsUtils.getOS(), OsUtils.getArch(), distro, "ga")); - if (res.result.isEmpty()) { - res = readJsonFromUrl( - getVersionsUrl(minVersion, openVersion, OsUtils.getOS(), OsUtils.getArch(), distro, "ea")); - } - return res; - } - - private Stream processPackages(List jdks, Comparator sortFunc) { - return filterEA(jdks) - .stream() - .sorted(sortFunc) - .map(jdk -> new AvailableFoojayJdk(jdkProvider, - jdkId.apply(jdk), jdk.java_version, - jdk.links.pkg_download_redirect, determineTags(jdk))); - } - - private @NonNull String determineId(@NonNull JdkResult jdk) { - String id = jdk.java_version + "-" + jdk.distribution; - if (Jdk.Default.Tags.Jre.name().equals(jdk.package_type)) { - id += "-jre"; - } - if (jdk.javafx_bundled) { - id += "-jfx"; - } - return id; - } - - private @NonNull Set determineTags(JdkResult jdk) { - Set tags = new HashSet<>(); - if (Jdk.Default.Tags.Ga.name().equalsIgnoreCase(jdk.release_status)) { - tags.add(Jdk.Default.Tags.Ga.name()); - } else if (Jdk.Default.Tags.Ea.name().equalsIgnoreCase(jdk.release_status)) { - tags.add(Jdk.Default.Tags.Ea.name()); - } - if (Jdk.Default.Tags.Jdk.name().equalsIgnoreCase(jdk.package_type)) { - tags.add(Jdk.Default.Tags.Jdk.name()); - } else if (Jdk.Default.Tags.Jre.name().equalsIgnoreCase(jdk.package_type)) { - tags.add(Jdk.Default.Tags.Jre.name()); - } - if (jdk.javafx_bundled) { - tags.add(Jdk.Default.Tags.Javafx.name()); - } - return tags; - } - - private VersionsResponse readJsonFromUrl(String url) throws IOException { - return RemoteAccessProvider.readJsonFromUrl(remoteAccessProvider(), url, VersionsResponse.class); - } - - // Filter out any EA releases for which a GA with - // the same major version exists - private List filterEA(List jdks) { - Set GAs = jdks.stream() - .filter(jdk -> jdk.release_status.equals("ga")) - .map(jdk -> jdk.major_version) - .collect(Collectors.toSet()); - - JdkResult[] lastJdk = new JdkResult[] { null }; - return jdks.stream() - .filter( - jdk -> { - if (lastJdk[0] == null - || lastJdk[0].major_version != jdk.major_version - && (jdk.release_status.equals("ga") - || !GAs.contains(jdk.major_version))) { - lastJdk[0] = jdk; - return true; - } else { - return false; - } - }) - .collect(Collectors.toList()); - } - - private static final Comparator jdkResultVersionComparator = (o1, o2) -> VersionComparator.INSTANCE - .compare(o1.java_version, o2.java_version); - - private Comparator majorVersionSort() { - List ds = Arrays.asList(distro.split(",")); - Comparator jdkResultDistroComparator = Comparator.comparingInt(o -> ds.indexOf(o.distribution)); - return Comparator - .comparingInt((JdkResult jdk) -> -jdk.major_version) - .thenComparing(jdkResultDistroComparator) - .thenComparing(jdkResultVersionComparator.reversed()); - } - - @Override - public Jdk.@NonNull InstalledJdk install(Jdk.@NonNull AvailableJdk jdk, @NonNull Path jdkDir) { - if (!(jdk instanceof AvailableFoojayJdk)) { - throw new IllegalArgumentException( - "FoojayJdkInstaller can only install JDKs listed as available by itself"); - } - AvailableFoojayJdk foojayJdk = (AvailableFoojayJdk) jdk; - int version = jdkVersion(foojayJdk.id()); - LOGGER.log( - Level.INFO, - "Downloading JDK {0}. Be patient, this can take several minutes...", - version); - String url = foojayJdk.downloadUrl; - - try { - LOGGER.log(Level.FINE, "Downloading {0}", url); - Path jdkPkg = remoteAccessProvider().downloadFromUrl(url); - - LOGGER.log(Level.INFO, "Installing JDK {0}...", version); - JavaUtils.installJdk(jdkPkg, jdkDir); - - Jdk.InstalledJdk newJdk = jdkProvider.createJdk(foojayJdk.id(), jdkDir); - if (newJdk == null) { - throw new IllegalStateException("Cannot obtain version of recently installed JDK"); - } - return newJdk; - } catch (Exception e) { - String msg = "Required Java version not possible to download or install: " + version; - LOGGER.log(Level.FINE, msg); - throw new IllegalStateException( - "Unable to download or install JDK version " + version, e); - } - } - - @Override - public void uninstall(Jdk.@NonNull InstalledJdk jdk) { - JavaUtils.safeDeleteJdk(jdk.home()); - } - - private static String getVersionsUrl(int minVersion, boolean openVersion, OsUtils.OS os, OsUtils.Arch arch, - String distro, String status) { - return FOOJAY_JDK_VERSIONS_URL + getUrlParams(minVersion, openVersion, os, arch, distro, status); - } - - private static String getUrlParams(int version, boolean openVersion, OsUtils.OS os, OsUtils.Arch arch, - String distro, String status) { - Map params = new HashMap<>(); - if (version > 0) { - String v = String.valueOf(version); - if (openVersion) { - v += "..<999"; - } - params.put("version", v); - } - - if (distro == null) { - if (version == 0 || version == 8 || version == 11 || version >= 17) { - distro = "temurin"; - } else { - distro = "aoj"; - } - } - params.put("distro", distro); - - String archiveType; - if (os == OsUtils.OS.windows) { - archiveType = "zip"; - } else { - archiveType = "tar.gz"; - } - params.put("archive_type", archiveType); - - params.put("architecture", arch.name()); - params.put("package_type", "jdk"); - params.put("operating_system", os.name()); - - if (os == OsUtils.OS.windows) { - params.put("libc_type", "c_std_lib"); - } else if (os == OsUtils.OS.mac) { - params.put("libc_type", "libc"); - } else if (os == OsUtils.OS.alpine_linux) { - params.put("libc_type", "musl"); - } else { - params.put("libc_type", "glibc"); - } - - params.put("javafx_bundled", "false"); - params.put("latest", "available"); - params.put("release_status", status); - params.put("directly_downloadable", "true"); - - return urlEncodeUTF8(params); - } - - static String urlEncodeUTF8(Map map) { - StringBuilder sb = new StringBuilder(); - for (Map.Entry entry : map.entrySet()) { - if (sb.length() > 0) { - sb.append("&"); - } - sb.append( - String.format( - "%s=%s", - urlEncodeUTF8(entry.getKey().toString()), - urlEncodeUTF8(entry.getValue().toString()))); - } - return sb.toString(); - } - - static String urlEncodeUTF8(String s) { - try { - return URLEncoder.encode(s, "UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new IllegalStateException(e); - } - } - - private static int jdkVersion(String jdk) { - return JavaUtils.parseJavaVersion(jdk); - } - - static class AvailableFoojayJdk extends Jdk.AvailableJdk.Default { - public final String downloadUrl; - - AvailableFoojayJdk(@NonNull JdkProvider provider, @NonNull String id, @NonNull String version, - @NonNull String downloadUrl, @NonNull Set tags) { - super(provider, id, version, tags); - this.downloadUrl = downloadUrl; - } - } - - public static class Discovery implements JdkInstallers.Discovery { - @Override - public @NonNull String name() { - return "foojay"; - } - - @Override - public @NonNull JdkInstaller create(Config config) { - FoojayJdkInstaller installer = new FoojayJdkInstaller(config.jdkProvider()); - installer.distro(config.properties().getOrDefault("distro", null)); - HttpClientBuilder httpClientBuilder = NetUtils.createCachingHttpClientBuilder(config.cachePath()); - RemoteAccessProvider rap = RemoteAccessProvider.createDefaultRemoteAccessProvider(httpClientBuilder); - installer.remoteAccessProvider(rap); - return installer; - } - } -} +package dev.jbang.devkitman.jdkinstallers; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.file.Path; +import java.util.*; +import java.util.function.Function; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import dev.jbang.devkitman.Jdk; +import dev.jbang.devkitman.JdkInstaller; +import dev.jbang.devkitman.JdkInstallers; +import dev.jbang.devkitman.JdkProvider; +import dev.jbang.devkitman.util.*; + +/** + * JVM's main JDK installer that can download and install the JDKs provided by + * the Foojay Disco API. + */ +public class FoojayJdkInstaller implements JdkInstaller { + protected final JdkProvider jdkProvider; + protected final Function jdkId; + protected RemoteAccessProvider remoteAccessProvider; + protected String distro = DEFAULT_DISTRO; + + public static final String FOOJAY_JDK_VERSIONS_URL = "https://api.foojay.io/disco/v3.0/packages?"; + + public static final String DEFAULT_DISTRO = "temurin,aoj"; + + private static final Logger LOGGER = Logger.getLogger(FoojayJdkInstaller.class.getName()); + + public static class JdkResultLinks { + public String pkg_download_redirect; + } + + public static class JdkResult { + public String java_version; + public int major_version; + public String distribution; // temurin, aoj, liberica, zulu, etc. + public String release_status; // ga, ea + public String package_type; // jdk, jre + public boolean javafx_bundled; + public JdkResultLinks links; + } + + public static class VersionsResponse { + public List result; + } + + public FoojayJdkInstaller(@NonNull JdkProvider jdkProvider) { + this.jdkProvider = jdkProvider; + this.jdkId = jdk -> determineId(jdk) + "-" + jdkProvider.name(); + } + + public FoojayJdkInstaller(@NonNull JdkProvider jdkProvider, @NonNull Function jdkId) { + this.jdkProvider = jdkProvider; + this.jdkId = jdkId; + } + + protected @NonNull RemoteAccessProvider remoteAccessProvider() { + if (remoteAccessProvider == null) { + remoteAccessProvider = RemoteAccessProvider.createDefaultRemoteAccessProvider(); + } + return remoteAccessProvider; + } + + public @NonNull FoojayJdkInstaller remoteAccessProvider(@NonNull RemoteAccessProvider remoteAccessProvider) { + this.remoteAccessProvider = remoteAccessProvider; + return this; + } + + public @NonNull FoojayJdkInstaller distro(String distro) { + this.distro = distro != null && !distro.isEmpty() ? distro : DEFAULT_DISTRO; + return this; + } + + @NonNull + @Override + public Stream listAvailable() { + try { + VersionsResponse res = readPackagesForList(); + return processPackages(res.result, majorVersionSort()).distinct(); + } catch (IOException e) { + LOGGER.log(Level.FINE, "Couldn't list available JDKs", e); + return Stream.empty(); + } + } + + private VersionsResponse readPackagesForList() throws IOException { + return readJsonFromUrl( + getVersionsUrl(0, true, OsUtils.getOS(), OsUtils.getArch(), distro, "ga,ea")); + } + + @Override + public Jdk.@Nullable AvailableJdk getAvailableByVersion(int version, boolean openVersion) { + int djv = jdkProvider.manager().defaultJavaVersion; + Comparator preferGaSort = (j1, j2) -> { + // Prefer versions equal to the default Java version + if (j1.major_version == djv && j2.major_version != djv) { + return -1; + } else if (j2.major_version == djv && j1.major_version != djv) { + return 1; + } + // Prefer GA releases over EA releases + if (!j1.release_status.equals(j2.release_status)) { + return j2.release_status.compareTo(j1.release_status); + } + // Prefer newer versions + return majorVersionSort().compare(j1, j2); + }; + try { + VersionsResponse res = readPackagesForVersion(version, openVersion); + return processPackages(res.result, preferGaSort) + .filter(Jdk.Predicates.forVersion(version, openVersion)) + .findFirst() + .orElse(null); + } catch (IOException e) { + LOGGER.log(Level.FINE, "Couldn't get available JDK by version", e); + return null; + } + } + + private VersionsResponse readPackagesForVersion(Integer minVersion, boolean openVersion) throws IOException { + VersionsResponse res = readJsonFromUrl( + getVersionsUrl(minVersion, openVersion, OsUtils.getOS(), OsUtils.getArch(), distro, "ga")); + if (res.result.isEmpty()) { + res = readJsonFromUrl( + getVersionsUrl(minVersion, openVersion, OsUtils.getOS(), OsUtils.getArch(), distro, "ea")); + } + return res; + } + + private Stream processPackages(List jdks, Comparator sortFunc) { + return filterEA(jdks) + .stream() + .sorted(sortFunc) + .map(jdk -> new AvailableFoojayJdk(jdkProvider, + jdkId.apply(jdk), jdk.java_version, + jdk.links.pkg_download_redirect, determineTags(jdk))); + } + + private @NonNull String determineId(@NonNull JdkResult jdk) { + String id = jdk.java_version + "-" + jdk.distribution; + if (Jdk.Default.Tags.Jre.name().equals(jdk.package_type)) { + id += "-jre"; + } + if (jdk.javafx_bundled) { + id += "-jfx"; + } + return id; + } + + private @NonNull Set determineTags(JdkResult jdk) { + Set tags = new HashSet<>(); + if (Jdk.Default.Tags.Ga.name().equalsIgnoreCase(jdk.release_status)) { + tags.add(Jdk.Default.Tags.Ga.name()); + } else if (Jdk.Default.Tags.Ea.name().equalsIgnoreCase(jdk.release_status)) { + tags.add(Jdk.Default.Tags.Ea.name()); + } + if (Jdk.Default.Tags.Jdk.name().equalsIgnoreCase(jdk.package_type)) { + tags.add(Jdk.Default.Tags.Jdk.name()); + } else if (Jdk.Default.Tags.Jre.name().equalsIgnoreCase(jdk.package_type)) { + tags.add(Jdk.Default.Tags.Jre.name()); + } + if (jdk.javafx_bundled) { + tags.add(Jdk.Default.Tags.Javafx.name()); + } + return tags; + } + + private VersionsResponse readJsonFromUrl(String url) throws IOException { + return RemoteAccessProvider.readJsonFromUrl(remoteAccessProvider(), url, VersionsResponse.class); + } + + // Filter out any EA releases for which a GA with + // the same major version exists + private List filterEA(List jdks) { + Set GAs = jdks.stream() + .filter(jdk -> jdk.release_status.equals("ga")) + .map(jdk -> jdk.major_version) + .collect(Collectors.toSet()); + + JdkResult[] lastJdk = new JdkResult[] { null }; + return jdks.stream() + .filter( + jdk -> { + if (lastJdk[0] == null + || lastJdk[0].major_version != jdk.major_version + && (jdk.release_status.equals("ga") + || !GAs.contains(jdk.major_version))) { + lastJdk[0] = jdk; + return true; + } else { + return false; + } + }) + .collect(Collectors.toList()); + } + + private static final Comparator jdkResultVersionComparator = (o1, o2) -> VersionComparator.INSTANCE + .compare(o1.java_version, o2.java_version); + + private Comparator majorVersionSort() { + List ds = Arrays.asList(distro.split(",")); + Comparator jdkResultDistroComparator = Comparator.comparingInt(o -> ds.indexOf(o.distribution)); + return Comparator + .comparingInt((JdkResult jdk) -> -jdk.major_version) + .thenComparing(jdkResultDistroComparator) + .thenComparing(jdkResultVersionComparator.reversed()); + } + + @Override + public Jdk.@NonNull InstalledJdk install(Jdk.@NonNull AvailableJdk jdk, @NonNull Path jdkDir) { + if (!(jdk instanceof AvailableFoojayJdk)) { + throw new IllegalArgumentException( + "FoojayJdkInstaller can only install JDKs listed as available by itself"); + } + AvailableFoojayJdk foojayJdk = (AvailableFoojayJdk) jdk; + int version = jdkVersion(foojayJdk.id()); + LOGGER.log( + Level.INFO, + "Downloading JDK {0}. Be patient, this can take several minutes...", + version); + String url = foojayJdk.downloadUrl; + + try { + LOGGER.log(Level.FINE, "Downloading {0}", url); + Path jdkPkg = remoteAccessProvider().downloadFromUrl(url); + + LOGGER.log(Level.INFO, "Installing JDK {0}...", version); + JavaUtils.installJdk(jdkPkg, jdkDir); + + Jdk.InstalledJdk newJdk = jdkProvider.createJdk(foojayJdk.id(), jdkDir); + if (newJdk == null) { + throw new IllegalStateException("Cannot obtain version of recently installed JDK"); + } + return newJdk; + } catch (Exception e) { + String msg = "Required Java version not possible to download or install: " + version; + LOGGER.log(Level.FINE, msg); + throw new IllegalStateException( + "Unable to download or install JDK version " + version, e); + } + } + + @Override + public void uninstall(Jdk.@NonNull InstalledJdk jdk) { + JavaUtils.safeDeleteJdk(jdk.home()); + } + + private static String getVersionsUrl(int minVersion, boolean openVersion, OsUtils.OS os, OsUtils.Arch arch, + String distro, String status) { + return FOOJAY_JDK_VERSIONS_URL + getUrlParams(minVersion, openVersion, os, arch, distro, status); + } + + private static String getUrlParams(int version, boolean openVersion, OsUtils.OS os, OsUtils.Arch arch, + String distro, String status) { + Map params = new HashMap<>(); + if (version > 0) { + String v = String.valueOf(version); + if (openVersion) { + v += "..<999"; + } + params.put("version", v); + } + + if (distro == null) { + if (version == 0 || version == 8 || version == 11 || version >= 17) { + distro = "temurin"; + } else { + distro = "aoj"; + } + } + params.put("distro", distro); + + String archiveType; + if (os == OsUtils.OS.windows) { + archiveType = "zip"; + } else { + archiveType = "tar.gz"; + } + params.put("archive_type", archiveType); + + params.put("architecture", arch.name()); + params.put("package_type", "jdk"); + params.put("operating_system", os.name()); + + if (os == OsUtils.OS.windows) { + params.put("libc_type", "c_std_lib"); + } else if (os == OsUtils.OS.mac) { + params.put("libc_type", "libc"); + } else if (os == OsUtils.OS.alpine_linux) { + params.put("libc_type", "musl"); + } else { + params.put("libc_type", "glibc"); + } + + params.put("javafx_bundled", "false"); + params.put("latest", "available"); + params.put("release_status", status); + params.put("directly_downloadable", "true"); + + return urlEncodeUTF8(params); + } + + static String urlEncodeUTF8(Map map) { + StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : map.entrySet()) { + if (sb.length() > 0) { + sb.append("&"); + } + sb.append( + String.format( + "%s=%s", + urlEncodeUTF8(entry.getKey().toString()), + urlEncodeUTF8(entry.getValue().toString()))); + } + return sb.toString(); + } + + static String urlEncodeUTF8(String s) { + try { + return URLEncoder.encode(s, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException(e); + } + } + + private static int jdkVersion(String jdk) { + return JavaUtils.parseJavaVersion(jdk); + } + + static class AvailableFoojayJdk extends Jdk.AvailableJdk.Default { + public final String downloadUrl; + + AvailableFoojayJdk(@NonNull JdkProvider provider, @NonNull String id, @NonNull String version, + @NonNull String downloadUrl, @NonNull Set tags) { + super(provider, id, version, tags); + this.downloadUrl = downloadUrl; + } + } + + public static class Discovery implements JdkInstallers.Discovery { + @Override + public @NonNull String name() { + return "foojay"; + } + + @Override + public @NonNull JdkInstaller create(Config config) { + FoojayJdkInstaller installer = new FoojayJdkInstaller(config.jdkProvider()); + installer.distro(config.properties().getOrDefault("distro", null)); + HttpClientBuilder httpClientBuilder = NetUtils.createCachingHttpClientBuilder(config.cachePath()); + RemoteAccessProvider rap = RemoteAccessProvider.createDefaultRemoteAccessProvider(httpClientBuilder); + installer.remoteAccessProvider(rap); + return installer; + } + } +} diff --git a/src/main/java/dev/jbang/devkitman/jdkinstallers/MetadataJdkInstaller.java b/src/main/java/dev/jbang/devkitman/jdkinstallers/MetadataJdkInstaller.java index 404e864..47a9b00 100644 --- a/src/main/java/dev/jbang/devkitman/jdkinstallers/MetadataJdkInstaller.java +++ b/src/main/java/dev/jbang/devkitman/jdkinstallers/MetadataJdkInstaller.java @@ -1,440 +1,440 @@ -package dev.jbang.devkitman.jdkinstallers; - -import java.io.IOException; -import java.net.URI; -import java.nio.file.Path; -import java.util.*; -import java.util.function.Function; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; -import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; - -import dev.jbang.devkitman.Jdk; -import dev.jbang.devkitman.JdkInstaller; -import dev.jbang.devkitman.JdkInstallers; -import dev.jbang.devkitman.JdkProvider; -import dev.jbang.devkitman.util.*; - -/** - * JDK installer that downloads and installs JDKs using the Java Metadata API - * from https://joschi.github.io/java-metadata/ - */ -public class MetadataJdkInstaller implements JdkInstaller { - protected final @NonNull JdkProvider jdkProvider; - protected final Function jdkId; - protected RemoteAccessProvider remoteAccessProvider; - protected @NonNull String distro = DEFAULT_DISTRO; - protected String jvmImpl = DEFAULT_JVM_IMPL; - - public static final String METADATA_BASE_URL = "https://joschi.github.io/java-metadata/metadata/"; - public static final String DEFAULT_DISTRO = "temurin,adoptopenjdk"; - public static final String DEFAULT_JVM_IMPL = "hotspot"; - - private static final Logger LOGGER = Logger.getLogger(MetadataJdkInstaller.class.getName()); - - /** - * Represents a single metadata entry from the Java Metadata API - */ - public static class MetadataResult { - public String vendor; - public String filename; - public String file_type; - public String release_type; // "ga" or "ea" - public String version; - public String java_version; - public String jvm_impl; // "hotspot", "openj9", "graalvm" - public String os; // "linux", "macosx", "windows", "solaris", "aix" - public String architecture; // "x86_64", "i686", "aarch64", etc. - public String image_type; // "jdk" or "jre" - public List features; - public String url; - public String md5; - public String md5_file; - public String sha1; - public String sha1_file; - public String sha256; - public String sha256_file; - public String sha512; - public String sha512_file; - public Integer size; - } - - public MetadataJdkInstaller(@NonNull JdkProvider jdkProvider) { - this.jdkProvider = jdkProvider; - this.jdkId = jdk -> determineId(jdk) + "-" + jdkProvider.name(); - } - - public MetadataJdkInstaller(@NonNull JdkProvider jdkProvider, @NonNull Function jdkId) { - this.jdkProvider = jdkProvider; - this.jdkId = jdkId; - } - - protected @NonNull RemoteAccessProvider remoteAccessProvider() { - if (remoteAccessProvider == null) { - remoteAccessProvider = RemoteAccessProvider.createDefaultRemoteAccessProvider(); - } - return remoteAccessProvider; - } - - public @NonNull MetadataJdkInstaller remoteAccessProvider(@NonNull RemoteAccessProvider remoteAccessProvider) { - this.remoteAccessProvider = remoteAccessProvider; - return this; - } - - public @NonNull MetadataJdkInstaller distro(@Nullable String distro) { - this.distro = distro != null && !distro.isEmpty() ? distro : DEFAULT_DISTRO; - return this; - } - - public @NonNull MetadataJdkInstaller jvmImpl(@Nullable String jvmImpl) { - this.jvmImpl = jvmImpl; - return this; - } - - @NonNull - @Override - public Stream listAvailable() { - try { - List results = readMetadataForList(); - return processMetadata(results, majorVersionSort()).distinct(); - } catch (IOException e) { - LOGGER.log(Level.FINE, "Couldn't list available JDKs", e); - return Stream.empty(); - } - } - - private List readMetadataForList() throws IOException { - List allResults = new ArrayList<>(); - IOException lastException = null; - - String[] distros = distro.split(","); - // Query for GA releases first - for (String d : distros) { - try { - List results = readJsonFromUrl( - getMetadataUrl("ga", OsUtils.getOS(), OsUtils.getArch(), "jdk", jvmImpl, d.trim())); - allResults.addAll(results); - } catch (IOException e) { - lastException = e; - } - } - // And for EA releases second - for (String d : distros) { - try { - List results = readJsonFromUrl( - getMetadataUrl("ea", OsUtils.getOS(), OsUtils.getArch(), "jdk", jvmImpl, d.trim())); - allResults.addAll(results); - } catch (IOException e) { - lastException = e; - } - } - - // If we have no results at all and had at least one exception, throw the last - // one - if (allResults.isEmpty() && lastException != null) { - throw lastException; - } - - return allResults; - } - - @Override - public Jdk.@Nullable AvailableJdk getAvailableByVersion(int version, boolean openVersion) { - int djv = jdkProvider.manager().defaultJavaVersion; - Comparator preferGaSort = (j1, j2) -> { - int v1 = extractMajorVersion(j1.java_version); - int v2 = extractMajorVersion(j2.java_version); - - // Prefer versions equal to the default Java version - if (v1 == djv && v2 != djv) { - return -1; - } else if (v2 == djv && v1 != djv) { - return 1; - } - // Prefer GA releases over EA releases - if (!j1.release_type.equals(j2.release_type)) { - return j2.release_type.compareTo(j1.release_type); - } - // Prefer newer versions - return majorVersionSort().compare(j1, j2); - }; - - try { - List results = readMetadataForVersion(version, openVersion); - return processMetadata(results, preferGaSort) - .filter(Jdk.Predicates.forVersion(version, openVersion)) - .findFirst() - .orElse(null); - } catch (IOException e) { - LOGGER.log(Level.FINE, "Couldn't get available JDK by version", e); - return null; - } - } - - private List readMetadataForVersion(int version, boolean openVersion) throws IOException { - String[] distros = distro.split(","); - // Try GA first for all selected distros, return the first that has results - for (String d : distros) { - List results = readMetadataForVersionAndDistro(version, openVersion, "ga", d.trim()); - if (!results.isEmpty()) { - return results; - } - } - // Try EA if no GA found - for (String d : distros) { - List results = readMetadataForVersionAndDistro(version, openVersion, "ea", d.trim()); - if (!results.isEmpty()) { - return results; - } - } - return Collections.emptyList(); - } - - private List readMetadataForVersionAndDistro(int version, boolean openVersion, String releaseType, - String distro) throws IOException { - List gaResults = readJsonFromUrl( - getMetadataUrl(releaseType, OsUtils.getOS(), OsUtils.getArch(), "jdk", jvmImpl, distro)); - return filterByVersion(gaResults, version, openVersion); - } - - private List filterByVersion(List results, int version, boolean openVersion) { - return results.stream() - .filter(r -> { - int majorVersion = extractMajorVersion(r.java_version); - if (openVersion) { - return majorVersion >= version; - } else { - return majorVersion == version; - } - }) - .collect(Collectors.toList()); - } - - private Stream processMetadata(List jdks, Comparator sortFunc) { - return filterEA(jdks) - .stream() - .sorted(sortFunc) - .map(jdk -> new AvailableMetadataJdk(jdkProvider, - jdkId.apply(jdk), jdk.java_version, - jdk.url, determineTags(jdk))); - } - - private @NonNull String determineId(@NonNull MetadataResult jdk) { - String id = jdk.java_version + "-" + jdk.vendor; - if ("jre".equals(jdk.image_type)) { - id += "-jre"; - } - if (jdk.features != null && jdk.features.contains("javafx")) { - id += "-jfx"; - } - if (!"hotspot".equals(jdk.jvm_impl)) { - id += "-" + jdk.jvm_impl; - } - return id; - } - - private @NonNull Set determineTags(MetadataResult jdk) { - Set tags = new HashSet<>(); - if ("ga".equalsIgnoreCase(jdk.release_type)) { - tags.add(Jdk.Default.Tags.Ga.name()); - } else if ("ea".equalsIgnoreCase(jdk.release_type)) { - tags.add(Jdk.Default.Tags.Ea.name()); - } - if ("jdk".equalsIgnoreCase(jdk.image_type)) { - tags.add(Jdk.Default.Tags.Jdk.name()); - } else if ("jre".equalsIgnoreCase(jdk.image_type)) { - tags.add(Jdk.Default.Tags.Jre.name()); - } - if (jdk.features != null && jdk.features.contains("javafx")) { - tags.add(Jdk.Default.Tags.Javafx.name()); - } - return tags; - } - - private List readJsonFromUrl(String url) throws IOException { - return Arrays.asList(RemoteAccessProvider.readJsonFromUrl(remoteAccessProvider(), url, MetadataResult[].class)); - } - - // Filter out any EA releases for which a GA with the same major version exists - private List filterEA(List jdks) { - Set GAs = jdks.stream() - .filter(jdk -> "ga".equals(jdk.release_type)) - .map(jdk -> extractMajorVersion(jdk.java_version)) - .collect(Collectors.toSet()); - - MetadataResult[] lastJdk = new MetadataResult[] { null }; - return jdks.stream() - .filter(jdk -> { - int majorVersion = extractMajorVersion(jdk.java_version); - if (lastJdk[0] == null - || extractMajorVersion(lastJdk[0].java_version) != majorVersion - && ("ga".equals(jdk.release_type) - || !GAs.contains(majorVersion))) { - lastJdk[0] = jdk; - return true; - } else { - return false; - } - }) - .collect(Collectors.toList()); - } - - private static final Comparator metadataResultVersionComparator = (o1, - o2) -> VersionComparator.INSTANCE.compare(o1.java_version, o2.java_version); - - private Comparator majorVersionSort() { - return Comparator - .comparingInt((MetadataResult jdk) -> -extractMajorVersion(jdk.java_version)) - .thenComparing(metadataResultVersionComparator.reversed()); - } - - @Override - public Jdk.@NonNull InstalledJdk install(Jdk.@NonNull AvailableJdk jdk, @NonNull Path jdkDir) { - if (!(jdk instanceof AvailableMetadataJdk)) { - throw new IllegalArgumentException( - "MetadataJdkInstaller can only install JDKs listed as available by itself"); - } - AvailableMetadataJdk metadataJdk = (AvailableMetadataJdk) jdk; - int version = jdkVersion(metadataJdk.id()); - LOGGER.log( - Level.INFO, - "Downloading JDK {0}. Be patient, this can take several minutes...", - version); - String url = metadataJdk.downloadUrl; - - try { - LOGGER.log(Level.FINE, "Downloading {0}", url); - Path jdkPkg = remoteAccessProvider().downloadFromUrl(url); - - LOGGER.log(Level.INFO, "Installing JDK {0}...", version); - JavaUtils.installJdk(jdkPkg, jdkDir); - - Jdk.InstalledJdk newJdk = jdkProvider.createJdk(metadataJdk.id(), jdkDir); - if (newJdk == null) { - throw new IllegalStateException("Cannot obtain version of recently installed JDK"); - } - return newJdk; - } catch (Exception e) { - String msg = "Required Java version not possible to download or install: " + version; - LOGGER.log(Level.FINE, msg); - throw new IllegalStateException( - "Unable to download or install JDK version " + version, e); - } - } - - @Override - public void uninstall(Jdk.@NonNull InstalledJdk jdk) { - JavaUtils.safeDeleteJdk(jdk.home()); - } - - /** - * Constructs the metadata API URL for the given parameters Format: - * /metadata/{release_type}/{os}/{arch}/{image_type}/{jvm_impl}/{vendor}.json - */ - private static String getMetadataUrl(String releaseType, OsUtils.OS os, OsUtils.Arch arch, - String imageType, String jvmImpl, String vendor) { - String osName = mapOsToMetadataName(os); - String archName = mapArchToMetadataName(arch); - if (jvmImpl == null || jvmImpl.isEmpty()) { - if (vendor.contains("graalvm") || vendor.equals("mandrel")) { - jvmImpl = "graalvm"; - } else { - jvmImpl = DEFAULT_JVM_IMPL; - } - } - URI uri = URI.create(METADATA_BASE_URL + releaseType + "/" + osName + "/" + archName + "/" - + imageType + "/" + jvmImpl + "/" + vendor + ".json"); - return uri.toString(); - } - - /** - * Maps OsUtils.OS enum to the metadata API os name - */ - private static String mapOsToMetadataName(OsUtils.OS os) { - switch (os) { - case linux: - case alpine_linux: - return "linux"; - case mac: - return "macosx"; - case windows: - return "windows"; - case aix: - return "aix"; - default: - return "linux"; - } - } - - /** - * Maps OsUtils.Arch enum to the metadata API architecture name - */ - private static String mapArchToMetadataName(OsUtils.Arch arch) { - switch (arch) { - case x64: - return "x86_64"; - case x32: - return "i686"; - case aarch64: - case arm64: - return "aarch64"; - case arm: - return "arm32"; - case ppc64: - return "ppc64"; - case ppc64le: - return "ppc64le"; - case s390x: - return "s390x"; - case riscv64: - return "riscv64"; - default: - return "x86_64"; - } - } - - /** - * Extracts the major version from a Java version string - */ - private static int extractMajorVersion(String javaVersion) { - return JavaUtils.parseJavaVersion(javaVersion); - } - - private static int jdkVersion(String jdk) { - return JavaUtils.parseJavaVersion(jdk); - } - - static class AvailableMetadataJdk extends Jdk.AvailableJdk.Default { - public final String downloadUrl; - - AvailableMetadataJdk(@NonNull JdkProvider provider, @NonNull String id, @NonNull String version, - @NonNull String downloadUrl, @NonNull Set tags) { - super(provider, id, version, tags); - this.downloadUrl = downloadUrl; - } - } - - public static class Discovery implements JdkInstallers.Discovery { - @Override - public @NonNull String name() { - return "metadata"; - } - - @Override - public @NonNull JdkInstaller create(Config config) { - MetadataJdkInstaller installer = new MetadataJdkInstaller(config.jdkProvider()); - installer - .distro(config.properties().getOrDefault("distro", null)) - .jvmImpl(config.properties().getOrDefault("impl", null)); - HttpClientBuilder httpClientBuilder = NetUtils.createCachingHttpClientBuilder(config.cachePath()); - RemoteAccessProvider rap = RemoteAccessProvider.createDefaultRemoteAccessProvider(httpClientBuilder); - installer.remoteAccessProvider(rap); - return installer; - } - } -} +package dev.jbang.devkitman.jdkinstallers; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.Path; +import java.util.*; +import java.util.function.Function; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import dev.jbang.devkitman.Jdk; +import dev.jbang.devkitman.JdkInstaller; +import dev.jbang.devkitman.JdkInstallers; +import dev.jbang.devkitman.JdkProvider; +import dev.jbang.devkitman.util.*; + +/** + * JDK installer that downloads and installs JDKs using the Java Metadata API + * from https://joschi.github.io/java-metadata/ + */ +public class MetadataJdkInstaller implements JdkInstaller { + protected final @NonNull JdkProvider jdkProvider; + protected final Function jdkId; + protected RemoteAccessProvider remoteAccessProvider; + protected @NonNull String distro = DEFAULT_DISTRO; + protected String jvmImpl = DEFAULT_JVM_IMPL; + + public static final String METADATA_BASE_URL = "https://joschi.github.io/java-metadata/metadata/"; + public static final String DEFAULT_DISTRO = "temurin,adoptopenjdk"; + public static final String DEFAULT_JVM_IMPL = "hotspot"; + + private static final Logger LOGGER = Logger.getLogger(MetadataJdkInstaller.class.getName()); + + /** + * Represents a single metadata entry from the Java Metadata API + */ + public static class MetadataResult { + public String vendor; + public String filename; + public String file_type; + public String release_type; // "ga" or "ea" + public String version; + public String java_version; + public String jvm_impl; // "hotspot", "openj9", "graalvm" + public String os; // "linux", "macosx", "windows", "solaris", "aix" + public String architecture; // "x86_64", "i686", "aarch64", etc. + public String image_type; // "jdk" or "jre" + public List features; + public String url; + public String md5; + public String md5_file; + public String sha1; + public String sha1_file; + public String sha256; + public String sha256_file; + public String sha512; + public String sha512_file; + public Integer size; + } + + public MetadataJdkInstaller(@NonNull JdkProvider jdkProvider) { + this.jdkProvider = jdkProvider; + this.jdkId = jdk -> determineId(jdk) + "-" + jdkProvider.name(); + } + + public MetadataJdkInstaller(@NonNull JdkProvider jdkProvider, @NonNull Function jdkId) { + this.jdkProvider = jdkProvider; + this.jdkId = jdkId; + } + + protected @NonNull RemoteAccessProvider remoteAccessProvider() { + if (remoteAccessProvider == null) { + remoteAccessProvider = RemoteAccessProvider.createDefaultRemoteAccessProvider(); + } + return remoteAccessProvider; + } + + public @NonNull MetadataJdkInstaller remoteAccessProvider(@NonNull RemoteAccessProvider remoteAccessProvider) { + this.remoteAccessProvider = remoteAccessProvider; + return this; + } + + public @NonNull MetadataJdkInstaller distro(@Nullable String distro) { + this.distro = distro != null && !distro.isEmpty() ? distro : DEFAULT_DISTRO; + return this; + } + + public @NonNull MetadataJdkInstaller jvmImpl(@Nullable String jvmImpl) { + this.jvmImpl = jvmImpl; + return this; + } + + @NonNull + @Override + public Stream listAvailable() { + try { + List results = readMetadataForList(); + return processMetadata(results, majorVersionSort()).distinct(); + } catch (IOException e) { + LOGGER.log(Level.FINE, "Couldn't list available JDKs", e); + return Stream.empty(); + } + } + + private List readMetadataForList() throws IOException { + List allResults = new ArrayList<>(); + IOException lastException = null; + + String[] distros = distro.split(","); + // Query for GA releases first + for (String d : distros) { + try { + List results = readJsonFromUrl( + getMetadataUrl("ga", OsUtils.getOS(), OsUtils.getArch(), "jdk", jvmImpl, d.trim())); + allResults.addAll(results); + } catch (IOException e) { + lastException = e; + } + } + // And for EA releases second + for (String d : distros) { + try { + List results = readJsonFromUrl( + getMetadataUrl("ea", OsUtils.getOS(), OsUtils.getArch(), "jdk", jvmImpl, d.trim())); + allResults.addAll(results); + } catch (IOException e) { + lastException = e; + } + } + + // If we have no results at all and had at least one exception, throw the last + // one + if (allResults.isEmpty() && lastException != null) { + throw lastException; + } + + return allResults; + } + + @Override + public Jdk.@Nullable AvailableJdk getAvailableByVersion(int version, boolean openVersion) { + int djv = jdkProvider.manager().defaultJavaVersion; + Comparator preferGaSort = (j1, j2) -> { + int v1 = extractMajorVersion(j1.java_version); + int v2 = extractMajorVersion(j2.java_version); + + // Prefer versions equal to the default Java version + if (v1 == djv && v2 != djv) { + return -1; + } else if (v2 == djv && v1 != djv) { + return 1; + } + // Prefer GA releases over EA releases + if (!j1.release_type.equals(j2.release_type)) { + return j2.release_type.compareTo(j1.release_type); + } + // Prefer newer versions + return majorVersionSort().compare(j1, j2); + }; + + try { + List results = readMetadataForVersion(version, openVersion); + return processMetadata(results, preferGaSort) + .filter(Jdk.Predicates.forVersion(version, openVersion)) + .findFirst() + .orElse(null); + } catch (IOException e) { + LOGGER.log(Level.FINE, "Couldn't get available JDK by version", e); + return null; + } + } + + private List readMetadataForVersion(int version, boolean openVersion) throws IOException { + String[] distros = distro.split(","); + // Try GA first for all selected distros, return the first that has results + for (String d : distros) { + List results = readMetadataForVersionAndDistro(version, openVersion, "ga", d.trim()); + if (!results.isEmpty()) { + return results; + } + } + // Try EA if no GA found + for (String d : distros) { + List results = readMetadataForVersionAndDistro(version, openVersion, "ea", d.trim()); + if (!results.isEmpty()) { + return results; + } + } + return Collections.emptyList(); + } + + private List readMetadataForVersionAndDistro(int version, boolean openVersion, String releaseType, + String distro) throws IOException { + List gaResults = readJsonFromUrl( + getMetadataUrl(releaseType, OsUtils.getOS(), OsUtils.getArch(), "jdk", jvmImpl, distro)); + return filterByVersion(gaResults, version, openVersion); + } + + private List filterByVersion(List results, int version, boolean openVersion) { + return results.stream() + .filter(r -> { + int majorVersion = extractMajorVersion(r.java_version); + if (openVersion) { + return majorVersion >= version; + } else { + return majorVersion == version; + } + }) + .collect(Collectors.toList()); + } + + private Stream processMetadata(List jdks, Comparator sortFunc) { + return filterEA(jdks) + .stream() + .sorted(sortFunc) + .map(jdk -> new AvailableMetadataJdk(jdkProvider, + jdkId.apply(jdk), jdk.java_version, + jdk.url, determineTags(jdk))); + } + + private @NonNull String determineId(@NonNull MetadataResult jdk) { + String id = jdk.java_version + "-" + jdk.vendor; + if ("jre".equals(jdk.image_type)) { + id += "-jre"; + } + if (jdk.features != null && jdk.features.contains("javafx")) { + id += "-jfx"; + } + if (!"hotspot".equals(jdk.jvm_impl)) { + id += "-" + jdk.jvm_impl; + } + return id; + } + + private @NonNull Set determineTags(MetadataResult jdk) { + Set tags = new HashSet<>(); + if ("ga".equalsIgnoreCase(jdk.release_type)) { + tags.add(Jdk.Default.Tags.Ga.name()); + } else if ("ea".equalsIgnoreCase(jdk.release_type)) { + tags.add(Jdk.Default.Tags.Ea.name()); + } + if ("jdk".equalsIgnoreCase(jdk.image_type)) { + tags.add(Jdk.Default.Tags.Jdk.name()); + } else if ("jre".equalsIgnoreCase(jdk.image_type)) { + tags.add(Jdk.Default.Tags.Jre.name()); + } + if (jdk.features != null && jdk.features.contains("javafx")) { + tags.add(Jdk.Default.Tags.Javafx.name()); + } + return tags; + } + + private List readJsonFromUrl(String url) throws IOException { + return Arrays.asList(RemoteAccessProvider.readJsonFromUrl(remoteAccessProvider(), url, MetadataResult[].class)); + } + + // Filter out any EA releases for which a GA with the same major version exists + private List filterEA(List jdks) { + Set GAs = jdks.stream() + .filter(jdk -> "ga".equals(jdk.release_type)) + .map(jdk -> extractMajorVersion(jdk.java_version)) + .collect(Collectors.toSet()); + + MetadataResult[] lastJdk = new MetadataResult[] { null }; + return jdks.stream() + .filter(jdk -> { + int majorVersion = extractMajorVersion(jdk.java_version); + if (lastJdk[0] == null + || extractMajorVersion(lastJdk[0].java_version) != majorVersion + && ("ga".equals(jdk.release_type) + || !GAs.contains(majorVersion))) { + lastJdk[0] = jdk; + return true; + } else { + return false; + } + }) + .collect(Collectors.toList()); + } + + private static final Comparator metadataResultVersionComparator = (o1, + o2) -> VersionComparator.INSTANCE.compare(o1.java_version, o2.java_version); + + private Comparator majorVersionSort() { + return Comparator + .comparingInt((MetadataResult jdk) -> -extractMajorVersion(jdk.java_version)) + .thenComparing(metadataResultVersionComparator.reversed()); + } + + @Override + public Jdk.@NonNull InstalledJdk install(Jdk.@NonNull AvailableJdk jdk, @NonNull Path jdkDir) { + if (!(jdk instanceof AvailableMetadataJdk)) { + throw new IllegalArgumentException( + "MetadataJdkInstaller can only install JDKs listed as available by itself"); + } + AvailableMetadataJdk metadataJdk = (AvailableMetadataJdk) jdk; + int version = jdkVersion(metadataJdk.id()); + LOGGER.log( + Level.INFO, + "Downloading JDK {0}. Be patient, this can take several minutes...", + version); + String url = metadataJdk.downloadUrl; + + try { + LOGGER.log(Level.FINE, "Downloading {0}", url); + Path jdkPkg = remoteAccessProvider().downloadFromUrl(url); + + LOGGER.log(Level.INFO, "Installing JDK {0}...", version); + JavaUtils.installJdk(jdkPkg, jdkDir); + + Jdk.InstalledJdk newJdk = jdkProvider.createJdk(metadataJdk.id(), jdkDir); + if (newJdk == null) { + throw new IllegalStateException("Cannot obtain version of recently installed JDK"); + } + return newJdk; + } catch (Exception e) { + String msg = "Required Java version not possible to download or install: " + version; + LOGGER.log(Level.FINE, msg); + throw new IllegalStateException( + "Unable to download or install JDK version " + version, e); + } + } + + @Override + public void uninstall(Jdk.@NonNull InstalledJdk jdk) { + JavaUtils.safeDeleteJdk(jdk.home()); + } + + /** + * Constructs the metadata API URL for the given parameters Format: + * /metadata/{release_type}/{os}/{arch}/{image_type}/{jvm_impl}/{vendor}.json + */ + private static String getMetadataUrl(String releaseType, OsUtils.OS os, OsUtils.Arch arch, + String imageType, String jvmImpl, String vendor) { + String osName = mapOsToMetadataName(os); + String archName = mapArchToMetadataName(arch); + if (jvmImpl == null || jvmImpl.isEmpty()) { + if (vendor.contains("graalvm") || vendor.equals("mandrel")) { + jvmImpl = "graalvm"; + } else { + jvmImpl = DEFAULT_JVM_IMPL; + } + } + URI uri = URI.create(METADATA_BASE_URL + releaseType + "/" + osName + "/" + archName + "/" + + imageType + "/" + jvmImpl + "/" + vendor + ".json"); + return uri.toString(); + } + + /** + * Maps OsUtils.OS enum to the metadata API os name + */ + private static String mapOsToMetadataName(OsUtils.OS os) { + switch (os) { + case linux: + case alpine_linux: + return "linux"; + case mac: + return "macosx"; + case windows: + return "windows"; + case aix: + return "aix"; + default: + return "linux"; + } + } + + /** + * Maps OsUtils.Arch enum to the metadata API architecture name + */ + private static String mapArchToMetadataName(OsUtils.Arch arch) { + switch (arch) { + case x64: + return "x86_64"; + case x32: + return "i686"; + case aarch64: + case arm64: + return "aarch64"; + case arm: + return "arm32"; + case ppc64: + return "ppc64"; + case ppc64le: + return "ppc64le"; + case s390x: + return "s390x"; + case riscv64: + return "riscv64"; + default: + return "x86_64"; + } + } + + /** + * Extracts the major version from a Java version string + */ + private static int extractMajorVersion(String javaVersion) { + return JavaUtils.parseJavaVersion(javaVersion); + } + + private static int jdkVersion(String jdk) { + return JavaUtils.parseJavaVersion(jdk); + } + + static class AvailableMetadataJdk extends Jdk.AvailableJdk.Default { + public final String downloadUrl; + + AvailableMetadataJdk(@NonNull JdkProvider provider, @NonNull String id, @NonNull String version, + @NonNull String downloadUrl, @NonNull Set tags) { + super(provider, id, version, tags); + this.downloadUrl = downloadUrl; + } + } + + public static class Discovery implements JdkInstallers.Discovery { + @Override + public @NonNull String name() { + return "metadata"; + } + + @Override + public @NonNull JdkInstaller create(Config config) { + MetadataJdkInstaller installer = new MetadataJdkInstaller(config.jdkProvider()); + installer + .distro(config.properties().getOrDefault("distro", null)) + .jvmImpl(config.properties().getOrDefault("impl", null)); + HttpClientBuilder httpClientBuilder = NetUtils.createCachingHttpClientBuilder(config.cachePath()); + RemoteAccessProvider rap = RemoteAccessProvider.createDefaultRemoteAccessProvider(httpClientBuilder); + installer.remoteAccessProvider(rap); + return installer; + } + } +} diff --git a/src/main/java/dev/jbang/devkitman/jdkproviders/SdkmanJdkProvider.java b/src/main/java/dev/jbang/devkitman/jdkproviders/SdkmanJdkProvider.java index 312ebdb..fe421eb 100644 --- a/src/main/java/dev/jbang/devkitman/jdkproviders/SdkmanJdkProvider.java +++ b/src/main/java/dev/jbang/devkitman/jdkproviders/SdkmanJdkProvider.java @@ -1,50 +1,50 @@ -package dev.jbang.devkitman.jdkproviders; - -import java.nio.file.Path; -import java.nio.file.Paths; - -import org.jspecify.annotations.NonNull; - -import dev.jbang.devkitman.JdkDiscovery; -import dev.jbang.devkitman.JdkProvider; -import dev.jbang.devkitman.util.FileUtils; -import dev.jbang.devkitman.util.JavaUtils; - -/** - * This JDK provider detects any JDKs that have been installed using the SDKMAN - * package manager. - */ -public class SdkmanJdkProvider extends BaseFoldersJdkProvider { - private static final Path JDKS_ROOT = Paths.get(".sdkman", "candidates", "java"); - - public SdkmanJdkProvider() { - super(Paths.get(System.getProperty("user.home")).resolve(JDKS_ROOT)); - } - - @Override - public @NonNull String description() { - return "The JDKs installed using the SDKMAN package manager."; - } - - @Override - protected boolean acceptFolder(@NonNull Path jdkFolder) { - return jdkFolder.startsWith(jdksRoot) - && !FileUtils.isSameFolderLink(jdkFolder) - && JavaUtils.hasJavacCmd(jdkFolder); - } - - public static class Discovery implements JdkDiscovery { - public static final String PROVIDER_ID = "sdkman"; - - @Override - @NonNull - public String name() { - return PROVIDER_ID; - } - - @Override - public JdkProvider create(@NonNull Config config) { - return new SdkmanJdkProvider(); - } - } -} +package dev.jbang.devkitman.jdkproviders; + +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.jspecify.annotations.NonNull; + +import dev.jbang.devkitman.JdkDiscovery; +import dev.jbang.devkitman.JdkProvider; +import dev.jbang.devkitman.util.FileUtils; +import dev.jbang.devkitman.util.JavaUtils; + +/** + * This JDK provider detects any JDKs that have been installed using the SDKMAN + * package manager. + */ +public class SdkmanJdkProvider extends BaseFoldersJdkProvider { + private static final Path JDKS_ROOT = Paths.get(".sdkman", "candidates", "java"); + + public SdkmanJdkProvider() { + super(Paths.get(System.getProperty("user.home")).resolve(JDKS_ROOT)); + } + + @Override + public @NonNull String description() { + return "The JDKs installed using the SDKMAN package manager."; + } + + @Override + protected boolean acceptFolder(@NonNull Path jdkFolder) { + return jdkFolder.startsWith(jdksRoot) + && !FileUtils.isSameFolderLink(jdkFolder) + && JavaUtils.hasJavacCmd(jdkFolder); + } + + public static class Discovery implements JdkDiscovery { + public static final String PROVIDER_ID = "sdkman"; + + @Override + @NonNull + public String name() { + return PROVIDER_ID; + } + + @Override + public JdkProvider create(@NonNull Config config) { + return new SdkmanJdkProvider(); + } + } +} diff --git a/src/main/java/dev/jbang/devkitman/util/FileHttpCacheStorage.java b/src/main/java/dev/jbang/devkitman/util/FileHttpCacheStorage.java index a1fca85..17b8824 100644 --- a/src/main/java/dev/jbang/devkitman/util/FileHttpCacheStorage.java +++ b/src/main/java/dev/jbang/devkitman/util/FileHttpCacheStorage.java @@ -1,105 +1,105 @@ -package dev.jbang.devkitman.util; - -import java.io.*; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; - -import org.apache.hc.client5.http.cache.HttpCacheCASOperation; -import org.apache.hc.client5.http.cache.HttpCacheEntry; -import org.apache.hc.client5.http.cache.HttpCacheStorage; -import org.apache.hc.client5.http.cache.ResourceIOException; - -public class FileHttpCacheStorage implements HttpCacheStorage { - - private final Path cacheDir; - - public FileHttpCacheStorage(Path cacheDir) { - this.cacheDir = cacheDir; - } - - @Override - public synchronized void putEntry(String key, HttpCacheEntry entry) throws ResourceIOException { - try { - Files.createDirectories(cacheDir); - } catch (IOException e) { - throw new RuntimeException("Failed to create cache directory", e); - } - Path filePath = cacheDir.resolve(encodeKey(key)); - try (ObjectOutputStream oos = new ObjectOutputStream( - new BufferedOutputStream(Files.newOutputStream(filePath)))) { - oos.writeObject(entry); - } catch (IOException e) { - throw new ResourceIOException("Failed to write cache entry", e); - } - } - - @Override - public synchronized HttpCacheEntry getEntry(String key) throws ResourceIOException { - Path filePath = cacheDir.resolve(encodeKey(key)); - if (!Files.exists(filePath)) { - return null; - } - try (ObjectInputStream ois = new ObjectInputStream( - new BufferedInputStream(Files.newInputStream(filePath)))) { - return (HttpCacheEntry) ois.readObject(); - } catch (IOException | ClassNotFoundException e) { - throw new ResourceIOException("Failed to read cache entry", e); - } - } - - @Override - public synchronized void removeEntry(String key) { - Path filePath = cacheDir.resolve(encodeKey(key)); - try { - Files.deleteIfExists(filePath); - } catch (IOException e) { - // Ignore errors on removal - } - } - - @Override - public synchronized void updateEntry(String key, HttpCacheCASOperation operation) throws ResourceIOException { - HttpCacheEntry existingEntry = getEntry(key); - HttpCacheEntry updatedEntry = operation.execute(existingEntry); - if (updatedEntry != null) { - putEntry(key, updatedEntry); - } - } - - @Override - public synchronized Map getEntries(Collection keys) throws ResourceIOException { - Map result = new HashMap<>(); - for (String key : keys) { - HttpCacheEntry entry = getEntry(key); - if (entry != null) { - result.put(key, entry); - } - } - return result; - } - - private String encodeKey(String key) { - int p = key.indexOf("https://"); - if (p == -1) { - p = key.indexOf("http://"); - } - if (p != -1) { - String hap = key.substring(p); - p = hap.indexOf("?"); - if (p != -1) { - hap = hap.substring(0, p); - } - String encoded = hap.replaceAll("[^a-zA-Z0-9-_]", "_"); - if (encoded.length() > 100) { - encoded = encoded.substring(0, 100); - } - encoded = encoded + "_" + Integer.toHexString(key.hashCode()) + ".cache"; - return encoded; - } else { - return Integer.toHexString(key.hashCode()) + ".cache"; - } - } -} +package dev.jbang.devkitman.util; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import org.apache.hc.client5.http.cache.HttpCacheCASOperation; +import org.apache.hc.client5.http.cache.HttpCacheEntry; +import org.apache.hc.client5.http.cache.HttpCacheStorage; +import org.apache.hc.client5.http.cache.ResourceIOException; + +public class FileHttpCacheStorage implements HttpCacheStorage { + + private final Path cacheDir; + + public FileHttpCacheStorage(Path cacheDir) { + this.cacheDir = cacheDir; + } + + @Override + public synchronized void putEntry(String key, HttpCacheEntry entry) throws ResourceIOException { + try { + Files.createDirectories(cacheDir); + } catch (IOException e) { + throw new RuntimeException("Failed to create cache directory", e); + } + Path filePath = cacheDir.resolve(encodeKey(key)); + try (ObjectOutputStream oos = new ObjectOutputStream( + new BufferedOutputStream(Files.newOutputStream(filePath)))) { + oos.writeObject(entry); + } catch (IOException e) { + throw new ResourceIOException("Failed to write cache entry", e); + } + } + + @Override + public synchronized HttpCacheEntry getEntry(String key) throws ResourceIOException { + Path filePath = cacheDir.resolve(encodeKey(key)); + if (!Files.exists(filePath)) { + return null; + } + try (ObjectInputStream ois = new ObjectInputStream( + new BufferedInputStream(Files.newInputStream(filePath)))) { + return (HttpCacheEntry) ois.readObject(); + } catch (IOException | ClassNotFoundException e) { + throw new ResourceIOException("Failed to read cache entry", e); + } + } + + @Override + public synchronized void removeEntry(String key) { + Path filePath = cacheDir.resolve(encodeKey(key)); + try { + Files.deleteIfExists(filePath); + } catch (IOException e) { + // Ignore errors on removal + } + } + + @Override + public synchronized void updateEntry(String key, HttpCacheCASOperation operation) throws ResourceIOException { + HttpCacheEntry existingEntry = getEntry(key); + HttpCacheEntry updatedEntry = operation.execute(existingEntry); + if (updatedEntry != null) { + putEntry(key, updatedEntry); + } + } + + @Override + public synchronized Map getEntries(Collection keys) throws ResourceIOException { + Map result = new HashMap<>(); + for (String key : keys) { + HttpCacheEntry entry = getEntry(key); + if (entry != null) { + result.put(key, entry); + } + } + return result; + } + + private String encodeKey(String key) { + int p = key.indexOf("https://"); + if (p == -1) { + p = key.indexOf("http://"); + } + if (p != -1) { + String hap = key.substring(p); + p = hap.indexOf("?"); + if (p != -1) { + hap = hap.substring(0, p); + } + String encoded = hap.replaceAll("[^a-zA-Z0-9-_]", "_"); + if (encoded.length() > 100) { + encoded = encoded.substring(0, 100); + } + encoded = encoded + "_" + Integer.toHexString(key.hashCode()) + ".cache"; + return encoded; + } else { + return Integer.toHexString(key.hashCode()) + ".cache"; + } + } +} diff --git a/src/main/java/dev/jbang/devkitman/util/FunctionWithError.java b/src/main/java/dev/jbang/devkitman/util/FunctionWithError.java index ba1c407..b65ca90 100644 --- a/src/main/java/dev/jbang/devkitman/util/FunctionWithError.java +++ b/src/main/java/dev/jbang/devkitman/util/FunctionWithError.java @@ -1,15 +1,15 @@ -package dev.jbang.devkitman.util; - -import java.io.IOException; - -@FunctionalInterface -public interface FunctionWithError { - OUT apply(IN in) throws IOException; - - default FunctionWithError andThen(FunctionWithError next) { - return in -> { - OUT intermediate = this.apply(in); - return next.apply(intermediate); - }; - } -} +package dev.jbang.devkitman.util; + +import java.io.IOException; + +@FunctionalInterface +public interface FunctionWithError { + OUT apply(IN in) throws IOException; + + default FunctionWithError andThen(FunctionWithError next) { + return in -> { + OUT intermediate = this.apply(in); + return next.apply(intermediate); + }; + } +} diff --git a/src/main/java/dev/jbang/devkitman/util/NetUtils.java b/src/main/java/dev/jbang/devkitman/util/NetUtils.java index 595cd41..e675629 100644 --- a/src/main/java/dev/jbang/devkitman/util/NetUtils.java +++ b/src/main/java/dev/jbang/devkitman/util/NetUtils.java @@ -1,142 +1,142 @@ -package dev.jbang.devkitman.util; - -import java.io.IOException; -import java.io.InputStream; -import java.io.UncheckedIOException; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Arrays; - -import org.apache.hc.client5.http.classic.methods.HttpGet; -import org.apache.hc.client5.http.config.RequestConfig; -import org.apache.hc.client5.http.impl.cache.CacheConfig; -import org.apache.hc.client5.http.impl.cache.CachingHttpClients; -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; -import org.apache.hc.core5.http.ClassicHttpResponse; -import org.apache.hc.core5.http.ContentType; -import org.apache.hc.core5.http.HttpEntity; -import org.apache.hc.core5.util.Timeout; -import org.jspecify.annotations.NonNull; - -public class NetUtils { - - public static final RequestConfig DEFAULT_REQUEST_CONFIG = RequestConfig.custom() - .setConnectionRequestTimeout(Timeout.ofMilliseconds(10000)) - .setConnectTimeout(Timeout.ofMilliseconds(10000)) - .setResponseTimeout(Timeout.ofMilliseconds(30000)) - .build(); - - public static Path downloadFromUrl(String url) throws IOException { - HttpClientBuilder builder = createDefaultHttpClientBuilder(); - return downloadFromUrl(builder, url); - } - - public static Path downloadFromUrl(HttpClientBuilder builder, String url) throws IOException { - return requestUrl(builder, url, NetUtils::handleDownloadResult); - } - - public static T resultFromUrl(String url, FunctionWithError streamToObject) - throws IOException { - HttpClientBuilder builder = createDefaultHttpClientBuilder(); - return resultFromUrl(builder, url, streamToObject); - } - - public static T resultFromUrl( - HttpClientBuilder builder, String url, FunctionWithError streamToObject) - throws IOException { - return requestUrl( - builder, - url, - mimetypeChecker("application/json", "text/plain") - .andThen(NetUtils::responseStreamer) - .andThen(is -> streamToObject.apply(is))); - } - - public static HttpClientBuilder createDefaultHttpClientBuilder() { - return createCachingHttpClientBuilder(Paths.get("http-cache")); - } - - public static HttpClientBuilder createCachingHttpClientBuilder(@NonNull Path cacheDir) { - CacheConfig cacheConfig = CacheConfig.custom() - .setMaxCacheEntries(1000) - .setSharedCache(false) - .build(); - - FileHttpCacheStorage cacheStorage = new FileHttpCacheStorage(cacheDir); - - return CachingHttpClients.custom() - .setCacheConfig(cacheConfig) - .setHttpCacheStorage(cacheStorage) - .addResponseInterceptorFirst((response, entity, context) -> { - // Force cache headers on all 200 OK responses to make them cacheable - if (response.getCode() == 200) { - response.setHeader("Cache-Control", "max-age=3600, public"); - if (!response.containsHeader("Date")) { - response.setHeader("Date", java.time.Instant.now().toString()); - } - } - }) - .setDefaultRequestConfig(DEFAULT_REQUEST_CONFIG); - } - - public static T requestUrl( - HttpClientBuilder builder, String url, FunctionWithError responseHandler) - throws IOException { - try (CloseableHttpClient httpClient = builder.build()) { - HttpGet httpGet = new HttpGet(url); - return httpClient.execute(httpGet, response -> { - int responseCode = response.getCode(); - if (responseCode != 200) { - throw new IOException( - "Failed to read from URL: " - + url - + ", response code: #" - + responseCode); - } - HttpEntity entity = response.getEntity(); - if (entity == null) { - throw new IOException("Failed to read from URL: " + url + ", no content"); - } - return responseHandler.apply(response); - }); - } catch (UncheckedIOException e) { - throw new IOException("Failed to read from URL: " + url + ", " + e.getMessage(), e); - } - } - - private static FunctionWithError mimetypeChecker( - String... expectedMimeTypes) { - return response -> { - ContentType contentType = ContentType.parse(response.getEntity().getContentType()); - String mimeType = contentType != null ? contentType.getMimeType() : "application/octet-stream"; - if (expectedMimeTypes != null && - expectedMimeTypes.length != 0 && - !Arrays.asList(expectedMimeTypes).contains(mimeType)) { - throw new RuntimeException("Unexpected MIME type: " + mimeType); - } - return response; - }; - } - - private static InputStream responseStreamer(ClassicHttpResponse response) { - try { - HttpEntity entity = response.getEntity(); - return entity.getContent(); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - - private static Path handleDownloadResult(ClassicHttpResponse response) { - try { - HttpEntity entity = response.getEntity(); - try (InputStream is = entity.getContent()) { - // TODO implement - return null; - } - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } -} +package dev.jbang.devkitman.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; + +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.cache.CacheConfig; +import org.apache.hc.client5.http.impl.cache.CachingHttpClients; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.apache.hc.core5.http.ClassicHttpResponse; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.util.Timeout; +import org.jspecify.annotations.NonNull; + +public class NetUtils { + + public static final RequestConfig DEFAULT_REQUEST_CONFIG = RequestConfig.custom() + .setConnectionRequestTimeout(Timeout.ofMilliseconds(10000)) + .setConnectTimeout(Timeout.ofMilliseconds(10000)) + .setResponseTimeout(Timeout.ofMilliseconds(30000)) + .build(); + + public static Path downloadFromUrl(String url) throws IOException { + HttpClientBuilder builder = createDefaultHttpClientBuilder(); + return downloadFromUrl(builder, url); + } + + public static Path downloadFromUrl(HttpClientBuilder builder, String url) throws IOException { + return requestUrl(builder, url, NetUtils::handleDownloadResult); + } + + public static T resultFromUrl(String url, FunctionWithError streamToObject) + throws IOException { + HttpClientBuilder builder = createDefaultHttpClientBuilder(); + return resultFromUrl(builder, url, streamToObject); + } + + public static T resultFromUrl( + HttpClientBuilder builder, String url, FunctionWithError streamToObject) + throws IOException { + return requestUrl( + builder, + url, + mimetypeChecker("application/json", "text/plain") + .andThen(NetUtils::responseStreamer) + .andThen(is -> streamToObject.apply(is))); + } + + public static HttpClientBuilder createDefaultHttpClientBuilder() { + return createCachingHttpClientBuilder(Paths.get("http-cache")); + } + + public static HttpClientBuilder createCachingHttpClientBuilder(@NonNull Path cacheDir) { + CacheConfig cacheConfig = CacheConfig.custom() + .setMaxCacheEntries(1000) + .setSharedCache(false) + .build(); + + FileHttpCacheStorage cacheStorage = new FileHttpCacheStorage(cacheDir); + + return CachingHttpClients.custom() + .setCacheConfig(cacheConfig) + .setHttpCacheStorage(cacheStorage) + .addResponseInterceptorFirst((response, entity, context) -> { + // Force cache headers on all 200 OK responses to make them cacheable + if (response.getCode() == 200) { + response.setHeader("Cache-Control", "max-age=3600, public"); + if (!response.containsHeader("Date")) { + response.setHeader("Date", java.time.Instant.now().toString()); + } + } + }) + .setDefaultRequestConfig(DEFAULT_REQUEST_CONFIG); + } + + public static T requestUrl( + HttpClientBuilder builder, String url, FunctionWithError responseHandler) + throws IOException { + try (CloseableHttpClient httpClient = builder.build()) { + HttpGet httpGet = new HttpGet(url); + return httpClient.execute(httpGet, response -> { + int responseCode = response.getCode(); + if (responseCode != 200) { + throw new IOException( + "Failed to read from URL: " + + url + + ", response code: #" + + responseCode); + } + HttpEntity entity = response.getEntity(); + if (entity == null) { + throw new IOException("Failed to read from URL: " + url + ", no content"); + } + return responseHandler.apply(response); + }); + } catch (UncheckedIOException e) { + throw new IOException("Failed to read from URL: " + url + ", " + e.getMessage(), e); + } + } + + private static FunctionWithError mimetypeChecker( + String... expectedMimeTypes) { + return response -> { + ContentType contentType = ContentType.parse(response.getEntity().getContentType()); + String mimeType = contentType != null ? contentType.getMimeType() : "application/octet-stream"; + if (expectedMimeTypes != null && + expectedMimeTypes.length != 0 && + !Arrays.asList(expectedMimeTypes).contains(mimeType)) { + throw new RuntimeException("Unexpected MIME type: " + mimeType); + } + return response; + }; + } + + private static InputStream responseStreamer(ClassicHttpResponse response) { + try { + HttpEntity entity = response.getEntity(); + return entity.getContent(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private static Path handleDownloadResult(ClassicHttpResponse response) { + try { + HttpEntity entity = response.getEntity(); + try (InputStream is = entity.getContent()) { + // TODO implement + return null; + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/src/main/java/dev/jbang/devkitman/util/RemoteAccessProvider.java b/src/main/java/dev/jbang/devkitman/util/RemoteAccessProvider.java index e7736fe..a2c55ad 100644 --- a/src/main/java/dev/jbang/devkitman/util/RemoteAccessProvider.java +++ b/src/main/java/dev/jbang/devkitman/util/RemoteAccessProvider.java @@ -1,79 +1,79 @@ -package dev.jbang.devkitman.util; - -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.file.Files; -import java.nio.file.Path; - -import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; - -public interface RemoteAccessProvider { - - Path downloadFromUrl(String url) throws IOException; - - default T resultFromUrl(String url, FunctionWithError streamToObject) - throws IOException { - Path file = downloadFromUrl(url); - try (InputStream is = Files.newInputStream(file)) { - return streamToObject.apply(is); - } - } - - static T readJsonFromUrl(RemoteAccessProvider rap, String url, Class klass) throws IOException { - return rap.resultFromUrl(url, is -> { - try (InputStream ignored = is) { - Gson parser = new GsonBuilder().create(); - return parser.fromJson(new InputStreamReader(is), klass); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - } - - static RemoteAccessProvider createDefaultRemoteAccessProvider() { - return new DefaultRemoteAccessProvider(); - } - - static RemoteAccessProvider createDefaultRemoteAccessProvider(Path cacheDir) { - return new DefaultRemoteAccessProvider(cacheDir); - } - - static RemoteAccessProvider createDefaultRemoteAccessProvider(HttpClientBuilder clientBuilder) { - if (clientBuilder != null) { - return new DefaultRemoteAccessProvider(clientBuilder); - } else { - return new DefaultRemoteAccessProvider(); - } - } - - class DefaultRemoteAccessProvider implements RemoteAccessProvider { - private final HttpClientBuilder clientBuilder; - - public DefaultRemoteAccessProvider() { - this.clientBuilder = NetUtils.createDefaultHttpClientBuilder(); - } - - public DefaultRemoteAccessProvider(Path cacheDir) { - this.clientBuilder = NetUtils.createCachingHttpClientBuilder(cacheDir); - } - - public DefaultRemoteAccessProvider(HttpClientBuilder clientBuilder) { - this.clientBuilder = clientBuilder; - } - - @Override - public Path downloadFromUrl(String url) throws IOException { - return NetUtils.downloadFromUrl(clientBuilder, url); - } - - @Override - public T resultFromUrl(String url, FunctionWithError streamToObject) - throws IOException { - return NetUtils.resultFromUrl(clientBuilder, url, streamToObject); - } - } -} +package dev.jbang.devkitman.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +public interface RemoteAccessProvider { + + Path downloadFromUrl(String url) throws IOException; + + default T resultFromUrl(String url, FunctionWithError streamToObject) + throws IOException { + Path file = downloadFromUrl(url); + try (InputStream is = Files.newInputStream(file)) { + return streamToObject.apply(is); + } + } + + static T readJsonFromUrl(RemoteAccessProvider rap, String url, Class klass) throws IOException { + return rap.resultFromUrl(url, is -> { + try (InputStream ignored = is) { + Gson parser = new GsonBuilder().create(); + return parser.fromJson(new InputStreamReader(is), klass); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + + static RemoteAccessProvider createDefaultRemoteAccessProvider() { + return new DefaultRemoteAccessProvider(); + } + + static RemoteAccessProvider createDefaultRemoteAccessProvider(Path cacheDir) { + return new DefaultRemoteAccessProvider(cacheDir); + } + + static RemoteAccessProvider createDefaultRemoteAccessProvider(HttpClientBuilder clientBuilder) { + if (clientBuilder != null) { + return new DefaultRemoteAccessProvider(clientBuilder); + } else { + return new DefaultRemoteAccessProvider(); + } + } + + class DefaultRemoteAccessProvider implements RemoteAccessProvider { + private final HttpClientBuilder clientBuilder; + + public DefaultRemoteAccessProvider() { + this.clientBuilder = NetUtils.createDefaultHttpClientBuilder(); + } + + public DefaultRemoteAccessProvider(Path cacheDir) { + this.clientBuilder = NetUtils.createCachingHttpClientBuilder(cacheDir); + } + + public DefaultRemoteAccessProvider(HttpClientBuilder clientBuilder) { + this.clientBuilder = clientBuilder; + } + + @Override + public Path downloadFromUrl(String url) throws IOException { + return NetUtils.downloadFromUrl(clientBuilder, url); + } + + @Override + public T resultFromUrl(String url, FunctionWithError streamToObject) + throws IOException { + return NetUtils.resultFromUrl(clientBuilder, url, streamToObject); + } + } +} diff --git a/src/test/java/dev/jbang/devkitman/BaseTest.java b/src/test/java/dev/jbang/devkitman/BaseTest.java index bdd347c..2e9b657 100644 --- a/src/test/java/dev/jbang/devkitman/BaseTest.java +++ b/src/test/java/dev/jbang/devkitman/BaseTest.java @@ -1,249 +1,249 @@ -package dev.jbang.devkitman; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Arrays; -import java.util.List; -import java.util.function.BiConsumer; -import java.util.function.Function; -import java.util.logging.*; -import java.util.stream.Collectors; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.io.TempDir; - -import dev.jbang.devkitman.jdkinstallers.FoojayJdkInstaller; -import dev.jbang.devkitman.jdkinstallers.MetadataJdkInstaller; -import dev.jbang.devkitman.jdkproviders.JBangJdkProvider; -import dev.jbang.devkitman.jdkproviders.MockJdkProvider; -import dev.jbang.devkitman.util.FileUtils; -import dev.jbang.devkitman.util.FunctionWithError; -import dev.jbang.devkitman.util.JavaUtils; -import dev.jbang.devkitman.util.RemoteAccessProvider; - -import uk.org.webcompere.systemstubs.environment.EnvironmentVariables; -import uk.org.webcompere.systemstubs.jupiter.SystemStub; -import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension; - -@ExtendWith(SystemStubsExtension.class) -public class BaseTest { - protected JdkDiscovery.Config config; - - @SystemStub - public final EnvironmentVariables environmentVariables = new EnvironmentVariables(); - - private static Path testJdkFile; - - @BeforeAll - protected static void initAll(@TempDir Path tempPath) throws IOException { - // Force the logging level to FINE - Logger root = LogManager.getLogManager().getLogger(""); - root.setLevel(Level.FINE); - Handler consoleHandler = null; - for (Handler handler : root.getHandlers()) { - if (handler instanceof ConsoleHandler) { - consoleHandler = handler; - break; - } - } - if (consoleHandler == null) { - consoleHandler = new ConsoleHandler(); - root.addHandler(new ConsoleHandler()); - } - consoleHandler.setLevel(Level.FINE); - - testJdkFile = tempPath.resolve("jdk-12.zip"); - Files.copy( - BaseTest.class.getResourceAsStream("/jdk-12.zip"), - testJdkFile, - java.nio.file.StandardCopyOption.REPLACE_EXISTING); - } - - @BeforeEach - protected void initEnv(@TempDir Path tempPath) throws IOException { - System.setProperty("user.home", tempPath.resolve("home").toString()); - config = new JdkDiscovery.Config(tempPath.resolve("jdks"), null, null); - } - - protected JdkManager jdkManager() { - return jdkManager("default", "linked", "jbang"); - } - - protected JdkManager jdkManager(String... providerNames) { - List providers = JdkProviders.instance() - .parseNames(config, providerNames) - .stream() - .map(p -> { - if (p instanceof JBangJdkProvider) { - return createJbangProvider(); - } else { - return p; - } - }) - .collect(Collectors.toList()); - - return JdkManager.builder() - .providers(providers) - .build(); - } - - protected JdkManager mockJdkManager(int... versions) { - String[] vs = Arrays.stream(versions) - .mapToObj(v -> v + ".0.7") - .toArray(String[]::new); - return mockJdkManager(vs); - } - - protected JdkManager mockJdkManager(String... versions) { - return mockJdkManager(this::createDummyJdk, versions); - } - - protected JdkManager mockJdkManager(Function mockJdk, String... versions) { - return JdkManager.builder() - .providers(new MockJdkProvider(config.installPath(), mockJdk, versions)) - .build(); - } - - protected Path createMockJdk(int jdkVersion) { - return createMockJdk(jdkVersion, this::initMockJdkDir); - } - - protected Path createMockJdk(String jdkVersion) { - Path jdkPath = config.installPath().resolve(jdkVersion + "-distro-jbang"); - return createMockJdk(jdkPath, jdkVersion, this::initMockJdkDir); - } - - protected Path createDummyJdk(String jdkVersion) { - Path jdkPath = config.installPath().resolve(jdkVersion + "-distro-dummy"); - return createMockJdk(jdkPath, jdkVersion, this::initMockJdkDir); - } - - protected Path createMockJdk(String jdkId, String jdkVersion) { - Path jdkPath = config.installPath().resolve(jdkId); - return createMockJdk(jdkPath, jdkVersion, this::initMockJdkDir); - } - - protected Path createMockJdkRuntime(int jdkVersion) { - return createMockJdk(jdkVersion, this::initMockJdkDirRuntime); - } - - protected Path createMockJdk(int jdkVersion, BiConsumer init) { - Path jdkPath = config.installPath().resolve(jdkVersion + ".0.7-distro-jbang"); - return createMockJdk(jdkPath, jdkVersion + ".0.7", init); - } - - protected Path createMockJdk(Path jdkPath, String jdkVersion, BiConsumer init) { - init.accept(jdkPath, jdkVersion); - Path link = config.installPath().resolve("default"); - if (!Files.exists(link)) { - FileUtils.createLink(link, jdkPath); - } - int v = JavaUtils.parseJavaVersion(jdkVersion); - Path vlink = config.installPath().resolve(String.valueOf(v)); - if (!Files.exists(vlink)) { - FileUtils.createLink(vlink, jdkPath); - } - return jdkPath; - } - - protected Path createMockJdkExt(int jdkVersion) { - Path jdkPath = config.cachePath().resolve("jdk" + jdkVersion); - FileUtils.mkdirs(jdkPath); - initMockJdkDir(jdkPath, jdkVersion + ".0.7"); - return jdkPath; - } - - protected void initMockJdkDirRuntime(Path jdkPath, String version) { - initMockJdkDir(jdkPath, version, "JAVA_RUNTIME_VERSION", true, false, false, false); - } - - protected void initMockJdkDir(Path jdkPath, String version) { - initMockJdkDir(jdkPath, version, "JAVA_VERSION", true, false, false, false); - } - - protected void initMockJdkDir(Path jdkPath, String version, String key, boolean isJdk, boolean isGraalVM, - boolean hasNativeCmd, boolean hasJavaFX) { - try { - Path jdkBinPath = jdkPath.resolve("bin"); - Files.createDirectories(jdkBinPath); - String releaseText = ""; - String rawJavaVersion = key + "=\"" + version + "\"\n"; - releaseText += rawJavaVersion; - Path release = jdkPath.resolve("release"); - Path javaPath = jdkBinPath.resolve("java"); - writeString(javaPath, "dummy"); - javaPath.toFile().setExecutable(true, true); - writeString(jdkBinPath.resolve("java.exe"), "dummy"); - if (isJdk) { - Path javacPath = jdkBinPath.resolve("javac"); - writeString(javacPath, "dummy"); - javacPath.toFile().setExecutable(true, true); - writeString(jdkBinPath.resolve("javac.exe"), "dummy"); - if (isGraalVM) { - String rawGraalVMVersion = "GRAALVM_VERSION=\"" + version + "\"\n"; - releaseText += rawGraalVMVersion; - if (hasNativeCmd) { - Path nativePath = jdkBinPath.resolve("native-image"); - writeString(nativePath, "dummy"); - nativePath.toFile().setExecutable(true, true); - writeString(jdkBinPath.resolve("native-image.exe"), "dummy"); - } - } - } - if (hasJavaFX) { - Path jdkLibPath = jdkPath.resolve("lib"); - Files.createDirectories(jdkLibPath); - String rawJavaFXVersion = "javafx.version=" + version; - Path jfxprops = jdkLibPath.resolve("javafx.properties"); - writeString(jfxprops, rawJavaFXVersion); - } - writeString(release, releaseText); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - protected JBangJdkProvider createJbangProvider() { - RemoteAccessProvider rap = new RemoteAccessProvider() { - @Override - public Path downloadFromUrl(String url) throws IOException { - if (url.startsWith("https://api.foojay.io/disco/v3.0/ids/") && url.endsWith("/redirect")) { - return testJdkFile; - } else if (url.startsWith("https://github.com/adoptium/") && url.endsWith(".zip")) { - return testJdkFile; - } - throw new IOException("Unexpected URL: " + url); - } - - @Override - public T resultFromUrl( - String url, FunctionWithError streamToObject) - throws IOException { - if (url.startsWith(FoojayJdkInstaller.FOOJAY_JDK_VERSIONS_URL)) { - return streamToObject.apply( - getClass().getResourceAsStream("/testFoojayInstall.json")); - } else if (url.startsWith(MetadataJdkInstaller.METADATA_BASE_URL)) { - return streamToObject.apply( - getClass().getResourceAsStream("/testMetadataInstall.json")); - } - throw new IOException("Unexpected URL: " + url); - } - }; - - JBangJdkProvider jbang = new JBangJdkProvider(config.installPath()); - FoojayJdkInstaller installer = new FoojayJdkInstaller(jbang) - .distro("jbang") - .remoteAccessProvider(rap); - installer.remoteAccessProvider(rap); - jbang.installer(installer); - return jbang; - } - - protected void writeString(Path toPath, String scriptText) throws IOException { - Files.write(toPath, scriptText.getBytes()); - } -} +package dev.jbang.devkitman; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.logging.*; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; + +import dev.jbang.devkitman.jdkinstallers.FoojayJdkInstaller; +import dev.jbang.devkitman.jdkinstallers.MetadataJdkInstaller; +import dev.jbang.devkitman.jdkproviders.JBangJdkProvider; +import dev.jbang.devkitman.jdkproviders.MockJdkProvider; +import dev.jbang.devkitman.util.FileUtils; +import dev.jbang.devkitman.util.FunctionWithError; +import dev.jbang.devkitman.util.JavaUtils; +import dev.jbang.devkitman.util.RemoteAccessProvider; + +import uk.org.webcompere.systemstubs.environment.EnvironmentVariables; +import uk.org.webcompere.systemstubs.jupiter.SystemStub; +import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension; + +@ExtendWith(SystemStubsExtension.class) +public class BaseTest { + protected JdkDiscovery.Config config; + + @SystemStub + public final EnvironmentVariables environmentVariables = new EnvironmentVariables(); + + private static Path testJdkFile; + + @BeforeAll + protected static void initAll(@TempDir Path tempPath) throws IOException { + // Force the logging level to FINE + Logger root = LogManager.getLogManager().getLogger(""); + root.setLevel(Level.FINE); + Handler consoleHandler = null; + for (Handler handler : root.getHandlers()) { + if (handler instanceof ConsoleHandler) { + consoleHandler = handler; + break; + } + } + if (consoleHandler == null) { + consoleHandler = new ConsoleHandler(); + root.addHandler(new ConsoleHandler()); + } + consoleHandler.setLevel(Level.FINE); + + testJdkFile = tempPath.resolve("jdk-12.zip"); + Files.copy( + BaseTest.class.getResourceAsStream("/jdk-12.zip"), + testJdkFile, + java.nio.file.StandardCopyOption.REPLACE_EXISTING); + } + + @BeforeEach + protected void initEnv(@TempDir Path tempPath) throws IOException { + System.setProperty("user.home", tempPath.resolve("home").toString()); + config = new JdkDiscovery.Config(tempPath.resolve("jdks"), null, null); + } + + protected JdkManager jdkManager() { + return jdkManager("default", "linked", "jbang"); + } + + protected JdkManager jdkManager(String... providerNames) { + List providers = JdkProviders.instance() + .parseNames(config, providerNames) + .stream() + .map(p -> { + if (p instanceof JBangJdkProvider) { + return createJbangProvider(); + } else { + return p; + } + }) + .collect(Collectors.toList()); + + return JdkManager.builder() + .providers(providers) + .build(); + } + + protected JdkManager mockJdkManager(int... versions) { + String[] vs = Arrays.stream(versions) + .mapToObj(v -> v + ".0.7") + .toArray(String[]::new); + return mockJdkManager(vs); + } + + protected JdkManager mockJdkManager(String... versions) { + return mockJdkManager(this::createDummyJdk, versions); + } + + protected JdkManager mockJdkManager(Function mockJdk, String... versions) { + return JdkManager.builder() + .providers(new MockJdkProvider(config.installPath(), mockJdk, versions)) + .build(); + } + + protected Path createMockJdk(int jdkVersion) { + return createMockJdk(jdkVersion, this::initMockJdkDir); + } + + protected Path createMockJdk(String jdkVersion) { + Path jdkPath = config.installPath().resolve(jdkVersion + "-distro-jbang"); + return createMockJdk(jdkPath, jdkVersion, this::initMockJdkDir); + } + + protected Path createDummyJdk(String jdkVersion) { + Path jdkPath = config.installPath().resolve(jdkVersion + "-distro-dummy"); + return createMockJdk(jdkPath, jdkVersion, this::initMockJdkDir); + } + + protected Path createMockJdk(String jdkId, String jdkVersion) { + Path jdkPath = config.installPath().resolve(jdkId); + return createMockJdk(jdkPath, jdkVersion, this::initMockJdkDir); + } + + protected Path createMockJdkRuntime(int jdkVersion) { + return createMockJdk(jdkVersion, this::initMockJdkDirRuntime); + } + + protected Path createMockJdk(int jdkVersion, BiConsumer init) { + Path jdkPath = config.installPath().resolve(jdkVersion + ".0.7-distro-jbang"); + return createMockJdk(jdkPath, jdkVersion + ".0.7", init); + } + + protected Path createMockJdk(Path jdkPath, String jdkVersion, BiConsumer init) { + init.accept(jdkPath, jdkVersion); + Path link = config.installPath().resolve("default"); + if (!Files.exists(link)) { + FileUtils.createLink(link, jdkPath); + } + int v = JavaUtils.parseJavaVersion(jdkVersion); + Path vlink = config.installPath().resolve(String.valueOf(v)); + if (!Files.exists(vlink)) { + FileUtils.createLink(vlink, jdkPath); + } + return jdkPath; + } + + protected Path createMockJdkExt(int jdkVersion) { + Path jdkPath = config.cachePath().resolve("jdk" + jdkVersion); + FileUtils.mkdirs(jdkPath); + initMockJdkDir(jdkPath, jdkVersion + ".0.7"); + return jdkPath; + } + + protected void initMockJdkDirRuntime(Path jdkPath, String version) { + initMockJdkDir(jdkPath, version, "JAVA_RUNTIME_VERSION", true, false, false, false); + } + + protected void initMockJdkDir(Path jdkPath, String version) { + initMockJdkDir(jdkPath, version, "JAVA_VERSION", true, false, false, false); + } + + protected void initMockJdkDir(Path jdkPath, String version, String key, boolean isJdk, boolean isGraalVM, + boolean hasNativeCmd, boolean hasJavaFX) { + try { + Path jdkBinPath = jdkPath.resolve("bin"); + Files.createDirectories(jdkBinPath); + String releaseText = ""; + String rawJavaVersion = key + "=\"" + version + "\"\n"; + releaseText += rawJavaVersion; + Path release = jdkPath.resolve("release"); + Path javaPath = jdkBinPath.resolve("java"); + writeString(javaPath, "dummy"); + javaPath.toFile().setExecutable(true, true); + writeString(jdkBinPath.resolve("java.exe"), "dummy"); + if (isJdk) { + Path javacPath = jdkBinPath.resolve("javac"); + writeString(javacPath, "dummy"); + javacPath.toFile().setExecutable(true, true); + writeString(jdkBinPath.resolve("javac.exe"), "dummy"); + if (isGraalVM) { + String rawGraalVMVersion = "GRAALVM_VERSION=\"" + version + "\"\n"; + releaseText += rawGraalVMVersion; + if (hasNativeCmd) { + Path nativePath = jdkBinPath.resolve("native-image"); + writeString(nativePath, "dummy"); + nativePath.toFile().setExecutable(true, true); + writeString(jdkBinPath.resolve("native-image.exe"), "dummy"); + } + } + } + if (hasJavaFX) { + Path jdkLibPath = jdkPath.resolve("lib"); + Files.createDirectories(jdkLibPath); + String rawJavaFXVersion = "javafx.version=" + version; + Path jfxprops = jdkLibPath.resolve("javafx.properties"); + writeString(jfxprops, rawJavaFXVersion); + } + writeString(release, releaseText); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + protected JBangJdkProvider createJbangProvider() { + RemoteAccessProvider rap = new RemoteAccessProvider() { + @Override + public Path downloadFromUrl(String url) throws IOException { + if (url.startsWith("https://api.foojay.io/disco/v3.0/ids/") && url.endsWith("/redirect")) { + return testJdkFile; + } else if (url.startsWith("https://github.com/adoptium/") && url.endsWith(".zip")) { + return testJdkFile; + } + throw new IOException("Unexpected URL: " + url); + } + + @Override + public T resultFromUrl( + String url, FunctionWithError streamToObject) + throws IOException { + if (url.startsWith(FoojayJdkInstaller.FOOJAY_JDK_VERSIONS_URL)) { + return streamToObject.apply( + getClass().getResourceAsStream("/testFoojayInstall.json")); + } else if (url.startsWith(MetadataJdkInstaller.METADATA_BASE_URL)) { + return streamToObject.apply( + getClass().getResourceAsStream("/testMetadataInstall.json")); + } + throw new IOException("Unexpected URL: " + url); + } + }; + + JBangJdkProvider jbang = new JBangJdkProvider(config.installPath()); + FoojayJdkInstaller installer = new FoojayJdkInstaller(jbang) + .distro("jbang") + .remoteAccessProvider(rap); + installer.remoteAccessProvider(rap); + jbang.installer(installer); + return jbang; + } + + protected void writeString(Path toPath, String scriptText) throws IOException { + Files.write(toPath, scriptText.getBytes()); + } +} diff --git a/src/test/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstallerTest.java b/src/test/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstallerTest.java index a86cc40..3c2c6bc 100644 --- a/src/test/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstallerTest.java +++ b/src/test/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstallerTest.java @@ -1,349 +1,349 @@ -package dev.jbang.devkitman.jdkinstallers; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; -import static org.junit.jupiter.api.Assertions.*; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import dev.jbang.devkitman.BaseTest; -import dev.jbang.devkitman.Jdk; -import dev.jbang.devkitman.JdkManager; -import dev.jbang.devkitman.jdkproviders.JBangJdkProvider; -import dev.jbang.devkitman.util.FunctionWithError; -import dev.jbang.devkitman.util.RemoteAccessProvider; - -public class FoojayJdkInstallerTest extends BaseTest { - - private FoojayJdkInstaller installer; - private JBangJdkProvider provider; - private Path testJdkFile; - - @BeforeEach - @Override - protected void initEnv(@TempDir Path tempPath) throws IOException { - super.initEnv(tempPath); - - // Copy test JDK file for installation tests - testJdkFile = tempPath.resolve("jdk-12.zip"); - Files.copy( - getClass().getResourceAsStream("/jdk-12.zip"), - testJdkFile, - java.nio.file.StandardCopyOption.REPLACE_EXISTING); - - RemoteAccessProvider rap = createRemoteAccessProvider(); - provider = new JBangJdkProvider(config.installPath()); - installer = new FoojayJdkInstaller(provider) - .distro("jbang") - .remoteAccessProvider(rap); - provider.installer(installer); - - // Create a manager so the provider has access to defaultJavaVersion - JdkManager manager = JdkManager.builder() - .providers(provider) - .build(); - } - - private RemoteAccessProvider createRemoteAccessProvider() { - return new RemoteAccessProvider() { - @Override - public Path downloadFromUrl(String url) throws IOException { - // Verify URL format for Foojay API - if (!url.startsWith("https://api.foojay.io/disco/v3.0/ids/") || !url.endsWith("/redirect")) { - throw new IOException("Unexpected URL: " + url); - } - return testJdkFile; - } - - @Override - public T resultFromUrl(String url, FunctionWithError streamToObject) - throws IOException { - // Verify the URL format matches expected Foojay API pattern - if (!url.startsWith(FoojayJdkInstaller.FOOJAY_JDK_VERSIONS_URL)) { - throw new IOException("Unexpected URL: " + url); - } - // Return our test Foojay JSON for all requests - return streamToObject.apply( - getClass().getResourceAsStream("/testFoojayInstall.json")); - } - }; - } - - @Test - public void testListAvailable() { - List jdks = installer.listAvailable().collect(Collectors.toList()); - - // Should have JDKs from test data - assertThat(jdks, is(not(empty()))); - - // Verify we have expected major versions from testInstall.json - List versions = jdks.stream() - .map(jdk -> Integer.parseInt(jdk.version().split("[.\\-+]")[0])) - .distinct() - .sorted() - .collect(Collectors.toList()); - - // testInstall.json contains versions 11-25 - assertThat(versions, hasItems(11, 17, 21, 23)); - } - - @Test - public void testListAvailableOrderedByVersionDescending() { - List jdks = installer.listAvailable().collect(Collectors.toList()); - - assertThat(jdks.size(), greaterThan(0)); - - // First JDK should be the highest version in our test data - String firstVersion = jdks.get(0).version(); - int firstMajor = Integer.parseInt(firstVersion.split("[.\\-+]")[0]); - - // Verify it's one of the higher versions - assertThat(firstMajor, greaterThanOrEqualTo(23)); - } - - @Test - public void testGetAvailableByVersionExact() { - Jdk.AvailableJdk jdk21 = installer.getAvailableByVersion(21, false); - - assertThat(jdk21, is(notNullValue())); - assertThat(jdk21.version(), startsWith("21.")); - assertTrue(jdk21.tags().contains(Jdk.Default.Tags.Ga.name())); - assertTrue(jdk21.tags().contains(Jdk.Default.Tags.Jdk.name())); - } - - @Test - public void testGetAvailableByVersionOpen() { - // Request version 17+, should return newest available - Jdk.AvailableJdk jdk = installer.getAvailableByVersion(17, true); - - assertThat(jdk, is(notNullValue())); - String version = jdk.version(); - int major = Integer.parseInt(version.split("[.\\-+]")[0]); - assertThat(major, greaterThanOrEqualTo(17)); - } - - @Test - public void testGetAvailableByVersionNotFound() { - // Request version that doesn't exist in our test data - Jdk.AvailableJdk jdk = installer.getAvailableByVersion(6, false); - - assertThat(jdk, is(nullValue())); - } - - @Test - public void testDetermineIdIncludesDistro() { - List jdks = installer.listAvailable().collect(Collectors.toList()); - - assertThat(jdks, is(not(empty()))); - - // All IDs should contain a distribution name - for (Jdk.AvailableJdk jdk : jdks) { - // The ID should contain either temurin or aoj or other distro names - assertThat(jdk.id(), anyOf( - containsString("temurin"), - containsString("aoj"), - containsString("liberica"), - containsString("zulu"))); - } - } - - @Test - public void testDetermineIdForJre() { - // Note: testInstall.json might not contain JRE entries - // This test verifies the logic would work if JRE was present - List jdks = installer.listAvailable().collect(Collectors.toList()); - - Jdk.AvailableJdk jre = jdks.stream() - .filter(j -> j.tags().contains(Jdk.Default.Tags.Jre.name())) - .findFirst() - .orElse(null); - - if (jre != null) { - assertThat(jre.id(), containsString("-jre")); - } - } - - @Test - public void testDetermineTagsForGa() { - Jdk.AvailableJdk jdk17 = installer.getAvailableByVersion(17, false); - - assertThat(jdk17, is(notNullValue())); - Set tags = jdk17.tags(); - - assertThat(tags, hasItem(Jdk.Default.Tags.Ga.name())); - assertThat(tags, hasItem(Jdk.Default.Tags.Jdk.name())); - } - - @Test - public void testDetermineTagsForEa() { - // testInstall.json contains EA versions - List jdks = installer.listAvailable().collect(Collectors.toList()); - - Jdk.AvailableJdk ea = jdks.stream() - .filter(j -> j.tags().contains(Jdk.Default.Tags.Ea.name())) - .findFirst() - .orElse(null); - - if (ea != null) { - Set tags = ea.tags(); - assertThat(tags, hasItem(Jdk.Default.Tags.Ea.name())); - } - } - - @Test - public void testDetermineTagsForJdk() { - List jdks = installer.listAvailable().collect(Collectors.toList()); - - // Most entries should be JDK - long jdkCount = jdks.stream() - .filter(j -> j.tags().contains(Jdk.Default.Tags.Jdk.name())) - .count(); - - assertThat(jdkCount, greaterThan(0L)); - } - - @Test - public void testInstallJdk() throws IOException { - Jdk.AvailableJdk jdk17 = installer.getAvailableByVersion(17, false); - assertThat(jdk17, is(notNullValue())); - - Path installDir = config.installPath().resolve("test-jdk-17"); - Files.createDirectories(installDir.getParent()); - - Jdk.InstalledJdk installed = installer.install(jdk17, installDir); - - assertThat(installed, is(notNullValue())); - assertThat(Files.exists(installDir), is(true)); - assertThat(Files.isDirectory(installDir), is(true)); - } - - @Test - public void testInstallJdkInvalidType() { - // Create a mock JDK from a different installer type - Jdk.AvailableJdk mockJdk = new Jdk.AvailableJdk.Default( - provider, - "mock-jdk-21", - "21.0.0", - Set.of(Jdk.Default.Tags.Ga.name())); - - Path installDir = config.installPath().resolve("test-jdk-mock"); - - IllegalArgumentException exception = assertThrows( - IllegalArgumentException.class, - () -> installer.install(mockJdk, installDir)); - - assertThat(exception.getMessage(), containsString("FoojayJdkInstaller can only install")); - } - - @Test - public void testUninstallJdk() throws IOException { - // Create a mock installed JDK - Path jdkPath = config.installPath().resolve("test-jdk-uninstall"); - initMockJdkDir(jdkPath, "17.0.13"); - - Jdk.InstalledJdk jdk = new Jdk.InstalledJdk.Default( - provider, - "17.0.13-temurin-jbang", - jdkPath, - "17.0.13", - Set.of(Jdk.Default.Tags.Ga.name())); - - assertThat(Files.exists(jdkPath), is(true)); - - installer.uninstall(jdk); - - // JDK directory should be removed - assertThat(Files.exists(jdkPath), is(false)); - } - - @Test - public void testFilterEAWhenGAExists() { - // If we have both EA and GA for same version, EA should be filtered out - List jdks = installer.listAvailable().collect(Collectors.toList()); - - // Count how many times each major version appears - List majorVersions = jdks.stream() - .map(jdk -> Integer.parseInt(jdk.version().split("[.\\-+]")[0])) - .collect(Collectors.toList()); - - // Each major version should appear at most once (after filtering) - long uniqueVersions = majorVersions.stream().distinct().count(); - assertThat((long) majorVersions.size(), greaterThanOrEqualTo(uniqueVersions)); - } - - @Test - public void testAvailableFoojayJdkCreation() { - List jdks = installer.listAvailable().collect(Collectors.toList()); - - assertThat(jdks, is(not(empty()))); - - // Verify that the JDKs are of the correct type - for (Jdk.AvailableJdk jdk : jdks) { - assertThat(jdk, is(instanceOf(FoojayJdkInstaller.AvailableFoojayJdk.class))); - - // Verify essential properties - assertThat(jdk.id(), is(not(emptyString()))); - assertThat(jdk.version(), is(not(emptyString()))); - assertThat(jdk.tags(), is(not(empty()))); - assertThat(jdk.provider(), is(provider)); - } - } - - @Test - public void testDistroSortingOrder() { - // Test that JDKs are sorted by distribution preference - List jdks = installer.listAvailable().collect(Collectors.toList()); - - assertThat(jdks, is(not(empty()))); - - // When filtering by same major version, the preferred distro should come first - // This is based on the distro configuration (default: "temurin,aoj") - List majorVersions = jdks.stream() - .map(jdk -> Integer.parseInt(jdk.version().split("[.\\-+]")[0])) - .distinct() - .collect(Collectors.toList()); - - // Just verify we have multiple versions - assertThat(majorVersions.size(), greaterThan(0)); - } - - @Test - public void testJavaFXBundled() { - // Test JavaFX bundled detection - List jdks = installer.listAvailable().collect(Collectors.toList()); - - // Check if any JDKs have JavaFX tag (depends on test data) - long javafxCount = jdks.stream() - .filter(j -> j.tags().contains(Jdk.Default.Tags.Javafx.name())) - .count(); - - // This is just verifying the tag system works, count might be 0 - assertThat(javafxCount, greaterThanOrEqualTo(0L)); - } - - @Test - public void testMajorVersionExtraction() { - List jdks = installer.listAvailable().collect(Collectors.toList()); - - assertThat(jdks, is(not(empty()))); - - // Verify all JDKs have parseable major versions - for (Jdk.AvailableJdk jdk : jdks) { - String version = jdk.version(); - int major = Integer.parseInt(version.split("[.\\-+]")[0]); - - // Major version should be reasonable (8-30 range as of 2026) - assertThat(major, allOf(greaterThanOrEqualTo(8), lessThanOrEqualTo(30))); - } - } -} +package dev.jbang.devkitman.jdkinstallers; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import dev.jbang.devkitman.BaseTest; +import dev.jbang.devkitman.Jdk; +import dev.jbang.devkitman.JdkManager; +import dev.jbang.devkitman.jdkproviders.JBangJdkProvider; +import dev.jbang.devkitman.util.FunctionWithError; +import dev.jbang.devkitman.util.RemoteAccessProvider; + +public class FoojayJdkInstallerTest extends BaseTest { + + private FoojayJdkInstaller installer; + private JBangJdkProvider provider; + private Path testJdkFile; + + @BeforeEach + @Override + protected void initEnv(@TempDir Path tempPath) throws IOException { + super.initEnv(tempPath); + + // Copy test JDK file for installation tests + testJdkFile = tempPath.resolve("jdk-12.zip"); + Files.copy( + getClass().getResourceAsStream("/jdk-12.zip"), + testJdkFile, + java.nio.file.StandardCopyOption.REPLACE_EXISTING); + + RemoteAccessProvider rap = createRemoteAccessProvider(); + provider = new JBangJdkProvider(config.installPath()); + installer = new FoojayJdkInstaller(provider) + .distro("jbang") + .remoteAccessProvider(rap); + provider.installer(installer); + + // Create a manager so the provider has access to defaultJavaVersion + JdkManager manager = JdkManager.builder() + .providers(provider) + .build(); + } + + private RemoteAccessProvider createRemoteAccessProvider() { + return new RemoteAccessProvider() { + @Override + public Path downloadFromUrl(String url) throws IOException { + // Verify URL format for Foojay API + if (!url.startsWith("https://api.foojay.io/disco/v3.0/ids/") || !url.endsWith("/redirect")) { + throw new IOException("Unexpected URL: " + url); + } + return testJdkFile; + } + + @Override + public T resultFromUrl(String url, FunctionWithError streamToObject) + throws IOException { + // Verify the URL format matches expected Foojay API pattern + if (!url.startsWith(FoojayJdkInstaller.FOOJAY_JDK_VERSIONS_URL)) { + throw new IOException("Unexpected URL: " + url); + } + // Return our test Foojay JSON for all requests + return streamToObject.apply( + getClass().getResourceAsStream("/testFoojayInstall.json")); + } + }; + } + + @Test + public void testListAvailable() { + List jdks = installer.listAvailable().collect(Collectors.toList()); + + // Should have JDKs from test data + assertThat(jdks, is(not(empty()))); + + // Verify we have expected major versions from testInstall.json + List versions = jdks.stream() + .map(jdk -> Integer.parseInt(jdk.version().split("[.\\-+]")[0])) + .distinct() + .sorted() + .collect(Collectors.toList()); + + // testInstall.json contains versions 11-25 + assertThat(versions, hasItems(11, 17, 21, 23)); + } + + @Test + public void testListAvailableOrderedByVersionDescending() { + List jdks = installer.listAvailable().collect(Collectors.toList()); + + assertThat(jdks.size(), greaterThan(0)); + + // First JDK should be the highest version in our test data + String firstVersion = jdks.get(0).version(); + int firstMajor = Integer.parseInt(firstVersion.split("[.\\-+]")[0]); + + // Verify it's one of the higher versions + assertThat(firstMajor, greaterThanOrEqualTo(23)); + } + + @Test + public void testGetAvailableByVersionExact() { + Jdk.AvailableJdk jdk21 = installer.getAvailableByVersion(21, false); + + assertThat(jdk21, is(notNullValue())); + assertThat(jdk21.version(), startsWith("21.")); + assertTrue(jdk21.tags().contains(Jdk.Default.Tags.Ga.name())); + assertTrue(jdk21.tags().contains(Jdk.Default.Tags.Jdk.name())); + } + + @Test + public void testGetAvailableByVersionOpen() { + // Request version 17+, should return newest available + Jdk.AvailableJdk jdk = installer.getAvailableByVersion(17, true); + + assertThat(jdk, is(notNullValue())); + String version = jdk.version(); + int major = Integer.parseInt(version.split("[.\\-+]")[0]); + assertThat(major, greaterThanOrEqualTo(17)); + } + + @Test + public void testGetAvailableByVersionNotFound() { + // Request version that doesn't exist in our test data + Jdk.AvailableJdk jdk = installer.getAvailableByVersion(6, false); + + assertThat(jdk, is(nullValue())); + } + + @Test + public void testDetermineIdIncludesDistro() { + List jdks = installer.listAvailable().collect(Collectors.toList()); + + assertThat(jdks, is(not(empty()))); + + // All IDs should contain a distribution name + for (Jdk.AvailableJdk jdk : jdks) { + // The ID should contain either temurin or aoj or other distro names + assertThat(jdk.id(), anyOf( + containsString("temurin"), + containsString("aoj"), + containsString("liberica"), + containsString("zulu"))); + } + } + + @Test + public void testDetermineIdForJre() { + // Note: testInstall.json might not contain JRE entries + // This test verifies the logic would work if JRE was present + List jdks = installer.listAvailable().collect(Collectors.toList()); + + Jdk.AvailableJdk jre = jdks.stream() + .filter(j -> j.tags().contains(Jdk.Default.Tags.Jre.name())) + .findFirst() + .orElse(null); + + if (jre != null) { + assertThat(jre.id(), containsString("-jre")); + } + } + + @Test + public void testDetermineTagsForGa() { + Jdk.AvailableJdk jdk17 = installer.getAvailableByVersion(17, false); + + assertThat(jdk17, is(notNullValue())); + Set tags = jdk17.tags(); + + assertThat(tags, hasItem(Jdk.Default.Tags.Ga.name())); + assertThat(tags, hasItem(Jdk.Default.Tags.Jdk.name())); + } + + @Test + public void testDetermineTagsForEa() { + // testInstall.json contains EA versions + List jdks = installer.listAvailable().collect(Collectors.toList()); + + Jdk.AvailableJdk ea = jdks.stream() + .filter(j -> j.tags().contains(Jdk.Default.Tags.Ea.name())) + .findFirst() + .orElse(null); + + if (ea != null) { + Set tags = ea.tags(); + assertThat(tags, hasItem(Jdk.Default.Tags.Ea.name())); + } + } + + @Test + public void testDetermineTagsForJdk() { + List jdks = installer.listAvailable().collect(Collectors.toList()); + + // Most entries should be JDK + long jdkCount = jdks.stream() + .filter(j -> j.tags().contains(Jdk.Default.Tags.Jdk.name())) + .count(); + + assertThat(jdkCount, greaterThan(0L)); + } + + @Test + public void testInstallJdk() throws IOException { + Jdk.AvailableJdk jdk17 = installer.getAvailableByVersion(17, false); + assertThat(jdk17, is(notNullValue())); + + Path installDir = config.installPath().resolve("test-jdk-17"); + Files.createDirectories(installDir.getParent()); + + Jdk.InstalledJdk installed = installer.install(jdk17, installDir); + + assertThat(installed, is(notNullValue())); + assertThat(Files.exists(installDir), is(true)); + assertThat(Files.isDirectory(installDir), is(true)); + } + + @Test + public void testInstallJdkInvalidType() { + // Create a mock JDK from a different installer type + Jdk.AvailableJdk mockJdk = new Jdk.AvailableJdk.Default( + provider, + "mock-jdk-21", + "21.0.0", + Set.of(Jdk.Default.Tags.Ga.name())); + + Path installDir = config.installPath().resolve("test-jdk-mock"); + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> installer.install(mockJdk, installDir)); + + assertThat(exception.getMessage(), containsString("FoojayJdkInstaller can only install")); + } + + @Test + public void testUninstallJdk() throws IOException { + // Create a mock installed JDK + Path jdkPath = config.installPath().resolve("test-jdk-uninstall"); + initMockJdkDir(jdkPath, "17.0.13"); + + Jdk.InstalledJdk jdk = new Jdk.InstalledJdk.Default( + provider, + "17.0.13-temurin-jbang", + jdkPath, + "17.0.13", + Set.of(Jdk.Default.Tags.Ga.name())); + + assertThat(Files.exists(jdkPath), is(true)); + + installer.uninstall(jdk); + + // JDK directory should be removed + assertThat(Files.exists(jdkPath), is(false)); + } + + @Test + public void testFilterEAWhenGAExists() { + // If we have both EA and GA for same version, EA should be filtered out + List jdks = installer.listAvailable().collect(Collectors.toList()); + + // Count how many times each major version appears + List majorVersions = jdks.stream() + .map(jdk -> Integer.parseInt(jdk.version().split("[.\\-+]")[0])) + .collect(Collectors.toList()); + + // Each major version should appear at most once (after filtering) + long uniqueVersions = majorVersions.stream().distinct().count(); + assertThat((long) majorVersions.size(), greaterThanOrEqualTo(uniqueVersions)); + } + + @Test + public void testAvailableFoojayJdkCreation() { + List jdks = installer.listAvailable().collect(Collectors.toList()); + + assertThat(jdks, is(not(empty()))); + + // Verify that the JDKs are of the correct type + for (Jdk.AvailableJdk jdk : jdks) { + assertThat(jdk, is(instanceOf(FoojayJdkInstaller.AvailableFoojayJdk.class))); + + // Verify essential properties + assertThat(jdk.id(), is(not(emptyString()))); + assertThat(jdk.version(), is(not(emptyString()))); + assertThat(jdk.tags(), is(not(empty()))); + assertThat(jdk.provider(), is(provider)); + } + } + + @Test + public void testDistroSortingOrder() { + // Test that JDKs are sorted by distribution preference + List jdks = installer.listAvailable().collect(Collectors.toList()); + + assertThat(jdks, is(not(empty()))); + + // When filtering by same major version, the preferred distro should come first + // This is based on the distro configuration (default: "temurin,aoj") + List majorVersions = jdks.stream() + .map(jdk -> Integer.parseInt(jdk.version().split("[.\\-+]")[0])) + .distinct() + .collect(Collectors.toList()); + + // Just verify we have multiple versions + assertThat(majorVersions.size(), greaterThan(0)); + } + + @Test + public void testJavaFXBundled() { + // Test JavaFX bundled detection + List jdks = installer.listAvailable().collect(Collectors.toList()); + + // Check if any JDKs have JavaFX tag (depends on test data) + long javafxCount = jdks.stream() + .filter(j -> j.tags().contains(Jdk.Default.Tags.Javafx.name())) + .count(); + + // This is just verifying the tag system works, count might be 0 + assertThat(javafxCount, greaterThanOrEqualTo(0L)); + } + + @Test + public void testMajorVersionExtraction() { + List jdks = installer.listAvailable().collect(Collectors.toList()); + + assertThat(jdks, is(not(empty()))); + + // Verify all JDKs have parseable major versions + for (Jdk.AvailableJdk jdk : jdks) { + String version = jdk.version(); + int major = Integer.parseInt(version.split("[.\\-+]")[0]); + + // Major version should be reasonable (8-30 range as of 2026) + assertThat(major, allOf(greaterThanOrEqualTo(8), lessThanOrEqualTo(30))); + } + } +} diff --git a/src/test/java/dev/jbang/devkitman/jdkinstallers/MetadataJdkInstallerTest.java b/src/test/java/dev/jbang/devkitman/jdkinstallers/MetadataJdkInstallerTest.java index c9b5158..9c8e440 100644 --- a/src/test/java/dev/jbang/devkitman/jdkinstallers/MetadataJdkInstallerTest.java +++ b/src/test/java/dev/jbang/devkitman/jdkinstallers/MetadataJdkInstallerTest.java @@ -1,294 +1,294 @@ -package dev.jbang.devkitman.jdkinstallers; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; -import static org.junit.jupiter.api.Assertions.*; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import dev.jbang.devkitman.BaseTest; -import dev.jbang.devkitman.Jdk; -import dev.jbang.devkitman.JdkManager; -import dev.jbang.devkitman.jdkproviders.JBangJdkProvider; -import dev.jbang.devkitman.util.FunctionWithError; -import dev.jbang.devkitman.util.RemoteAccessProvider; - -public class MetadataJdkInstallerTest extends BaseTest { - - private MetadataJdkInstaller installer; - private JBangJdkProvider provider; - private Path testJdkFile; - - @BeforeEach - @Override - protected void initEnv(@TempDir Path tempPath) throws IOException { - super.initEnv(tempPath); - - // Copy test JDK file for installation tests - testJdkFile = tempPath.resolve("jdk-12.zip"); - Files.copy( - getClass().getResourceAsStream("/jdk-12.zip"), - testJdkFile, - java.nio.file.StandardCopyOption.REPLACE_EXISTING); - - RemoteAccessProvider rap = createRemoteAccessProvider(); - provider = new JBangJdkProvider(config.installPath()); - installer = new MetadataJdkInstaller(provider) - .distro("temurin") - .jvmImpl("hotspot") - .remoteAccessProvider(rap); - provider.installer(installer); - - // Create a manager so the provider has access to defaultJavaVersion - JdkManager manager = JdkManager.builder() - .providers(provider) - .build(); - } - - private RemoteAccessProvider createRemoteAccessProvider() { - return new RemoteAccessProvider() { - @Override - public Path downloadFromUrl(String url) throws IOException { - // Return our test JDK file for any download request - return testJdkFile; - } - - @Override - public T resultFromUrl(String url, FunctionWithError streamToObject) - throws IOException { - // Verify the URL format matches expected metadata API pattern - if (!url.startsWith(MetadataJdkInstaller.METADATA_BASE_URL)) { - throw new IOException("Unexpected URL: " + url); - } - // Return our test metadata JSON for all requests - return streamToObject.apply( - getClass().getResourceAsStream("/testMetadataInstall.json")); - } - }; - } - - @Test - public void testListAvailable() { - List jdks = installer.listAvailable().collect(Collectors.toList()); - - // Should have JDKs from test data (excluding duplicate major versions and - // filtered EAs) - assertThat(jdks, is(not(empty()))); - - // Verify we have expected major versions (23, 21, 17, 11) - // 24 is EA and should be filtered if 24 GA exists (it doesn't in our test data) - List versions = jdks.stream() - .map(jdk -> Integer.parseInt(jdk.version().split("[.\\-+]")[0])) - .distinct() - .sorted() - .collect(Collectors.toList()); - - assertThat(versions, hasItems(11, 17, 21, 23)); - } - - @Test - public void testListAvailableOrderedByVersionDescending() { - List jdks = installer.listAvailable().collect(Collectors.toList()); - - assertThat(jdks.size(), greaterThan(0)); - - // First JDK should be the highest version (24 EA in our test data) - String firstVersion = jdks.get(0).version(); - int firstMajor = Integer.parseInt(firstVersion.split("[.\\-+]")[0]); - assertThat(firstMajor, is(24)); - } - - @Test - public void testGetAvailableByVersionExact() { - Jdk.AvailableJdk jdk21 = installer.getAvailableByVersion(21, false); - - assertThat(jdk21, is(notNullValue())); - assertThat(jdk21.version(), startsWith("21.")); - assertTrue(jdk21.tags().contains(Jdk.Default.Tags.Ga.name())); - assertTrue(jdk21.tags().contains(Jdk.Default.Tags.Jdk.name())); - } - - @Test - public void testGetAvailableByVersionOpen() { - // Request version 17+, should return newest available (23 in our test data) - Jdk.AvailableJdk jdk = installer.getAvailableByVersion(17, true); - - assertThat(jdk, is(notNullValue())); - String version = jdk.version(); - int major = Integer.parseInt(version.split("[.\\-+]")[0]); - assertThat(major, greaterThanOrEqualTo(17)); - } - - @Test - public void testGetAvailableByVersionNotFound() { - // Request version that doesn't exist in our test data - Jdk.AvailableJdk jdk = installer.getAvailableByVersion(8, false); - - assertThat(jdk, is(nullValue())); - } - - @Test - public void testDetermineIdIncludesDistro() { - List jdks = installer.listAvailable().collect(Collectors.toList()); - - assertThat(jdks, is(not(empty()))); - - // All IDs should contain the vendor name - for (Jdk.AvailableJdk jdk : jdks) { - assertThat(jdk.id(), containsString("temurin")); - } - } - - @Test - public void testDetermineIdForJre() { - // Find the JRE in our test data (11.0.25+9 JRE) - List jdks = installer.listAvailable().collect(Collectors.toList()); - - Jdk.AvailableJdk jre = jdks.stream() - .filter(j -> j.tags().contains(Jdk.Default.Tags.Jre.name())) - .findFirst() - .orElse(null); - - assertThat(jre, is(notNullValue())); - assertThat(jre.id(), containsString("-jre")); - } - - @Test - public void testDetermineTagsForGa() { - Jdk.AvailableJdk jdk17 = installer.getAvailableByVersion(17, false); - - assertThat(jdk17, is(notNullValue())); - Set tags = jdk17.tags(); - - assertThat(tags, hasItem(Jdk.Default.Tags.Ga.name())); - assertThat(tags, hasItem(Jdk.Default.Tags.Jdk.name())); - } - - @Test - public void testDetermineTagsForEa() { - // Our test data has version 24 as EA - List jdks = installer.listAvailable().collect(Collectors.toList()); - - Jdk.AvailableJdk ea = jdks.stream() - .filter(j -> j.version().contains("24")) - .findFirst() - .orElse(null); - - if (ea != null) { - Set tags = ea.tags(); - assertThat(tags, hasItem(Jdk.Default.Tags.Ea.name())); - } - } - - @Test - public void testDetermineTagsForJre() { - List jdks = installer.listAvailable().collect(Collectors.toList()); - - Jdk.AvailableJdk jre = jdks.stream() - .filter(j -> j.tags().contains(Jdk.Default.Tags.Jre.name())) - .findFirst() - .orElse(null); - - assertThat(jre, is(notNullValue())); - assertThat(jre.tags(), hasItem(Jdk.Default.Tags.Jre.name())); - assertThat(jre.tags(), not(hasItem(Jdk.Default.Tags.Jdk.name()))); - } - - @Test - public void testInstallJdk() throws IOException { - Jdk.AvailableJdk jdk17 = installer.getAvailableByVersion(17, false); - assertThat(jdk17, is(notNullValue())); - - Path installDir = config.installPath().resolve("test-jdk-17"); - Files.createDirectories(installDir.getParent()); - - Jdk.InstalledJdk installed = installer.install(jdk17, installDir); - - assertThat(installed, is(notNullValue())); - assertThat(Files.exists(installDir), is(true)); - assertThat(Files.isDirectory(installDir), is(true)); - } - - @Test - public void testInstallJdkInvalidType() { - // Create a mock JDK from a different installer type - Jdk.AvailableJdk mockJdk = new Jdk.AvailableJdk.Default( - provider, - "mock-jdk-21", - "21.0.0", - Set.of(Jdk.Default.Tags.Ga.name())); - - Path installDir = config.installPath().resolve("test-jdk-mock"); - - IllegalArgumentException exception = assertThrows( - IllegalArgumentException.class, - () -> installer.install(mockJdk, installDir)); - - assertThat(exception.getMessage(), containsString("MetadataJdkInstaller can only install")); - } - - @Test - public void testUninstallJdk() throws IOException { - // Create a mock installed JDK - Path jdkPath = config.installPath().resolve("test-jdk-uninstall"); - initMockJdkDir(jdkPath, "17.0.13"); - - Jdk.InstalledJdk jdk = new Jdk.InstalledJdk.Default( - provider, - "17.0.13-temurin-jbang", - jdkPath, - "17.0.13", - Set.of(Jdk.Default.Tags.Ga.name())); - - assertThat(Files.exists(jdkPath), is(true)); - - installer.uninstall(jdk); - - // JDK directory should be removed - assertThat(Files.exists(jdkPath), is(false)); - } - - @Test - public void testFilterEAWhenGAExists() { - // In our test data, if we had both EA and GA for same version, - // EA should be filtered out - List jdks = installer.listAvailable().collect(Collectors.toList()); - - // Count how many times each major version appears - List majorVersions = jdks.stream() - .map(jdk -> Integer.parseInt(jdk.version().split("[.\\-+]")[0])) - .collect(Collectors.toList()); - - // Each major version should appear at most once (after filtering) - long uniqueVersions = majorVersions.stream().distinct().count(); - assertThat((long) majorVersions.size(), greaterThanOrEqualTo(uniqueVersions)); - } - - @Test - public void testAvailableMetadataJdkCreation() { - List jdks = installer.listAvailable().collect(Collectors.toList()); - - assertThat(jdks, is(not(empty()))); - - // Verify that the JDKs are of the correct type - for (Jdk.AvailableJdk jdk : jdks) { - assertThat(jdk, is(instanceOf(MetadataJdkInstaller.AvailableMetadataJdk.class))); - - // Verify essential properties - assertThat(jdk.id(), is(not(emptyString()))); - assertThat(jdk.version(), is(not(emptyString()))); - assertThat(jdk.tags(), is(not(empty()))); - assertThat(jdk.provider(), is(provider)); - } - } -} +package dev.jbang.devkitman.jdkinstallers; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import dev.jbang.devkitman.BaseTest; +import dev.jbang.devkitman.Jdk; +import dev.jbang.devkitman.JdkManager; +import dev.jbang.devkitman.jdkproviders.JBangJdkProvider; +import dev.jbang.devkitman.util.FunctionWithError; +import dev.jbang.devkitman.util.RemoteAccessProvider; + +public class MetadataJdkInstallerTest extends BaseTest { + + private MetadataJdkInstaller installer; + private JBangJdkProvider provider; + private Path testJdkFile; + + @BeforeEach + @Override + protected void initEnv(@TempDir Path tempPath) throws IOException { + super.initEnv(tempPath); + + // Copy test JDK file for installation tests + testJdkFile = tempPath.resolve("jdk-12.zip"); + Files.copy( + getClass().getResourceAsStream("/jdk-12.zip"), + testJdkFile, + java.nio.file.StandardCopyOption.REPLACE_EXISTING); + + RemoteAccessProvider rap = createRemoteAccessProvider(); + provider = new JBangJdkProvider(config.installPath()); + installer = new MetadataJdkInstaller(provider) + .distro("temurin") + .jvmImpl("hotspot") + .remoteAccessProvider(rap); + provider.installer(installer); + + // Create a manager so the provider has access to defaultJavaVersion + JdkManager manager = JdkManager.builder() + .providers(provider) + .build(); + } + + private RemoteAccessProvider createRemoteAccessProvider() { + return new RemoteAccessProvider() { + @Override + public Path downloadFromUrl(String url) throws IOException { + // Return our test JDK file for any download request + return testJdkFile; + } + + @Override + public T resultFromUrl(String url, FunctionWithError streamToObject) + throws IOException { + // Verify the URL format matches expected metadata API pattern + if (!url.startsWith(MetadataJdkInstaller.METADATA_BASE_URL)) { + throw new IOException("Unexpected URL: " + url); + } + // Return our test metadata JSON for all requests + return streamToObject.apply( + getClass().getResourceAsStream("/testMetadataInstall.json")); + } + }; + } + + @Test + public void testListAvailable() { + List jdks = installer.listAvailable().collect(Collectors.toList()); + + // Should have JDKs from test data (excluding duplicate major versions and + // filtered EAs) + assertThat(jdks, is(not(empty()))); + + // Verify we have expected major versions (23, 21, 17, 11) + // 24 is EA and should be filtered if 24 GA exists (it doesn't in our test data) + List versions = jdks.stream() + .map(jdk -> Integer.parseInt(jdk.version().split("[.\\-+]")[0])) + .distinct() + .sorted() + .collect(Collectors.toList()); + + assertThat(versions, hasItems(11, 17, 21, 23)); + } + + @Test + public void testListAvailableOrderedByVersionDescending() { + List jdks = installer.listAvailable().collect(Collectors.toList()); + + assertThat(jdks.size(), greaterThan(0)); + + // First JDK should be the highest version (24 EA in our test data) + String firstVersion = jdks.get(0).version(); + int firstMajor = Integer.parseInt(firstVersion.split("[.\\-+]")[0]); + assertThat(firstMajor, is(24)); + } + + @Test + public void testGetAvailableByVersionExact() { + Jdk.AvailableJdk jdk21 = installer.getAvailableByVersion(21, false); + + assertThat(jdk21, is(notNullValue())); + assertThat(jdk21.version(), startsWith("21.")); + assertTrue(jdk21.tags().contains(Jdk.Default.Tags.Ga.name())); + assertTrue(jdk21.tags().contains(Jdk.Default.Tags.Jdk.name())); + } + + @Test + public void testGetAvailableByVersionOpen() { + // Request version 17+, should return newest available (23 in our test data) + Jdk.AvailableJdk jdk = installer.getAvailableByVersion(17, true); + + assertThat(jdk, is(notNullValue())); + String version = jdk.version(); + int major = Integer.parseInt(version.split("[.\\-+]")[0]); + assertThat(major, greaterThanOrEqualTo(17)); + } + + @Test + public void testGetAvailableByVersionNotFound() { + // Request version that doesn't exist in our test data + Jdk.AvailableJdk jdk = installer.getAvailableByVersion(8, false); + + assertThat(jdk, is(nullValue())); + } + + @Test + public void testDetermineIdIncludesDistro() { + List jdks = installer.listAvailable().collect(Collectors.toList()); + + assertThat(jdks, is(not(empty()))); + + // All IDs should contain the vendor name + for (Jdk.AvailableJdk jdk : jdks) { + assertThat(jdk.id(), containsString("temurin")); + } + } + + @Test + public void testDetermineIdForJre() { + // Find the JRE in our test data (11.0.25+9 JRE) + List jdks = installer.listAvailable().collect(Collectors.toList()); + + Jdk.AvailableJdk jre = jdks.stream() + .filter(j -> j.tags().contains(Jdk.Default.Tags.Jre.name())) + .findFirst() + .orElse(null); + + assertThat(jre, is(notNullValue())); + assertThat(jre.id(), containsString("-jre")); + } + + @Test + public void testDetermineTagsForGa() { + Jdk.AvailableJdk jdk17 = installer.getAvailableByVersion(17, false); + + assertThat(jdk17, is(notNullValue())); + Set tags = jdk17.tags(); + + assertThat(tags, hasItem(Jdk.Default.Tags.Ga.name())); + assertThat(tags, hasItem(Jdk.Default.Tags.Jdk.name())); + } + + @Test + public void testDetermineTagsForEa() { + // Our test data has version 24 as EA + List jdks = installer.listAvailable().collect(Collectors.toList()); + + Jdk.AvailableJdk ea = jdks.stream() + .filter(j -> j.version().contains("24")) + .findFirst() + .orElse(null); + + if (ea != null) { + Set tags = ea.tags(); + assertThat(tags, hasItem(Jdk.Default.Tags.Ea.name())); + } + } + + @Test + public void testDetermineTagsForJre() { + List jdks = installer.listAvailable().collect(Collectors.toList()); + + Jdk.AvailableJdk jre = jdks.stream() + .filter(j -> j.tags().contains(Jdk.Default.Tags.Jre.name())) + .findFirst() + .orElse(null); + + assertThat(jre, is(notNullValue())); + assertThat(jre.tags(), hasItem(Jdk.Default.Tags.Jre.name())); + assertThat(jre.tags(), not(hasItem(Jdk.Default.Tags.Jdk.name()))); + } + + @Test + public void testInstallJdk() throws IOException { + Jdk.AvailableJdk jdk17 = installer.getAvailableByVersion(17, false); + assertThat(jdk17, is(notNullValue())); + + Path installDir = config.installPath().resolve("test-jdk-17"); + Files.createDirectories(installDir.getParent()); + + Jdk.InstalledJdk installed = installer.install(jdk17, installDir); + + assertThat(installed, is(notNullValue())); + assertThat(Files.exists(installDir), is(true)); + assertThat(Files.isDirectory(installDir), is(true)); + } + + @Test + public void testInstallJdkInvalidType() { + // Create a mock JDK from a different installer type + Jdk.AvailableJdk mockJdk = new Jdk.AvailableJdk.Default( + provider, + "mock-jdk-21", + "21.0.0", + Set.of(Jdk.Default.Tags.Ga.name())); + + Path installDir = config.installPath().resolve("test-jdk-mock"); + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> installer.install(mockJdk, installDir)); + + assertThat(exception.getMessage(), containsString("MetadataJdkInstaller can only install")); + } + + @Test + public void testUninstallJdk() throws IOException { + // Create a mock installed JDK + Path jdkPath = config.installPath().resolve("test-jdk-uninstall"); + initMockJdkDir(jdkPath, "17.0.13"); + + Jdk.InstalledJdk jdk = new Jdk.InstalledJdk.Default( + provider, + "17.0.13-temurin-jbang", + jdkPath, + "17.0.13", + Set.of(Jdk.Default.Tags.Ga.name())); + + assertThat(Files.exists(jdkPath), is(true)); + + installer.uninstall(jdk); + + // JDK directory should be removed + assertThat(Files.exists(jdkPath), is(false)); + } + + @Test + public void testFilterEAWhenGAExists() { + // In our test data, if we had both EA and GA for same version, + // EA should be filtered out + List jdks = installer.listAvailable().collect(Collectors.toList()); + + // Count how many times each major version appears + List majorVersions = jdks.stream() + .map(jdk -> Integer.parseInt(jdk.version().split("[.\\-+]")[0])) + .collect(Collectors.toList()); + + // Each major version should appear at most once (after filtering) + long uniqueVersions = majorVersions.stream().distinct().count(); + assertThat((long) majorVersions.size(), greaterThanOrEqualTo(uniqueVersions)); + } + + @Test + public void testAvailableMetadataJdkCreation() { + List jdks = installer.listAvailable().collect(Collectors.toList()); + + assertThat(jdks, is(not(empty()))); + + // Verify that the JDKs are of the correct type + for (Jdk.AvailableJdk jdk : jdks) { + assertThat(jdk, is(instanceOf(MetadataJdkInstaller.AvailableMetadataJdk.class))); + + // Verify essential properties + assertThat(jdk.id(), is(not(emptyString()))); + assertThat(jdk.version(), is(not(emptyString()))); + assertThat(jdk.tags(), is(not(empty()))); + assertThat(jdk.provider(), is(provider)); + } + } +} diff --git a/src/test/java/dev/jbang/devkitman/jdkproviders/SdkmanJdkProviderTest.java b/src/test/java/dev/jbang/devkitman/jdkproviders/SdkmanJdkProviderTest.java index c825148..7cb9e42 100644 --- a/src/test/java/dev/jbang/devkitman/jdkproviders/SdkmanJdkProviderTest.java +++ b/src/test/java/dev/jbang/devkitman/jdkproviders/SdkmanJdkProviderTest.java @@ -1,68 +1,68 @@ -package dev.jbang.devkitman.jdkproviders; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.hasSize; -import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.Matchers.notNullValue; - -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.List; -import java.util.stream.Collectors; - -import org.hamcrest.Matchers; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; - -import dev.jbang.devkitman.BaseTest; -import dev.jbang.devkitman.Jdk; -import dev.jbang.devkitman.util.FileUtils; - -public class SdkmanJdkProviderTest extends BaseTest { - @ParameterizedTest - @CsvSource({ - "25.0.1-tem,25.0.1,25+", - "21-tem,21.0.7,21+", - "25.0.2-zulu,25.0.2,25+", - "25.0.2.fx-zulu,25.0.2,25+", - "21.0.10-librca,21.0.10,21+", - "25-graal,25,25+", - "26.ea.13-graal,26.ea.13,26+", - "22.1.0.1.r17-gln,22.1.0.1.r17,22+" - }) - void testSdkmanProviderFindsInstalledJdkByVersionPattern( - String folderName, String releaseVersion, String requestedVersion) { - Path jdkHome = installSdkmanJdk(folderName, releaseVersion); - Jdk.InstalledJdk jdk = jdkManager("sdkman").getInstalledJdk(requestedVersion); - - assertThat(jdk, notNullValue()); - assertThat(jdk.provider(), instanceOf(SdkmanJdkProvider.class)); - assertThat(jdk.home(), Matchers.is(jdkHome)); - assertThat(jdk.id(), Matchers.is(folderName)); - } - - @Test - void testSdkmanProviderIgnoresCurrentSymlink() { - installSdkmanJdk("25.0.1-tem", "25.0.1"); - Path jdkHome = sdkmanJdksRoot().resolve("25.0.1-tem"); - FileUtils.createLink(sdkmanJdksRoot().resolve("current"), jdkHome); - - List ids = jdkManager("sdkman").listInstalledJdks() - .stream() - .map(Jdk::id) - .collect(Collectors.toList()); - assertThat(ids, hasSize(1)); - assertThat(ids, Matchers.contains("25.0.1-tem")); - } - - private Path sdkmanJdksRoot() { - return Paths.get(System.getProperty("user.home")).resolve(".sdkman/candidates/java"); - } - - private Path installSdkmanJdk(String folderName, String releaseVersion) { - Path jdkHome = sdkmanJdksRoot().resolve(folderName); - initMockJdkDir(jdkHome, releaseVersion); - return jdkHome; - } -} +package dev.jbang.devkitman.jdkproviders; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.notNullValue; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Collectors; + +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import dev.jbang.devkitman.BaseTest; +import dev.jbang.devkitman.Jdk; +import dev.jbang.devkitman.util.FileUtils; + +public class SdkmanJdkProviderTest extends BaseTest { + @ParameterizedTest + @CsvSource({ + "25.0.1-tem,25.0.1,25+", + "21-tem,21.0.7,21+", + "25.0.2-zulu,25.0.2,25+", + "25.0.2.fx-zulu,25.0.2,25+", + "21.0.10-librca,21.0.10,21+", + "25-graal,25,25+", + "26.ea.13-graal,26.ea.13,26+", + "22.1.0.1.r17-gln,22.1.0.1.r17,22+" + }) + void testSdkmanProviderFindsInstalledJdkByVersionPattern( + String folderName, String releaseVersion, String requestedVersion) { + Path jdkHome = installSdkmanJdk(folderName, releaseVersion); + Jdk.InstalledJdk jdk = jdkManager("sdkman").getInstalledJdk(requestedVersion); + + assertThat(jdk, notNullValue()); + assertThat(jdk.provider(), instanceOf(SdkmanJdkProvider.class)); + assertThat(jdk.home(), Matchers.is(jdkHome)); + assertThat(jdk.id(), Matchers.is(folderName)); + } + + @Test + void testSdkmanProviderIgnoresCurrentSymlink() { + installSdkmanJdk("25.0.1-tem", "25.0.1"); + Path jdkHome = sdkmanJdksRoot().resolve("25.0.1-tem"); + FileUtils.createLink(sdkmanJdksRoot().resolve("current"), jdkHome); + + List ids = jdkManager("sdkman").listInstalledJdks() + .stream() + .map(Jdk::id) + .collect(Collectors.toList()); + assertThat(ids, hasSize(1)); + assertThat(ids, Matchers.contains("25.0.1-tem")); + } + + private Path sdkmanJdksRoot() { + return Paths.get(System.getProperty("user.home")).resolve(".sdkman/candidates/java"); + } + + private Path installSdkmanJdk(String folderName, String releaseVersion) { + Path jdkHome = sdkmanJdksRoot().resolve(folderName); + initMockJdkDir(jdkHome, releaseVersion); + return jdkHome; + } +} diff --git a/src/test/java/dev/jbang/devkitman/util/TestRemoteAccessProvider.java b/src/test/java/dev/jbang/devkitman/util/TestRemoteAccessProvider.java index ada8d24..1d39b28 100644 --- a/src/test/java/dev/jbang/devkitman/util/TestRemoteAccessProvider.java +++ b/src/test/java/dev/jbang/devkitman/util/TestRemoteAccessProvider.java @@ -1,35 +1,35 @@ -package dev.jbang.devkitman.util; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Map; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import dev.jbang.devkitman.BaseTest; - -public class TestRemoteAccessProvider extends BaseTest { - - @Test - void testDefaultReadJsonFromUrl(@TempDir Path cacheDir) throws IOException { - RemoteAccessProvider rap = RemoteAccessProvider.createDefaultRemoteAccessProvider(cacheDir); - String url = "https://raw.githubusercontent.com/jbangdev/jbang-devkitman/refs/heads/main/renovate.json"; - Object json = RemoteAccessProvider.readJsonFromUrl(rap, url, Object.class); - assertThat(json, instanceOf(Map.class)); - } - - @Test - void testDefaultCache(@TempDir Path cacheDir) throws IOException { - RemoteAccessProvider rap = RemoteAccessProvider.createDefaultRemoteAccessProvider(cacheDir); - String url = "https://raw.githubusercontent.com/jbangdev/jbang-devkitman/refs/heads/main/renovate.json"; - Object json = RemoteAccessProvider.readJsonFromUrl(rap, url, Object.class); - assertThat(json, instanceOf(Map.class)); - assertThat(Files.exists(cacheDir), is(true)); - assertThat(Files.list(cacheDir).count(), greaterThan(0L)); - } -} +package dev.jbang.devkitman.util; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import dev.jbang.devkitman.BaseTest; + +public class TestRemoteAccessProvider extends BaseTest { + + @Test + void testDefaultReadJsonFromUrl(@TempDir Path cacheDir) throws IOException { + RemoteAccessProvider rap = RemoteAccessProvider.createDefaultRemoteAccessProvider(cacheDir); + String url = "https://raw.githubusercontent.com/jbangdev/jbang-devkitman/refs/heads/main/renovate.json"; + Object json = RemoteAccessProvider.readJsonFromUrl(rap, url, Object.class); + assertThat(json, instanceOf(Map.class)); + } + + @Test + void testDefaultCache(@TempDir Path cacheDir) throws IOException { + RemoteAccessProvider rap = RemoteAccessProvider.createDefaultRemoteAccessProvider(cacheDir); + String url = "https://raw.githubusercontent.com/jbangdev/jbang-devkitman/refs/heads/main/renovate.json"; + Object json = RemoteAccessProvider.readJsonFromUrl(rap, url, Object.class); + assertThat(json, instanceOf(Map.class)); + assertThat(Files.exists(cacheDir), is(true)); + assertThat(Files.list(cacheDir).count(), greaterThan(0L)); + } +}