From 7080a41b1bbdc427c51a63fd90eb276013fef4f0 Mon Sep 17 00:00:00 2001 From: Tako Schotanus Date: Wed, 21 Jan 2026 16:49:16 +0100 Subject: [PATCH 1/8] chore: added some `@NonNull` annotations --- src/main/java/dev/jbang/devkitman/JdkDiscovery.java | 2 +- .../dev/jbang/devkitman/jdkproviders/CurrentJdkProvider.java | 2 +- .../dev/jbang/devkitman/jdkproviders/DefaultJdkProvider.java | 2 +- .../java/dev/jbang/devkitman/jdkproviders/JBangJdkProvider.java | 2 +- .../dev/jbang/devkitman/jdkproviders/JavaHomeJdkProvider.java | 2 +- .../dev/jbang/devkitman/jdkproviders/LinkedJdkProvider.java | 2 +- .../java/dev/jbang/devkitman/jdkproviders/LinuxJdkProvider.java | 2 +- .../java/dev/jbang/devkitman/jdkproviders/MiseJdkProvider.java | 2 +- .../dev/jbang/devkitman/jdkproviders/MultiHomeJdkProvider.java | 2 +- .../java/dev/jbang/devkitman/jdkproviders/PathJdkProvider.java | 2 +- .../java/dev/jbang/devkitman/jdkproviders/ScoopJdkProvider.java | 2 +- .../dev/jbang/devkitman/jdkproviders/SdkmanJdkProvider.java | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/main/java/dev/jbang/devkitman/JdkDiscovery.java b/src/main/java/dev/jbang/devkitman/JdkDiscovery.java index b5025f4..488be31 100644 --- a/src/main/java/dev/jbang/devkitman/JdkDiscovery.java +++ b/src/main/java/dev/jbang/devkitman/JdkDiscovery.java @@ -22,7 +22,7 @@ public interface JdkDiscovery { String name(); @Nullable - JdkProvider create(Config config); + JdkProvider create(@NonNull Config config); class Config { @NonNull diff --git a/src/main/java/dev/jbang/devkitman/jdkproviders/CurrentJdkProvider.java b/src/main/java/dev/jbang/devkitman/jdkproviders/CurrentJdkProvider.java index a2e1020..15b3043 100644 --- a/src/main/java/dev/jbang/devkitman/jdkproviders/CurrentJdkProvider.java +++ b/src/main/java/dev/jbang/devkitman/jdkproviders/CurrentJdkProvider.java @@ -55,7 +55,7 @@ public static class Discovery implements JdkDiscovery { } @Override - public @NonNull JdkProvider create(Config config) { + public @NonNull JdkProvider create(@NonNull Config config) { return new CurrentJdkProvider(); } } diff --git a/src/main/java/dev/jbang/devkitman/jdkproviders/DefaultJdkProvider.java b/src/main/java/dev/jbang/devkitman/jdkproviders/DefaultJdkProvider.java index 09ab937..0ab6476 100644 --- a/src/main/java/dev/jbang/devkitman/jdkproviders/DefaultJdkProvider.java +++ b/src/main/java/dev/jbang/devkitman/jdkproviders/DefaultJdkProvider.java @@ -222,7 +222,7 @@ public String name() { } @Override - public JdkProvider create(Config config) { + public JdkProvider create(@NonNull Config config) { String defaultLink = config.properties() .computeIfAbsent("link", k -> config.installPath().resolve(PROVIDER_ID).toString()); diff --git a/src/main/java/dev/jbang/devkitman/jdkproviders/JBangJdkProvider.java b/src/main/java/dev/jbang/devkitman/jdkproviders/JBangJdkProvider.java index 84fd50d..17204a3 100644 --- a/src/main/java/dev/jbang/devkitman/jdkproviders/JBangJdkProvider.java +++ b/src/main/java/dev/jbang/devkitman/jdkproviders/JBangJdkProvider.java @@ -138,7 +138,7 @@ public String name() { } @Override - public JdkProvider create(Config config) { + public JdkProvider create(@NonNull Config config) { JBangJdkProvider prov = new JBangJdkProvider(config.installPath()); return prov .installer(new FoojayJdkInstaller(prov) diff --git a/src/main/java/dev/jbang/devkitman/jdkproviders/JavaHomeJdkProvider.java b/src/main/java/dev/jbang/devkitman/jdkproviders/JavaHomeJdkProvider.java index f07cce1..887ec59 100644 --- a/src/main/java/dev/jbang/devkitman/jdkproviders/JavaHomeJdkProvider.java +++ b/src/main/java/dev/jbang/devkitman/jdkproviders/JavaHomeJdkProvider.java @@ -56,7 +56,7 @@ public String name() { } @Override - public JdkProvider create(Config config) { + public JdkProvider create(@NonNull Config config) { return new JavaHomeJdkProvider(); } } diff --git a/src/main/java/dev/jbang/devkitman/jdkproviders/LinkedJdkProvider.java b/src/main/java/dev/jbang/devkitman/jdkproviders/LinkedJdkProvider.java index 66213eb..6a438ad 100644 --- a/src/main/java/dev/jbang/devkitman/jdkproviders/LinkedJdkProvider.java +++ b/src/main/java/dev/jbang/devkitman/jdkproviders/LinkedJdkProvider.java @@ -136,7 +136,7 @@ public String name() { } @Override - public JdkProvider create(Config config) { + public JdkProvider create(@NonNull Config config) { return new LinkedJdkProvider(config.installPath()); } } diff --git a/src/main/java/dev/jbang/devkitman/jdkproviders/LinuxJdkProvider.java b/src/main/java/dev/jbang/devkitman/jdkproviders/LinuxJdkProvider.java index d5cae75..f2b27cb 100644 --- a/src/main/java/dev/jbang/devkitman/jdkproviders/LinuxJdkProvider.java +++ b/src/main/java/dev/jbang/devkitman/jdkproviders/LinuxJdkProvider.java @@ -48,7 +48,7 @@ public String name() { } @Override - public JdkProvider create(Config config) { + public JdkProvider create(@NonNull Config config) { return new LinuxJdkProvider(); } } diff --git a/src/main/java/dev/jbang/devkitman/jdkproviders/MiseJdkProvider.java b/src/main/java/dev/jbang/devkitman/jdkproviders/MiseJdkProvider.java index 59e8fff..0e0179b 100644 --- a/src/main/java/dev/jbang/devkitman/jdkproviders/MiseJdkProvider.java +++ b/src/main/java/dev/jbang/devkitman/jdkproviders/MiseJdkProvider.java @@ -41,7 +41,7 @@ public String name() { } @Override - public JdkProvider create(Config config) { + public JdkProvider create(@NonNull Config config) { return new MiseJdkProvider(); } } diff --git a/src/main/java/dev/jbang/devkitman/jdkproviders/MultiHomeJdkProvider.java b/src/main/java/dev/jbang/devkitman/jdkproviders/MultiHomeJdkProvider.java index 91bef94..7191850 100644 --- a/src/main/java/dev/jbang/devkitman/jdkproviders/MultiHomeJdkProvider.java +++ b/src/main/java/dev/jbang/devkitman/jdkproviders/MultiHomeJdkProvider.java @@ -65,7 +65,7 @@ public String name() { } @Override - public JdkProvider create(Config config) { + public JdkProvider create(@NonNull Config config) { return new MultiHomeJdkProvider(); } } diff --git a/src/main/java/dev/jbang/devkitman/jdkproviders/PathJdkProvider.java b/src/main/java/dev/jbang/devkitman/jdkproviders/PathJdkProvider.java index 572749f..7eff04b 100644 --- a/src/main/java/dev/jbang/devkitman/jdkproviders/PathJdkProvider.java +++ b/src/main/java/dev/jbang/devkitman/jdkproviders/PathJdkProvider.java @@ -60,7 +60,7 @@ public String name() { } @Override - public JdkProvider create(Config config) { + public JdkProvider create(@NonNull Config config) { return new PathJdkProvider(); } } diff --git a/src/main/java/dev/jbang/devkitman/jdkproviders/ScoopJdkProvider.java b/src/main/java/dev/jbang/devkitman/jdkproviders/ScoopJdkProvider.java index 3d11a59..f0731a4 100644 --- a/src/main/java/dev/jbang/devkitman/jdkproviders/ScoopJdkProvider.java +++ b/src/main/java/dev/jbang/devkitman/jdkproviders/ScoopJdkProvider.java @@ -66,7 +66,7 @@ public String name() { } @Override - public JdkProvider create(Config config) { + public JdkProvider create(@NonNull Config config) { return new ScoopJdkProvider(); } } diff --git a/src/main/java/dev/jbang/devkitman/jdkproviders/SdkmanJdkProvider.java b/src/main/java/dev/jbang/devkitman/jdkproviders/SdkmanJdkProvider.java index a30d52f..471a8f0 100644 --- a/src/main/java/dev/jbang/devkitman/jdkproviders/SdkmanJdkProvider.java +++ b/src/main/java/dev/jbang/devkitman/jdkproviders/SdkmanJdkProvider.java @@ -34,7 +34,7 @@ public String name() { } @Override - public JdkProvider create(Config config) { + public JdkProvider create(@NonNull Config config) { return new SdkmanJdkProvider(); } } From 69ecfe326783b4d9fe4835f8a75eb55bd213be8b Mon Sep 17 00:00:00 2001 From: Tako Schotanus Date: Wed, 21 Jan 2026 16:57:15 +0100 Subject: [PATCH 2/8] feat: installers are now discoverable --- .../dev/jbang/devkitman/JdkInstallers.java | 145 ++++++++++++++++++ .../jdkinstallers/FoojayJdkInstaller.java | 15 ++ ...ev.jbang.devkitman.JdkInstallers$Discovery | 1 + 3 files changed, 161 insertions(+) create mode 100644 src/main/java/dev/jbang/devkitman/JdkInstallers.java create mode 100644 src/main/resources/META-INF/services/dev.jbang.devkitman.JdkInstallers$Discovery diff --git a/src/main/java/dev/jbang/devkitman/JdkInstallers.java b/src/main/java/dev/jbang/devkitman/JdkInstallers.java new file mode 100644 index 0000000..f0a465c --- /dev/null +++ b/src/main/java/dev/jbang/devkitman/JdkInstallers.java @@ -0,0 +1,145 @@ +package dev.jbang.devkitman; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.function.BiFunction; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +public class JdkInstallers { + private List discoveries; + + private static final JdkInstallers INSTANCE = new JdkInstallers(); + + private JdkInstallers() { + } + + public static JdkInstallers instance() { + return INSTANCE; + } + + public static Discovery.Config config(JdkProvider jdkProvider, Map properties) { + return new Discovery.Config(jdkProvider, properties); + } + + /** + * Returns a list of names of all available installers. + * + * @return a list of installer names + */ + public List allNames() { + LinkedHashSet names = new LinkedHashSet<>(); + ArrayList sorted = new ArrayList<>(); + for (Discovery discovery : discoveries()) { + sorted.add(discovery.name()); + } + names.addAll(sorted); + return new ArrayList<>(names); + } + + public List all(Discovery.Config config) { + return parseNames(config, allNames().toArray(new String[0])); + } + + public List parseNames(Discovery.Config config, String names) { + return parseNames(config, names.split(",")); + } + + public List parseNames(Discovery.Config config, String... names) { + ArrayList installers = new ArrayList<>(); + if (names != null) { + for (String nameAndConfig : names) { + JdkInstaller installer = parseName(config, nameAndConfig); + if (installer != null) { + installers.add(installer); + } + } + } + return installers; + } + + public JdkInstaller parseName(Discovery.Config config, String nameAndConfig) { + return parseName(config, nameAndConfig, this::byName); + } + + JdkInstaller parseName( + Discovery.Config config, + String nameAndConfig, + BiFunction action) { + String[] parts = nameAndConfig.split(";"); + String name = parts[0]; + Discovery.Config cfg = config.copy(); + for (int i = 1; i < parts.length; i++) { + String[] keyValue = parts[i].split("="); + if (keyValue.length == 2) { + cfg.properties().put(keyValue[0], keyValue[1]); + } + } + return action.apply(name, cfg); + } + + public JdkInstaller byName(String name, Discovery.Config config) { + for (Discovery discovery : discoveries()) { + if (discovery.name().equals(name)) { + JdkInstaller installer = discovery.create(config); + if (installer != null) { + return installer; + } + } + } + return null; + } + + private synchronized List discoveries() { + if (discoveries == null) { + ServiceLoader loader = ServiceLoader.load(Discovery.class); + discoveries = new ArrayList<>(); + for (Discovery discovery : loader) { + discoveries.add(discovery); + } + discoveries.sort(Comparator.comparing(Discovery::name)); + } + return discoveries; + } + + public interface Discovery { + @NonNull + String name(); + + @Nullable + JdkInstaller create(Config config); + + class Config { + @NonNull + private final JdkProvider jdkProvider; + @NonNull + private final Map properties; + + public Config(@NonNull JdkProvider jdkProvider, @Nullable Map properties) { + this.jdkProvider = jdkProvider; + this.properties = new HashMap<>(); + if (properties != null) { + this.properties.putAll(properties); + } + } + + public @NonNull JdkProvider jdkProvider() { + return jdkProvider; + } + + public @NonNull Map properties() { + return properties; + } + + public Config copy() { + return new Config(jdkProvider, properties); + } + } + } +} diff --git a/src/main/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstaller.java b/src/main/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstaller.java index 225766a..1872012 100644 --- a/src/main/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstaller.java +++ b/src/main/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstaller.java @@ -22,6 +22,7 @@ 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.*; @@ -369,4 +370,18 @@ static class AvailableFoojayJdk extends Jdk.AvailableJdk.Default { 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)); + return installer; + } + } } diff --git a/src/main/resources/META-INF/services/dev.jbang.devkitman.JdkInstallers$Discovery b/src/main/resources/META-INF/services/dev.jbang.devkitman.JdkInstallers$Discovery new file mode 100644 index 0000000..c865bd5 --- /dev/null +++ b/src/main/resources/META-INF/services/dev.jbang.devkitman.JdkInstallers$Discovery @@ -0,0 +1 @@ +dev.jbang.devkitman.jdkinstallers.FoojayJdkInstaller$Discovery From 2dd81524cd3fd2ba55108ddc05046e4decb30538 Mon Sep 17 00:00:00 2001 From: Tako Schotanus Date: Wed, 21 Jan 2026 16:58:55 +0100 Subject: [PATCH 3/8] feat: added Java Metadata based installer --- .../jdkinstallers/MetadataJdkInstaller.java | 462 ++++++++++++++++++ ...ev.jbang.devkitman.JdkInstallers$Discovery | 1 + 2 files changed, 463 insertions(+) create mode 100644 src/main/java/dev/jbang/devkitman/jdkinstallers/MetadataJdkInstaller.java diff --git a/src/main/java/dev/jbang/devkitman/jdkinstallers/MetadataJdkInstaller.java b/src/main/java/dev/jbang/devkitman/jdkinstallers/MetadataJdkInstaller.java new file mode 100644 index 0000000..cf8b1b5 --- /dev/null +++ b/src/main/java/dev/jbang/devkitman/jdkinstallers/MetadataJdkInstaller.java @@ -0,0 +1,462 @@ +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.Files; +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.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; + LOGGER.log(Level.FINE, "Downloading {0}", url); + Path jdkTmpDir = jdkDir.getParent().resolve(jdkDir.getFileName() + ".tmp"); + Path jdkOldDir = jdkDir.getParent().resolve(jdkDir.getFileName() + ".old"); + FileUtils.deletePath(jdkTmpDir); + FileUtils.deletePath(jdkOldDir); + try { + Path jdkPkg = remoteAccessProvider.downloadFromUrl(url); + LOGGER.log(Level.INFO, "Installing JDK {0}...", version); + LOGGER.log(Level.FINE, "Unpacking to {0}", jdkDir); + UnpackUtils.unpackJdk(jdkPkg, jdkTmpDir); + if (Files.isDirectory(jdkDir)) { + Files.move(jdkDir, jdkOldDir); + } else if (Files.isSymbolicLink(jdkDir)) { + // This means we have a broken/invalid link + FileUtils.deletePath(jdkDir); + } + Files.move(jdkTmpDir, jdkDir); + FileUtils.deletePath(jdkOldDir); + 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) { + FileUtils.deletePath(jdkTmpDir); + if (!Files.isDirectory(jdkDir) && Files.isDirectory(jdkOldDir)) { + try { + Files.move(jdkOldDir, jdkDir); + } catch (IOException ex) { + // Ignore + } + } + String msg = "Required Java version not possible to download or install."; + 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)); + return installer; + } + } +} diff --git a/src/main/resources/META-INF/services/dev.jbang.devkitman.JdkInstallers$Discovery b/src/main/resources/META-INF/services/dev.jbang.devkitman.JdkInstallers$Discovery index c865bd5..1576825 100644 --- a/src/main/resources/META-INF/services/dev.jbang.devkitman.JdkInstallers$Discovery +++ b/src/main/resources/META-INF/services/dev.jbang.devkitman.JdkInstallers$Discovery @@ -1 +1,2 @@ dev.jbang.devkitman.jdkinstallers.FoojayJdkInstaller$Discovery +dev.jbang.devkitman.jdkinstallers.MetadataJdkInstaller$Discovery From bc460a7c2c48805770721bc6eab527abce5dc89b Mon Sep 17 00:00:00 2001 From: Tako Schotanus Date: Wed, 21 Jan 2026 17:00:10 +0100 Subject: [PATCH 4/8] test: added tests for installers --- .../java/dev/jbang/devkitman/BaseTest.java | 20 +- .../jbang/devkitman/TestJdkInstallers.java | 79 ++++ .../jdkinstallers/FoojayJdkInstallerTest.java | 349 ++++++++++++++++++ .../MetadataJdkInstallerTest.java | 294 +++++++++++++++ ...estInstall.json => testFoojayInstall.json} | 0 src/test/resources/testMetadataInstall.json | 87 +++++ 6 files changed, 822 insertions(+), 7 deletions(-) create mode 100644 src/test/java/dev/jbang/devkitman/TestJdkInstallers.java create mode 100644 src/test/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstallerTest.java create mode 100644 src/test/java/dev/jbang/devkitman/jdkinstallers/MetadataJdkInstallerTest.java rename src/test/resources/{testInstall.json => testFoojayInstall.json} (100%) create mode 100644 src/test/resources/testMetadataInstall.json diff --git a/src/test/java/dev/jbang/devkitman/BaseTest.java b/src/test/java/dev/jbang/devkitman/BaseTest.java index 7398bfd..6f799cb 100644 --- a/src/test/java/dev/jbang/devkitman/BaseTest.java +++ b/src/test/java/dev/jbang/devkitman/BaseTest.java @@ -17,6 +17,7 @@ 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; @@ -209,21 +210,26 @@ 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")) { - throw new IOException("Unexpected URL: " + url); + 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; } - 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)) { - throw new IOException("Unexpected URL: " + url); + 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")); } - return streamToObject.apply( - getClass().getResourceAsStream("/testInstall.json")); + throw new IOException("Unexpected URL: " + url); } }; diff --git a/src/test/java/dev/jbang/devkitman/TestJdkInstallers.java b/src/test/java/dev/jbang/devkitman/TestJdkInstallers.java new file mode 100644 index 0000000..0474861 --- /dev/null +++ b/src/test/java/dev/jbang/devkitman/TestJdkInstallers.java @@ -0,0 +1,79 @@ +package dev.jbang.devkitman; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; + +import java.io.IOException; +import java.nio.file.Path; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import dev.jbang.devkitman.jdkinstallers.FoojayJdkInstaller; +import dev.jbang.devkitman.jdkinstallers.MetadataJdkInstaller; + +public class TestJdkInstallers extends BaseTest { + private JdkInstallers.Discovery.Config iconfig; + + @BeforeEach + protected void initInstallerEnv(@TempDir Path tempPath) throws IOException { + iconfig = JdkInstallers.config(createJbangProvider(), java.util.Collections.emptyMap()); + } + + @Test + void testAllNames() { + assertThat( + JdkInstallers.instance().allNames(), + contains( + "foojay", + "metadata")); + } + + @Test + void testAll() { + assertThat( + JdkInstallers.instance().all(iconfig), + contains( + instanceOf(FoojayJdkInstaller.class), + instanceOf(MetadataJdkInstaller.class))); + } + + @Test + void testParseNames() { + String names = "foojay,metadata"; + assertThat( + JdkInstallers.instance().parseNames(iconfig, names), + contains( + instanceOf(FoojayJdkInstaller.class), + instanceOf(MetadataJdkInstaller.class))); + } + + @Test + void testParseNameWithConfig() { + String name = "foojay;aap=noot;mies=wim"; + assertThat( + JdkInstallers.instance() + .parseName( + iconfig, + name, + (prov, config) -> { + assertThat(prov, equalTo("foojay")); + assertThat(config.properties(), hasEntry("aap", "noot")); + assertThat(config.properties(), hasEntry("mies", "wim")); + return new FoojayJdkInstaller(createJbangProvider()); + }), + is(instanceOf(FoojayJdkInstaller.class))); + } + + @Test + void testByName() { + assertThat( + JdkInstallers.instance().byName("foojay", iconfig), + is(instanceOf(FoojayJdkInstaller.class))); + } +} diff --git a/src/test/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstallerTest.java b/src/test/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstallerTest.java new file mode 100644 index 0000000..fac8bdd --- /dev/null +++ b/src/test/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstallerTest.java @@ -0,0 +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))); + } + } +} diff --git a/src/test/java/dev/jbang/devkitman/jdkinstallers/MetadataJdkInstallerTest.java b/src/test/java/dev/jbang/devkitman/jdkinstallers/MetadataJdkInstallerTest.java new file mode 100644 index 0000000..aae7d28 --- /dev/null +++ b/src/test/java/dev/jbang/devkitman/jdkinstallers/MetadataJdkInstallerTest.java @@ -0,0 +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)); + } + } +} diff --git a/src/test/resources/testInstall.json b/src/test/resources/testFoojayInstall.json similarity index 100% rename from src/test/resources/testInstall.json rename to src/test/resources/testFoojayInstall.json diff --git a/src/test/resources/testMetadataInstall.json b/src/test/resources/testMetadataInstall.json new file mode 100644 index 0000000..f186097 --- /dev/null +++ b/src/test/resources/testMetadataInstall.json @@ -0,0 +1,87 @@ +[ + { + "vendor": "temurin", + "filename": "OpenJDK23U-jdk_x64_windows_hotspot_23.0.2_7.zip", + "release_type": "ga", + "version": "23.0.2+7", + "java_version": "23.0.2+7", + "jvm_impl": "hotspot", + "os": "windows", + "architecture": "x86_64", + "file_type": "zip", + "image_type": "jdk", + "features": [], + "url": "https://github.com/adoptium/temurin23-binaries/releases/download/jdk-23.0.2%2B7/OpenJDK23U-jdk_x64_windows_hotspot_23.0.2_7.zip", + "md5": "abc123def456", + "sha256": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "size": 212107191 + }, + { + "vendor": "temurin", + "filename": "OpenJDK21U-jdk_x64_windows_hotspot_21.0.5_11.zip", + "release_type": "ga", + "version": "21.0.5+11", + "java_version": "21.0.5+11", + "jvm_impl": "hotspot", + "os": "windows", + "architecture": "x86_64", + "file_type": "zip", + "image_type": "jdk", + "features": [], + "url": "https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.5%2B11/OpenJDK21U-jdk_x64_windows_hotspot_21.0.5_11.zip", + "md5": "xyz789abc456", + "sha256": "fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321", + "size": 195842560 + }, + { + "vendor": "temurin", + "filename": "OpenJDK17U-jdk_x64_windows_hotspot_17.0.13_11.zip", + "release_type": "ga", + "version": "17.0.13+11", + "java_version": "17.0.13+11", + "jvm_impl": "hotspot", + "os": "windows", + "architecture": "x86_64", + "file_type": "zip", + "image_type": "jdk", + "features": [], + "url": "https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.13%2B11/OpenJDK17U-jdk_x64_windows_hotspot_17.0.13_11.zip", + "md5": "111222333444", + "sha256": "aaabbbcccdddeeefff000111222333444555666777888999aaabbbcccdddeeefff", + "size": 185432000 + }, + { + "vendor": "temurin", + "filename": "OpenJDK24U-jdk_x64_windows_hotspot_24_34-ea.zip", + "release_type": "ea", + "version": "24-ea+34", + "java_version": "24-ea+34", + "jvm_impl": "hotspot", + "os": "windows", + "architecture": "x86_64", + "file_type": "zip", + "image_type": "jdk", + "features": [], + "url": "https://github.com/adoptium/temurin24-binaries/releases/download/jdk-24-ea%2B34/OpenJDK24U-jdk_x64_windows_hotspot_24_34-ea.zip", + "md5": "ea123456789", + "sha256": "ea11223344556677889900aabbccddeeff00112233445566778899aabbccddee", + "size": 140494721 + }, + { + "vendor": "temurin", + "filename": "OpenJDK11U-jre_x64_windows_hotspot_11.0.25_9.zip", + "release_type": "ga", + "version": "11.0.25+9", + "java_version": "11.0.25+9", + "jvm_impl": "hotspot", + "os": "windows", + "architecture": "x86_64", + "file_type": "zip", + "image_type": "jre", + "features": [], + "url": "https://github.com/adoptium/temurin11-binaries/releases/download/jdk-11.0.25%2B9/OpenJDK11U-jre_x64_windows_hotspot_11.0.25_9.zip", + "md5": "jre111222333", + "sha256": "jre0011223344556677889900aabbccddeeff00112233445566778899aabbccdd", + "size": 45123456 + } +] From f67439f5e73b3d9b8439a631f42ba407ae7d0d97 Mon Sep 17 00:00:00 2001 From: Tako Schotanus Date: Wed, 21 Jan 2026 17:01:34 +0100 Subject: [PATCH 5/8] chore: added extra sample script --- samples/listavailable.java | 11 +++++++++++ src/main/java/dev/jbang/devkitman/Jdk.java | 6 ++---- 2 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 samples/listavailable.java diff --git a/samples/listavailable.java b/samples/listavailable.java new file mode 100644 index 0000000..9ab6d2c --- /dev/null +++ b/samples/listavailable.java @@ -0,0 +1,11 @@ +///usr/bin/env jbang "$0" "$@" ; exit $? +//DEPS dev.jbang:devkitman:0.4.2 + +import dev.jbang.devkitman.*; + +class listavailable { + public static void main(String[] args) { + JdkManager jdkManager = JdkManager.create(); + jdkManager.listAvailableJdks().forEach(System.out::println); + } +} diff --git a/src/main/java/dev/jbang/devkitman/Jdk.java b/src/main/java/dev/jbang/devkitman/Jdk.java index aef01c5..b7ab83f 100644 --- a/src/main/java/dev/jbang/devkitman/Jdk.java +++ b/src/main/java/dev/jbang/devkitman/Jdk.java @@ -86,7 +86,7 @@ public InstalledJdk install() { @Override public String toString() { - return majorVersion() + " (" + version + ", " + id + ", " + ", " + tags + "))"; + return majorVersion() + " (" + version + ", " + id + ", " + tags + "))"; } } } @@ -171,9 +171,7 @@ public int compareTo(Jdk o) { @Override public String toString() { return majorVersion() + " (" + version + (provider.hasFixedVersions() ? " [fixed]" : " [dynamic]") - + ", " + id - + ", " - + home + ", " + tags + "))"; + + ", " + id + ", " + home + ", " + tags + "))"; } @NonNull From ce0a7a9d4d3afb2e8bcf6ff8c4e4d83d1de3eaa8 Mon Sep 17 00:00:00 2001 From: Tako Schotanus Date: Wed, 21 Jan 2026 17:02:17 +0100 Subject: [PATCH 6/8] feat: made JBang installer configurable --- .../jdkproviders/JBangJdkProvider.java | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/main/java/dev/jbang/devkitman/jdkproviders/JBangJdkProvider.java b/src/main/java/dev/jbang/devkitman/jdkproviders/JBangJdkProvider.java index 17204a3..c930c06 100644 --- a/src/main/java/dev/jbang/devkitman/jdkproviders/JBangJdkProvider.java +++ b/src/main/java/dev/jbang/devkitman/jdkproviders/JBangJdkProvider.java @@ -2,28 +2,31 @@ import java.nio.file.Path; import java.nio.file.Paths; -import java.util.*; import java.util.stream.Stream; +import dev.jbang.devkitman.jdkinstallers.FoojayJdkInstaller; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; import dev.jbang.devkitman.Jdk; import dev.jbang.devkitman.JdkDiscovery; import dev.jbang.devkitman.JdkInstaller; +import dev.jbang.devkitman.JdkInstallers; import dev.jbang.devkitman.JdkProvider; -import dev.jbang.devkitman.jdkinstallers.FoojayJdkInstaller; +import dev.jbang.devkitman.jdkinstallers.MetadataJdkInstaller; import dev.jbang.devkitman.util.FileUtils; import dev.jbang.devkitman.util.JavaUtils; /** * JBang's main JDK provider that (by default) can download and install the JDKs - * provided by the Foojay Disco API. They get installed in the user's JBang + * provided by the Java Metadata API. They get installed in the user's JBang * folder. */ public class JBangJdkProvider extends BaseFoldersJdkProvider { protected JdkInstaller jdkInstaller; + public static final String DEFAULT_INSTALLER = "foojay"; + public JBangJdkProvider() { this(getJBangJdkDir()); } @@ -140,11 +143,12 @@ public String name() { @Override public JdkProvider create(@NonNull Config config) { JBangJdkProvider prov = new JBangJdkProvider(config.installPath()); - return prov - .installer(new FoojayJdkInstaller(prov) - .distro(config.properties().getOrDefault("distro", null))); - // TODO make RAP configurable - // .remoteAccessProvider(RemoteAccessProvider.createDefaultRemoteAccessProvider(config.cachePath)); + + String instName = config.properties().getOrDefault("installer", DEFAULT_INSTALLER); + JdkInstallers.Discovery.Config instConfig = JdkInstallers.config(prov, config.properties()); + JdkInstaller installer = JdkInstallers.instance().byName(instName, instConfig); + + return prov.installer(installer); } } } From 9962d6700573979e694ec40408267d06bf55a016 Mon Sep 17 00:00:00 2001 From: Tako Schotanus Date: Mon, 26 Jan 2026 10:12:13 +0100 Subject: [PATCH 7/8] feat: added cache dir to installer configuration We now support specifying a cache directory for JDK installers. --- .../dev/jbang/devkitman/JdkInstallers.java | 26 ++++++++++++++++--- .../jdkinstallers/FoojayJdkInstaller.java | 4 +++ .../jdkinstallers/MetadataJdkInstaller.java | 4 +++ .../jdkproviders/JBangJdkProvider.java | 6 ++--- .../dev/jbang/devkitman/util/NetUtils.java | 9 ++++--- .../devkitman/util/RemoteAccessProvider.java | 24 +++++++++++++++-- .../jbang/devkitman/TestJdkInstallers.java | 2 +- 7 files changed, 62 insertions(+), 13 deletions(-) diff --git a/src/main/java/dev/jbang/devkitman/JdkInstallers.java b/src/main/java/dev/jbang/devkitman/JdkInstallers.java index f0a465c..c905dc3 100644 --- a/src/main/java/dev/jbang/devkitman/JdkInstallers.java +++ b/src/main/java/dev/jbang/devkitman/JdkInstallers.java @@ -1,5 +1,8 @@ package dev.jbang.devkitman; +import static dev.jbang.devkitman.util.FileUtils.deleteOnExit; + +import java.nio.file.Path; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; @@ -24,8 +27,8 @@ public static JdkInstallers instance() { return INSTANCE; } - public static Discovery.Config config(JdkProvider jdkProvider, Map properties) { - return new Discovery.Config(jdkProvider, properties); + public static Discovery.Config config(JdkProvider jdkProvider, Map properties, Path cachePath) { + return new Discovery.Config(jdkProvider, properties, cachePath); } /** @@ -120,13 +123,15 @@ class Config { private final JdkProvider jdkProvider; @NonNull private final Map properties; + private Path cachePath; - public Config(@NonNull JdkProvider jdkProvider, @Nullable Map properties) { + public Config(@NonNull JdkProvider jdkProvider, @Nullable Map properties, Path cachePath) { this.jdkProvider = jdkProvider; this.properties = new HashMap<>(); if (properties != null) { this.properties.putAll(properties); } + this.cachePath = cachePath; } public @NonNull JdkProvider jdkProvider() { @@ -137,8 +142,21 @@ public Config(@NonNull JdkProvider jdkProvider, @Nullable Map pr return properties; } + public @NonNull Path cachePath() { + if (cachePath == null) { + // If no cache path is set, we create a temp dir as a curtesy that will be + // deleted on exit + try { + cachePath = deleteOnExit(java.nio.file.Files.createTempDirectory("jdk-installer-cache")); + } catch (java.io.IOException e) { + throw new RuntimeException(e); + } + } + return cachePath; + } + public Config copy() { - return new Config(jdkProvider, properties); + return new Config(jdkProvider, properties, cachePath); } } } diff --git a/src/main/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstaller.java b/src/main/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstaller.java index 1872012..af17d42 100644 --- a/src/main/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstaller.java +++ b/src/main/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstaller.java @@ -14,6 +14,7 @@ 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; @@ -381,6 +382,9 @@ public static class Discovery implements JdkInstallers.Discovery { 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 cf8b1b5..79ba9ff 100644 --- a/src/main/java/dev/jbang/devkitman/jdkinstallers/MetadataJdkInstaller.java +++ b/src/main/java/dev/jbang/devkitman/jdkinstallers/MetadataJdkInstaller.java @@ -13,6 +13,7 @@ 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; @@ -456,6 +457,9 @@ public static class Discovery implements JdkInstallers.Discovery { 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/JBangJdkProvider.java b/src/main/java/dev/jbang/devkitman/jdkproviders/JBangJdkProvider.java index c930c06..5d7e359 100644 --- a/src/main/java/dev/jbang/devkitman/jdkproviders/JBangJdkProvider.java +++ b/src/main/java/dev/jbang/devkitman/jdkproviders/JBangJdkProvider.java @@ -4,7 +4,6 @@ import java.nio.file.Paths; import java.util.stream.Stream; -import dev.jbang.devkitman.jdkinstallers.FoojayJdkInstaller; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; @@ -13,7 +12,7 @@ import dev.jbang.devkitman.JdkInstaller; import dev.jbang.devkitman.JdkInstallers; import dev.jbang.devkitman.JdkProvider; -import dev.jbang.devkitman.jdkinstallers.MetadataJdkInstaller; +import dev.jbang.devkitman.jdkinstallers.FoojayJdkInstaller; import dev.jbang.devkitman.util.FileUtils; import dev.jbang.devkitman.util.JavaUtils; @@ -145,7 +144,8 @@ public JdkProvider create(@NonNull Config config) { JBangJdkProvider prov = new JBangJdkProvider(config.installPath()); String instName = config.properties().getOrDefault("installer", DEFAULT_INSTALLER); - JdkInstallers.Discovery.Config instConfig = JdkInstallers.config(prov, config.properties()); + JdkInstallers.Discovery.Config instConfig = JdkInstallers.config(prov, config.properties(), + config.cachePath()); JdkInstaller installer = JdkInstallers.instance().byName(instName, instConfig); return prov.installer(installer); diff --git a/src/main/java/dev/jbang/devkitman/util/NetUtils.java b/src/main/java/dev/jbang/devkitman/util/NetUtils.java index 35a86e7..7c555f5 100644 --- a/src/main/java/dev/jbang/devkitman/util/NetUtils.java +++ b/src/main/java/dev/jbang/devkitman/util/NetUtils.java @@ -17,6 +17,7 @@ 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 { @@ -53,12 +54,14 @@ public static T resultFromUrl( } 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(Paths.get("http-cache")); + FileHttpCacheStorage cacheStorage = new FileHttpCacheStorage(cacheDir); - // return - // HttpClientBuilder.create().setDefaultRequestConfig(DEFAULT_REQUEST_CONFIG); return CachingHttpClientBuilder.create() .setCacheConfig(cacheConfig) .setHttpCacheStorage(cacheStorage) diff --git a/src/main/java/dev/jbang/devkitman/util/RemoteAccessProvider.java b/src/main/java/dev/jbang/devkitman/util/RemoteAccessProvider.java index 24e5812..811efa7 100644 --- a/src/main/java/dev/jbang/devkitman/util/RemoteAccessProvider.java +++ b/src/main/java/dev/jbang/devkitman/util/RemoteAccessProvider.java @@ -6,6 +6,8 @@ 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; @@ -22,16 +24,34 @@ 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(url); + return NetUtils.downloadFromUrl(clientBuilder, url); } @Override public T resultFromUrl(String url, Function streamToObject) throws IOException { - return NetUtils.resultFromUrl(url, streamToObject); + return NetUtils.resultFromUrl(clientBuilder, url, streamToObject); } } } diff --git a/src/test/java/dev/jbang/devkitman/TestJdkInstallers.java b/src/test/java/dev/jbang/devkitman/TestJdkInstallers.java index 0474861..7125487 100644 --- a/src/test/java/dev/jbang/devkitman/TestJdkInstallers.java +++ b/src/test/java/dev/jbang/devkitman/TestJdkInstallers.java @@ -22,7 +22,7 @@ public class TestJdkInstallers extends BaseTest { @BeforeEach protected void initInstallerEnv(@TempDir Path tempPath) throws IOException { - iconfig = JdkInstallers.config(createJbangProvider(), java.util.Collections.emptyMap()); + iconfig = JdkInstallers.config(createJbangProvider(), java.util.Collections.emptyMap(), null); } @Test From f64be25519f3d04cd63c15ffe29a3735f7ba1703 Mon Sep 17 00:00:00 2001 From: Tako Schotanus Date: Mon, 26 Jan 2026 10:14:49 +0100 Subject: [PATCH 8/8] feat: extracted jdk package installation into public util function --- .../jdkinstallers/FoojayJdkInstaller.java | 31 +++----------- .../jdkinstallers/MetadataJdkInstaller.java | 31 +++----------- .../dev/jbang/devkitman/util/JavaUtils.java | 40 ++++++++++++++++++- 3 files changed, 51 insertions(+), 51 deletions(-) diff --git a/src/main/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstaller.java b/src/main/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstaller.java index af17d42..0700593 100644 --- a/src/main/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstaller.java +++ b/src/main/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstaller.java @@ -5,7 +5,6 @@ import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; -import java.nio.file.Files; import java.nio.file.Path; import java.util.*; import java.util.function.Function; @@ -236,39 +235,21 @@ private Comparator majorVersionSort() { "Downloading JDK {0}. Be patient, this can take several minutes...", version); String url = foojayJdk.downloadUrl; - LOGGER.log(Level.FINE, "Downloading {0}", url); - Path jdkTmpDir = jdkDir.getParent().resolve(jdkDir.getFileName() + ".tmp"); - Path jdkOldDir = jdkDir.getParent().resolve(jdkDir.getFileName() + ".old"); - FileUtils.deletePath(jdkTmpDir); - FileUtils.deletePath(jdkOldDir); + try { + LOGGER.log(Level.FINE, "Downloading {0}", url); Path jdkPkg = remoteAccessProvider.downloadFromUrl(url); + LOGGER.log(Level.INFO, "Installing JDK {0}...", version); - LOGGER.log(Level.FINE, "Unpacking to {0}", jdkDir); - UnpackUtils.unpackJdk(jdkPkg, jdkTmpDir); - if (Files.isDirectory(jdkDir)) { - Files.move(jdkDir, jdkOldDir); - } else if (Files.isSymbolicLink(jdkDir)) { - // This means we have a broken/invalid link - FileUtils.deletePath(jdkDir); - } - Files.move(jdkTmpDir, jdkDir); - FileUtils.deletePath(jdkOldDir); + 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) { - FileUtils.deletePath(jdkTmpDir); - if (!Files.isDirectory(jdkDir) && Files.isDirectory(jdkOldDir)) { - try { - Files.move(jdkOldDir, jdkDir); - } catch (IOException ex) { - // Ignore - } - } - String msg = "Required Java version not possible to download or install."; + 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); diff --git a/src/main/java/dev/jbang/devkitman/jdkinstallers/MetadataJdkInstaller.java b/src/main/java/dev/jbang/devkitman/jdkinstallers/MetadataJdkInstaller.java index 79ba9ff..69ef656 100644 --- a/src/main/java/dev/jbang/devkitman/jdkinstallers/MetadataJdkInstaller.java +++ b/src/main/java/dev/jbang/devkitman/jdkinstallers/MetadataJdkInstaller.java @@ -4,7 +4,6 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.net.URI; -import java.nio.file.Files; import java.nio.file.Path; import java.util.*; import java.util.function.Function; @@ -314,39 +313,21 @@ private Comparator majorVersionSort() { "Downloading JDK {0}. Be patient, this can take several minutes...", version); String url = metadataJdk.downloadUrl; - LOGGER.log(Level.FINE, "Downloading {0}", url); - Path jdkTmpDir = jdkDir.getParent().resolve(jdkDir.getFileName() + ".tmp"); - Path jdkOldDir = jdkDir.getParent().resolve(jdkDir.getFileName() + ".old"); - FileUtils.deletePath(jdkTmpDir); - FileUtils.deletePath(jdkOldDir); + try { + LOGGER.log(Level.FINE, "Downloading {0}", url); Path jdkPkg = remoteAccessProvider.downloadFromUrl(url); + LOGGER.log(Level.INFO, "Installing JDK {0}...", version); - LOGGER.log(Level.FINE, "Unpacking to {0}", jdkDir); - UnpackUtils.unpackJdk(jdkPkg, jdkTmpDir); - if (Files.isDirectory(jdkDir)) { - Files.move(jdkDir, jdkOldDir); - } else if (Files.isSymbolicLink(jdkDir)) { - // This means we have a broken/invalid link - FileUtils.deletePath(jdkDir); - } - Files.move(jdkTmpDir, jdkDir); - FileUtils.deletePath(jdkOldDir); + 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) { - FileUtils.deletePath(jdkTmpDir); - if (!Files.isDirectory(jdkDir) && Files.isDirectory(jdkOldDir)) { - try { - Files.move(jdkOldDir, jdkDir); - } catch (IOException ex) { - // Ignore - } - } - String msg = "Required Java version not possible to download or install."; + 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); diff --git a/src/main/java/dev/jbang/devkitman/util/JavaUtils.java b/src/main/java/dev/jbang/devkitman/util/JavaUtils.java index 0104c9e..037cfde 100644 --- a/src/main/java/dev/jbang/devkitman/util/JavaUtils.java +++ b/src/main/java/dev/jbang/devkitman/util/JavaUtils.java @@ -157,7 +157,7 @@ public static Path jre2jdk(@NonNull Path jdkHome) { return jdkHome; } - static public void safeDeleteJdk(@NonNull Path jdkHome) { + public static void safeDeleteJdk(@NonNull Path jdkHome) { if (OsUtils.isWindows()) { // On Windows we have to check nobody is currently using the JDK or we could // be causing all kinds of trouble @@ -175,4 +175,42 @@ static public void safeDeleteJdk(@NonNull Path jdkHome) { FileUtils.deletePath(jdkHome); } } + + public static void installJdk(Path jdkPkg, Path jdkDir) throws IOException { + Path jdkTmpDir = jdkDir.getParent().resolve(jdkDir.getFileName() + ".tmp"); + Path jdkOldDir = jdkDir.getParent().resolve(jdkDir.getFileName() + ".old"); + FileUtils.deletePath(jdkTmpDir); + FileUtils.deletePath(jdkOldDir); + try { + LOGGER.log(Level.FINE, "Unpacking to {0}", jdkDir); + // Unpack JDK package to temp dir + UnpackUtils.unpackJdk(jdkPkg, jdkTmpDir); + // Check if the package contains a valid JDK + Optional v = JavaUtils.resolveJavaVersionStringFromPath(jdkTmpDir); + if (!v.isPresent()) { + throw new IllegalStateException("The JDK package does not seem to contain a valid JDK"); + } + if (Files.isDirectory(jdkDir)) { + // Rename existing JDK dir to have an .old extension + Files.move(jdkDir, jdkOldDir); + } else if (Files.isSymbolicLink(jdkDir)) { + // This means we have a broken/invalid link + FileUtils.deletePath(jdkDir); + } + // Rename temp dir to the final JDK dir name + Files.move(jdkTmpDir, jdkDir); + // Delete old JDK dir + FileUtils.deletePath(jdkOldDir); + } catch (Exception e) { + FileUtils.deletePath(jdkTmpDir); + if (!Files.isDirectory(jdkDir) && Files.isDirectory(jdkOldDir)) { + try { + Files.move(jdkOldDir, jdkDir); + } catch (IOException ex) { + // Ignore + } + } + throw new IOException("Unable to install JDK", e); + } + } }