diff --git a/.justfile b/.justfile index 8a1380c..4892d77 100644 --- a/.justfile +++ b/.justfile @@ -10,7 +10,7 @@ clean: # build without tests build: - ./gradlew spotlessApply build -x test + ./gradlew spotlessApply build publishToMavenLocal -x test # run tests test: diff --git a/src/main/java/dev/jbang/devkitman/Jdk.java b/src/main/java/dev/jbang/devkitman/Jdk.java index cecdb65..ddd9374 100644 --- a/src/main/java/dev/jbang/devkitman/Jdk.java +++ b/src/main/java/dev/jbang/devkitman/Jdk.java @@ -78,11 +78,7 @@ public Default( @Override @NonNull public InstalledJdk install() { - if (!provider.canUpdate()) { - throw new UnsupportedOperationException( - "Installing a JDK is not supported by " + provider); - } - return provider.install(this); + return provider.manager().installJdk(this); } @Override @@ -101,11 +97,6 @@ interface InstalledJdk extends Jdk { @NonNull Path home(); - /** - * Determines if the JDK version is fixed or that it can change - */ - boolean isFixedVersion(); - @Override default boolean isInstalled() { return true; @@ -117,7 +108,6 @@ default boolean isInstalled() { void uninstall(); class Default extends Jdk.Default implements InstalledJdk { - private final boolean fixedVersion; @Nullable private final Path home; @@ -130,18 +120,11 @@ public Default( @NonNull String id, @Nullable Path home, @NonNull String version, - boolean fixedVersion, @Nullable Set tags) { super(provider, id, version, tags != null ? tags : determineTagsFromJdkHome(home)); - this.fixedVersion = fixedVersion; this.home = home; } - @Override - public boolean isFixedVersion() { - return fixedVersion; - } - @Override @NonNull public Path home() { @@ -154,7 +137,7 @@ public Path home() { @Override public void uninstall() { - provider.uninstall(this); + provider.manager().uninstallJdk(this); } @Override @@ -184,7 +167,8 @@ public int compareTo(Jdk o) { @Override public String toString() { - return majorVersion() + " (" + version + (isFixedVersion() ? " [fixed]" : " [dynamic]") + ", " + id + return majorVersion() + " (" + version + (provider.hasFixedVersions() ? " [fixed]" : " [dynamic]") + + ", " + id + ", " + home + ", " + tags + "))"; } @@ -310,6 +294,14 @@ public static Predicate openVersion(int version) { return jdk -> jdk.majorVersion() >= version; } + public static Predicate minVersion(int version) { + return jdk -> jdk.majorVersion() >= version; + } + + public static Predicate maxVersion(int version) { + return jdk -> jdk.majorVersion() <= version; + } + public static Predicate forVersion(String version) { int v = JavaUtils.parseJavaVersion(version); return forVersion(v, JavaUtils.isOpenVersion(version)); @@ -328,7 +320,7 @@ public static Predicate allTags(Set tags) { } public static Predicate fixedVersion() { - return InstalledJdk::isFixedVersion; + return jdk -> jdk.provider().hasFixedVersions(); } public static Predicate path(Path jdkPath) { diff --git a/src/main/java/dev/jbang/devkitman/JdkInstaller.java b/src/main/java/dev/jbang/devkitman/JdkInstaller.java index ba0e8a1..3216b9d 100644 --- a/src/main/java/dev/jbang/devkitman/JdkInstaller.java +++ b/src/main/java/dev/jbang/devkitman/JdkInstaller.java @@ -1,7 +1,7 @@ package dev.jbang.devkitman; import java.nio.file.Path; -import java.util.List; +import java.util.stream.Stream; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; @@ -23,7 +23,7 @@ public interface JdkInstaller { * @return List of Jdk objects */ @NonNull - default List listAvailable() { + default Stream listAvailable() { throw new UnsupportedOperationException( "Listing available JDKs is not supported by " + getClass().getName()); } @@ -38,7 +38,7 @@ default List listAvailable() { * @return A Jdk object or null */ default Jdk.@Nullable AvailableJdk getAvailableByVersion(int version, boolean openVersion) { - return listAvailable().stream() + return listAvailable() .filter(Jdk.Predicates.forVersion(version, openVersion)) .findFirst() .orElse(null); @@ -58,7 +58,7 @@ default List listAvailable() { * @return A Jdk object or null */ default Jdk.@Nullable AvailableJdk getAvailableByIdOrToken(String idOrToken) { - return listAvailable().stream() + return listAvailable() .filter(Jdk.Predicates.id(idOrToken)) .findFirst() .orElse(null); diff --git a/src/main/java/dev/jbang/devkitman/JdkManager.java b/src/main/java/dev/jbang/devkitman/JdkManager.java index d55c92f..9c84eec 100644 --- a/src/main/java/dev/jbang/devkitman/JdkManager.java +++ b/src/main/java/dev/jbang/devkitman/JdkManager.java @@ -14,12 +14,9 @@ import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; -import dev.jbang.devkitman.jdkproviders.DefaultJdkProvider; import dev.jbang.devkitman.jdkproviders.JBangJdkProvider; import dev.jbang.devkitman.jdkproviders.LinkedJdkProvider; -import dev.jbang.devkitman.util.FileUtils; import dev.jbang.devkitman.util.JavaUtils; -import dev.jbang.devkitman.util.OsUtils; public class JdkManager { public static final int DEFAULT_JAVA_VERSION = 21; @@ -411,7 +408,7 @@ private Jdk getJdkByVersion( } private Jdk.@Nullable AvailableJdk getAvailableJdkByVersion(int version, boolean openVersion) { - return providers(JdkProvider.Predicates.canUpdate) + return providers(JdkProvider.Predicates.canInstall) .map(p -> p.getAvailableByVersion(version, openVersion)) .filter(Objects::nonNull) .findFirst() @@ -419,56 +416,94 @@ private Jdk getJdkByVersion( } private Jdk.@Nullable AvailableJdk getAvailableJdkById(String id) { - return providers(JdkProvider.Predicates.canUpdate) + return providers(JdkProvider.Predicates.canInstall) .map(p -> p.getAvailableByIdOrToken(id)) .filter(Objects::nonNull) .findFirst() .orElse(null); } - public void uninstallJdk(Jdk.@NonNull InstalledJdk jdk) { - Jdk.InstalledJdk defaultJdk = getDefaultJdk(); - if (OsUtils.isWindows()) { - // On Windows we have to check nobody is currently using the JDK or we could - // be causing all kinds of trouble - try { - Path jdkTmpDir = jdk.home() - .getParent() - .resolve("_delete_me_" + jdk.home().getFileName().toString()); - Files.move(jdk.home(), jdkTmpDir); - Files.move(jdkTmpDir, jdk.home()); - } catch (IOException ex) { - LOGGER.log(Level.WARNING, "Cannot uninstall JDK, it's being used: {0}", jdk); - return; + Jdk.@NonNull InstalledJdk installJdk(Jdk.@NonNull AvailableJdk jdk) { + Jdk.InstalledJdk newJdk = jdk.provider().install(jdk); + + if (hasDefaultProvider() && !newJdk.provider().equals(defaultProvider)) { + // Check if we have a global default Jdk set, if not set the new JDK as default + Jdk.InstalledJdk defJdk = getDefaultJdk(); + if (defJdk == null) { + Jdk.AvailableJdk newDefJdk = defaultProvider.getAvailableByIdOrToken("default@" + newJdk.home()); + assert newDefJdk != null : "Internal error, global default JDK should always be available"; + newDefJdk.install(); + } + // Check if we have a versioned default Jdk set, if not set the new JDK as + // the default for the installed JDK's major version + int v = newJdk.majorVersion(); + Jdk.InstalledJdk defJdkVer = getDefaultJdkForVersion(v); + if (defJdkVer == null) { + Jdk.AvailableJdk newDefJdk = defaultProvider.getAvailableByIdOrToken(v + "-default@" + newJdk.home()); + assert newDefJdk != null : "Internal error, versioned default JDK should always be available"; + newDefJdk.install(); } } + return newJdk; + } + + void uninstallJdk(Jdk.@NonNull InstalledJdk jdk) { boolean resetDefault = false; - if (defaultJdk != null) { - Path defHome = defaultJdk.home(); - try { - resetDefault = Files.isSameFile(defHome, jdk.home()); - } catch (IOException ex) { - LOGGER.log(Level.WARNING, "Error while trying to reset default JDK", ex); - resetDefault = defHome.equals(jdk.home()); + boolean resetDefaultVer = false; + if (hasDefaultProvider() && !jdk.provider().equals(defaultProvider)) { + // Check if the JDK is the global default JDK, if so we need to reset it + Jdk.InstalledJdk defaultJdk = getDefaultJdk(); + if (defaultJdk != null) { + Path defHome = defaultJdk.home(); + try { + resetDefault = Files.isSameFile(defHome, jdk.home()); + } catch (IOException ex) { + LOGGER.log(Level.WARNING, "Error while trying to reset global default JDK", ex); + resetDefault = defHome.equals(jdk.home()); + } + } + // Check if the JDK is the global default JDK, if so we need to reset it + Jdk.InstalledJdk defaultJdkVer = getDefaultJdk(); + if (defaultJdkVer != null) { + Path defHome = defaultJdkVer.home(); + try { + resetDefaultVer = Files.isSameFile(defHome, jdk.home()); + } catch (IOException ex) { + LOGGER.log(Level.WARNING, "Error while trying to reset versioned default JDK", ex); + resetDefaultVer = defHome.equals(jdk.home()); + } } } - if (jdk.isInstalled()) { - FileUtils.deletePath(jdk.home()); - LOGGER.log(Level.INFO, "JDK {0} has been uninstalled", new Object[] { jdk.id() }); - } + jdk.provider().uninstall(jdk); if (resetDefault) { - Optional newjdk = nextInstalledJdk(jdk.majorVersion(), JdkProvider.Predicates.canUpdate); + Optional newjdk = nextInstalledJdk(Jdk.Predicates.minVersion(jdk.majorVersion()), + JdkProvider.Predicates.canInstall); if (!newjdk.isPresent()) { - newjdk = prevInstalledJdk(jdk.majorVersion(), JdkProvider.Predicates.canUpdate); + newjdk = prevInstalledJdk(Jdk.Predicates.maxVersion(jdk.majorVersion()), + JdkProvider.Predicates.canInstall); } if (newjdk.isPresent()) { setDefaultJdk(newjdk.get()); } else { removeDefaultJdk(); - LOGGER.log(Level.INFO, "Default JDK unset"); + LOGGER.log(Level.INFO, "Global default JDK unset"); + } + } + if (resetDefaultVer) { + int v = jdk.majorVersion(); + Optional newjdk = nextInstalledJdk(Jdk.Predicates.exactVersion(v), + JdkProvider.Predicates.canInstall); + if (!newjdk.isPresent()) { + newjdk = prevInstalledJdk(Jdk.Predicates.exactVersion(v), JdkProvider.Predicates.canInstall); + } + if (newjdk.isPresent()) { + setDefaultJdkForVersion(newjdk.get()); + } else { + removeDefaultJdkForVersion(v); + LOGGER.log(Level.INFO, "Versioned default JDK unset"); } } } @@ -486,20 +521,13 @@ public void linkToExistingJdk(Path jdkPath, String id) { if (linked == null) { throw new IllegalStateException("No provider available to link JDK"); } - if (JavaUtils.parseToInt(id, 0) == 0) { - throw new IllegalArgumentException("Invalid JDK id: " + id + ", must be a valid major version number"); - } if (!Files.isDirectory(jdkPath)) { throw new IllegalArgumentException("Unable to resolve path as directory: " + jdkPath); } - Jdk.AvailableJdk linkedJdk = linked.getAvailableByIdOrToken(id + "@" + jdkPath); + Jdk.AvailableJdk linkedJdk = linked.getAvailableByIdOrToken(id + "-linked@" + jdkPath); if (linkedJdk == null) { throw new IllegalArgumentException("Unable to create link to JDK in path: " + jdkPath); } - if (linkedJdk.majorVersion() != JavaUtils.parseToInt(id, 0)) { - throw new IllegalArgumentException( - "Linked JDK is not of the correct version: " + linkedJdk.majorVersion() + " instead of: " + id); - } LOGGER.log(Level.FINE, "Linking JDK: {0} to {1}", new Object[] { id, jdkPath }); linked.install(linkedJdk); } @@ -509,14 +537,15 @@ public void linkToExistingJdk(Path jdkPath, String id) { * available version. Returns Optional.empty() if no matching JDK * was found; * - * @param minVersion the minimal version to return + * @param jdkFilter Only return JDKs that match the filter * @param providerFilter Only return JDKs from providers that match the filter * @return an optional JDK */ private Optional nextInstalledJdk( - int minVersion, @NonNull Predicate providerFilter) { + @NonNull Predicate jdkFilter, + @NonNull Predicate providerFilter) { return listInstalledJdks(providerFilter) - .filter(jdk -> jdk.majorVersion() >= minVersion) + .filter(jdkFilter) .min(Jdk::compareTo); } @@ -525,14 +554,15 @@ private Optional nextInstalledJdk( * available version. Returns Optional.empty() if no matching JDK * was found; * - * @param maxVersion the maximum version to return + * @param jdkFilter Only return JDKs that match the filter * @param providerFilter Only return JDKs from providers that match the filter * @return an optional JDK */ private Optional prevInstalledJdk( - int maxVersion, @NonNull Predicate providerFilter) { + @NonNull Predicate jdkFilter, + @NonNull Predicate providerFilter) { return listInstalledJdks(providerFilter) - .filter(jdk -> jdk.majorVersion() <= maxVersion) + .filter(jdkFilter) .max(Jdk::compareTo); } @@ -550,39 +580,101 @@ private Optional prevAvailableJdk(int maxVersion) { .max(Jdk::compareTo); } + /** + * Returns a list of all JDKs that are available for installation. This includes + * JDKs from all active JDK providers. + * + * @return A list of Jdk.AvailableJdk objects, possibly empty + */ + @NonNull public List listAvailableJdks() { - return providers(JdkProvider.Predicates.canUpdate) - .flatMap(p -> p.listAvailable().stream()) + return providers(JdkProvider.Predicates.canInstall) + .flatMap(JdkProvider::listAvailable) .sorted(Comparator.comparingInt(Jdk::majorVersion).reversed()) .collect(Collectors.toList()); } + /** + * Returns a list of all JDKs that are currently installed. This includes JDKs + * from all active JDK providers. + * + * @return A list of Jdk.InstalledJdk objects, possibly empty + */ + @NonNull public List listInstalledJdks() { return listInstalledJdks(JdkProvider.Predicates.all).collect(Collectors.toList()); } private Stream listInstalledJdks(Predicate providerFilter) { - return providers(providerFilter).flatMap(p -> p.listInstalled().stream()); + return providers(providerFilter).flatMap(JdkProvider::listInstalled); } + /** + * Returns the default provider that is used to manage the default JDK and + * versioned defaults. + * + * @return boolean indicating if a default provider is available + */ public boolean hasDefaultProvider() { return defaultProvider != null; } + /** + * Returns a list of all JDKs that are managed by the default provider. + * + * @return A list of Jdk.InstalledJdk objects, possibly empty + */ + @NonNull + public List listDefaultJdks() { + if (hasDefaultProvider()) { + return defaultProvider.listInstalled().collect(Collectors.toList()); + } else { + return Collections.emptyList(); + } + } + + /** + * Returns the default JDK, if one is set. This is the JDK that will be used by + * default when no specific JDK is requested. + * + * @return The default JDK or null if no default is set + */ public Jdk.@Nullable InstalledJdk getDefaultJdk() { return hasDefaultProvider() - ? defaultProvider.getInstalledById( - DefaultJdkProvider.Discovery.PROVIDER_ID) + ? defaultProvider.getInstalledById("default") : null; } + /** + * Returns the default JDK for a specific major version, if one is set. This is + * the JDK that will be used by default when a specific major version is + * requested, e.g. "21" for JDK 21. + * + * @param majorVersion The major version of the JDK to return + * @return The default JDK for the given version or null if no + * default is set + */ + public Jdk.@Nullable InstalledJdk getDefaultJdkForVersion(int majorVersion) { + return hasDefaultProvider() + ? defaultProvider.getInstalledById(majorVersion + "-default") + : null; + } + + /** + * Sets the default JDK to the given JDK. This is the JDK that will be used by + * default when no specific JDK is requested. + * + * @param jdk The JDK to set as the default + * @throws IllegalArgumentException If the JDK is not installed or if it cannot + * be determined + */ public void setDefaultJdk(Jdk.@NonNull InstalledJdk jdk) { if (hasDefaultProvider()) { Jdk.InstalledJdk defJdk = getDefaultJdk(); // Check if the new jdk exists and isn't the same as the current default if (jdk.isInstalled() && !jdk.equals(defJdk)) { // Special syntax for "installing" the default JDK - Jdk.AvailableJdk newDefJdk = defaultProvider.getAvailableByIdOrToken(jdk.home().toString()); + Jdk.AvailableJdk newDefJdk = defaultProvider.getAvailableByIdOrToken("default@" + jdk.home()); if (newDefJdk == null) { throw new IllegalArgumentException( "Unable to determine Java version in given path: " + jdk.home()); @@ -593,6 +685,39 @@ public void setDefaultJdk(Jdk.@NonNull InstalledJdk jdk) { } } + /** + * Sets the default JDK for a specific major version. This is the JDK that will + * be used by default when a specific major version is requested, e.g. "21" for + * JDK 21. + * + * @param jdk The JDK to set as the default for the given major version + * @throws IllegalArgumentException If the JDK is not installed or if it cannot + * be determined + */ + public void setDefaultJdkForVersion(Jdk.@NonNull InstalledJdk jdk) { + if (hasDefaultProvider()) { + Jdk.InstalledJdk defJdk = getDefaultJdkForVersion(jdk.majorVersion()); + // Check if the new jdk exists and isn't the same as the current default + if (jdk.isInstalled() && !jdk.equals(defJdk)) { + // Special syntax for "installing" the default JDK + Jdk.AvailableJdk newDefJdk = defaultProvider + .getAvailableByIdOrToken(jdk.majorVersion() + "-default@" + jdk.home()); + if (newDefJdk == null) { + throw new IllegalArgumentException( + "Unable to determine Java version in given path: " + jdk.home()); + } + defaultProvider.install(newDefJdk); + LOGGER.log(Level.INFO, "Default JDK for version {0} set to {1}", + new Object[] { jdk.majorVersion(), jdk }); + } + } + } + + /** + * Unsets the default JDK, if one is set. This will not uninstall the JDK, but + * it will make the selection of a JDK more ambiguous, as JBang will no longer + * know which JDK to use when the user does not specify a version or id. + */ public void removeDefaultJdk() { Jdk.InstalledJdk defJdk = getDefaultJdk(); if (defJdk != null) { @@ -600,6 +725,21 @@ public void removeDefaultJdk() { } } + /** + * Unsets the default JDK for a specific major version, if one is set. This will + * not uninstall the JDK, but it will make the selection of a versioned JDK more + * ambiguous, as JBang will no longer know which JDK to use when the user does + * not specify an id. + * + * @param majorVersion The major version of the JDK to remove as default + */ + public void removeDefaultJdkForVersion(int majorVersion) { + Jdk.InstalledJdk defJdk = getDefaultJdkForVersion(majorVersion); + if (defJdk != null) { + defJdk.uninstall(); + } + } + public boolean isCurrentJdkManaged() { String jh = System.getProperty("java.home"); if (jh == null) { diff --git a/src/main/java/dev/jbang/devkitman/JdkProvider.java b/src/main/java/dev/jbang/devkitman/JdkProvider.java index 4d8ecdd..de4d42e 100644 --- a/src/main/java/dev/jbang/devkitman/JdkProvider.java +++ b/src/main/java/dev/jbang/devkitman/JdkProvider.java @@ -3,6 +3,7 @@ import java.nio.file.Path; import java.util.*; import java.util.function.Predicate; +import java.util.stream.Stream; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; @@ -23,10 +24,10 @@ public interface JdkProvider { default Jdk.@Nullable InstalledJdk createJdk(@NonNull String id, @Nullable Path home, @Nullable String version, - boolean fixedVersion, @Nullable Set tags) { + @Nullable Set tags) { Optional v = version != null ? Optional.of(version) : JavaUtils.resolveJavaVersionStringFromPath(home); if (v.isPresent()) { - return new Jdk.InstalledJdk.Default(this, id, home, v.get(), fixedVersion, tags); + return new Jdk.InstalledJdk.Default(this, id, home, v.get(), tags); } else { return null; } @@ -60,7 +61,7 @@ public interface JdkProvider { * @return List of Jdk objects, possibly empty */ @NonNull - List listInstalled(); + Stream listInstalled(); /** * Determines if a JDK of the requested version is currently installed by this @@ -74,10 +75,12 @@ public interface JdkProvider { * @return A Jdk object or null */ default Jdk.@Nullable InstalledJdk getInstalledByVersion(int version, boolean openVersion) { - return listInstalled().stream() - .filter(Jdk.Predicates.forVersion(version, openVersion)) - .findFirst() - .orElse(null); + try (Stream installed = listInstalled()) { + return installed + .filter(Jdk.Predicates.forVersion(version, openVersion)) + .findFirst() + .orElse(null); + } } /** @@ -90,10 +93,12 @@ public interface JdkProvider { */ default Jdk.@Nullable InstalledJdk getInstalledById(@NonNull String id) { if (isValidId(id)) { - return listInstalled().stream() - .filter(Jdk.Predicates.id(id)) - .findFirst() - .orElse(null); + try (Stream installed = listInstalled()) { + return installed + .filter(Jdk.Predicates.id(id)) + .findFirst() + .orElse(null); + } } return null; } @@ -107,10 +112,12 @@ public interface JdkProvider { * @return A Jdk object or null */ default Jdk.@Nullable InstalledJdk getInstalledByPath(@NonNull Path jdkPath) { - return listInstalled().stream() - .filter(Jdk.Predicates.path(jdkPath)) - .findFirst() - .orElse(null); + try (Stream installed = listInstalled()) { + return installed + .filter(Jdk.Predicates.path(jdkPath)) + .findFirst() + .orElse(null); + } } /** @@ -144,6 +151,18 @@ default boolean canUpdate() { return false; } + /** + * Determines if the JDK versions are fixed or that they can change. For + * example, providers like "default" and "linked" can have JDKs with the same id + * that refer to different versions over time, while other providers will always + * return the same JDK for a given id. + * + * @return True if the provider has fixed versions, false otherwise + */ + default boolean hasFixedVersions() { + return true; + } + /** * This method returns a set of JDKs that are available for installation. * Implementations might set the home field of the JDK objects if @@ -154,16 +173,18 @@ default boolean canUpdate() { * @return List of Jdk objects */ @NonNull - default List listAvailable() { + default Stream listAvailable() { throw new UnsupportedOperationException( "Listing available JDKs is not supported by " + getClass().getName()); } default Jdk.@Nullable AvailableJdk getAvailableByVersion(int version, boolean openVersion) { - return listAvailable().stream() - .filter(Jdk.Predicates.forVersion(version, openVersion)) - .findFirst() - .orElse(null); + try (Stream available = listAvailable()) { + return available + .filter(Jdk.Predicates.forVersion(version, openVersion)) + .findFirst() + .orElse(null); + } } /** @@ -180,14 +201,17 @@ default List listAvailable() { * @return A Jdk object or null */ default Jdk.@Nullable AvailableJdk getAvailableByIdOrToken(String idOrToken) { - return listAvailable().stream() - .filter(Jdk.Predicates.id(idOrToken)) - .findFirst() - .orElse(null); + try (Stream available = listAvailable()) { + return available + .filter(Jdk.Predicates.id(idOrToken)) + .findFirst() + .orElse(null); + } } /** - * Installs the indicated JDK + * Installs the indicated JDK. NB: Never call this method directly, always use + * Jdk.install(jdk) instead. * * @param jdk The Jdk object of the JDK to install * @return A Jdk object @@ -199,22 +223,21 @@ default List listAvailable() { } /** - * Uninstalls the indicated JDK + * Uninstalls the indicated JDK. NB: Never call this method directly, always use + * Jdk.uninstall(jdk) instead. * * @param jdk The Jdk object of the JDK to uninstall * @throws UnsupportedOperationException if the provider can not update */ default void uninstall(Jdk.@NonNull InstalledJdk jdk) { - if (!canUpdate()) { - throw new UnsupportedOperationException( - "Uninstalling a JDK is not supported by " + getClass().getName()); - } - manager().uninstallJdk(jdk); + throw new UnsupportedOperationException( + "Uninstalling JDKs is not supported by " + getClass().getName()); } class Predicates { public static final Predicate all = provider -> true; public static final Predicate canUpdate = JdkProvider::canUpdate; + public static final Predicate canInstall = p -> p.canUpdate() && p.hasFixedVersions(); public static Predicate name(String name) { return provider -> provider.name().equalsIgnoreCase(name); diff --git a/src/main/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstaller.java b/src/main/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstaller.java index 59d4bfc..04e1335 100644 --- a/src/main/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstaller.java +++ b/src/main/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstaller.java @@ -31,7 +31,7 @@ */ public class FoojayJdkInstaller implements JdkInstaller { protected final JdkProvider jdkProvider; - protected final Function versionToId; + protected final Function jdkId; protected RemoteAccessProvider remoteAccessProvider = RemoteAccessProvider.createDefaultRemoteAccessProvider(); protected String distro = DEFAULT_DISTRO; @@ -39,10 +39,6 @@ public class FoojayJdkInstaller implements JdkInstaller { public static final String DEFAULT_DISTRO = "temurin,aoj"; - private static final Comparator majorVersionSort = Comparator - .comparingInt((JdkResult jdk) -> jdk.major_version) - .reversed(); - private static final Logger LOGGER = Logger.getLogger(FoojayJdkInstaller.class.getName()); public static class JdkResultLinks { @@ -52,6 +48,7 @@ public static class JdkResultLinks { 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; @@ -62,9 +59,14 @@ public static class VersionsResponse { public List result; } - public FoojayJdkInstaller(@NonNull JdkProvider jdkProvider, @NonNull Function versionToId) { + 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.versionToId = versionToId; + this.jdkId = jdkId; } public @NonNull FoojayJdkInstaller remoteAccessProvider(@NonNull RemoteAccessProvider remoteAccessProvider) { @@ -79,13 +81,13 @@ public FoojayJdkInstaller(@NonNull JdkProvider jdkProvider, @NonNull Function listAvailable() { + public Stream listAvailable() { try { VersionsResponse res = readPackagesForList(); - return processPackages(res.result, majorVersionSort).distinct().collect(Collectors.toList()); + return processPackages(res.result, majorVersionSort()).distinct(); } catch (IOException e) { LOGGER.log(Level.FINE, "Couldn't list available JDKs", e); - return Collections.emptyList(); + return Stream.empty(); } } @@ -109,7 +111,7 @@ private VersionsResponse readPackagesForList() throws IOException { return j2.release_status.compareTo(j1.release_status); } // Prefer newer versions - return majorVersionSort.compare(j1, j2); + return majorVersionSort().compare(j1, j2); }; try { VersionsResponse res = readPackagesForVersion(version, openVersion); @@ -137,20 +139,32 @@ private Stream processPackages(List jdks, Comparato return filterEA(jdks) .stream() .sorted(sortFunc) - .map(jdk -> new AvailableFoojayJdk(jdkProvider, versionToId.apply(jdk.java_version), - jdk.java_version, jdk.links.pkg_download_redirect, determineTags(jdk))); + .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().equals(jdk.release_status)) { + if (Jdk.Default.Tags.Ga.name().equalsIgnoreCase(jdk.release_status)) { tags.add(Jdk.Default.Tags.Ga.name()); - } else if (Jdk.Default.Tags.Ea.name().equals(jdk.release_status)) { + } else if (Jdk.Default.Tags.Ea.name().equalsIgnoreCase(jdk.release_status)) { tags.add(Jdk.Default.Tags.Ea.name()); } - if (Jdk.Default.Tags.Jdk.name().equals(jdk.package_type)) { + if (Jdk.Default.Tags.Jdk.name().equalsIgnoreCase(jdk.package_type)) { tags.add(Jdk.Default.Tags.Jdk.name()); - } else if (Jdk.Default.Tags.Jre.name().equals(jdk.package_type)) { + } else if (Jdk.Default.Tags.Jre.name().equalsIgnoreCase(jdk.package_type)) { tags.add(Jdk.Default.Tags.Jre.name()); } if (jdk.javafx_bundled) { @@ -195,6 +209,18 @@ private List filterEA(List jdks) { .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, Path jdkDir) { if (!(jdk instanceof AvailableFoojayJdk)) { @@ -226,7 +252,7 @@ private List filterEA(List jdks) { } Files.move(jdkTmpDir, jdkDir); FileUtils.deletePath(jdkOldDir); - Jdk.InstalledJdk newJdk = jdkProvider.createJdk(foojayJdk.id(), jdkDir, null, true, null); + Jdk.InstalledJdk newJdk = jdkProvider.createJdk(foojayJdk.id(), jdkDir, null, null); if (newJdk == null) { throw new IllegalStateException("Cannot obtain version of recently installed JDK"); } @@ -249,9 +275,7 @@ private List filterEA(List jdks) { @Override public void uninstall(Jdk.@NonNull InstalledJdk jdk) { - if (jdk.isInstalled()) { - FileUtils.deletePath(jdk.home()); - } + JavaUtils.safeDeleteJdk(jdk.home()); } private static String getVersionsUrl(int minVersion, boolean openVersion, OsUtils.OS os, OsUtils.Arch arch, diff --git a/src/main/java/dev/jbang/devkitman/jdkproviders/BaseFoldersJdkProvider.java b/src/main/java/dev/jbang/devkitman/jdkproviders/BaseFoldersJdkProvider.java index 4568734..1afff98 100644 --- a/src/main/java/dev/jbang/devkitman/jdkproviders/BaseFoldersJdkProvider.java +++ b/src/main/java/dev/jbang/devkitman/jdkproviders/BaseFoldersJdkProvider.java @@ -3,13 +3,11 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Collections; -import java.util.List; import java.util.Objects; import java.util.function.Predicate; import java.util.logging.Level; import java.util.logging.Logger; -import java.util.stream.Collectors; +import java.util.regex.Pattern; import java.util.stream.Stream; import org.jspecify.annotations.NonNull; @@ -63,27 +61,28 @@ public boolean canUse() { @NonNull @Override - public List listInstalled() { - if (Files.isDirectory(jdksRoot)) { - try (Stream jdkPaths = listJdkPaths()) { - return jdkPaths.map(this::createJdk) - .filter(Objects::nonNull) - .collect(Collectors.toList()); - } catch (IOException e) { - LOGGER.log(Level.FINE, "Couldn't list installed JDKs", e); - } + public Stream listInstalled() { + try { + return listJdkPaths() + .map(this::createJdk) + .filter(Objects::nonNull); + } catch (IOException e) { + LOGGER.log(Level.FINE, "Couldn't list installed JDKs", e); + return Stream.empty(); } - return Collections.emptyList(); } @Override public Jdk.@Nullable InstalledJdk getInstalledById(@NonNull String id) { - return getInstalledByPath(getJdkPath(id)); + if (isValidId(id)) { + return getInstalledByPath(getJdkPath(id)); + } + return null; } @Override public Jdk.@Nullable InstalledJdk getInstalledByPath(@NonNull Path jdkPath) { - if (jdkPath.startsWith(jdksRoot) && Files.isDirectory(jdkPath) && acceptFolder(jdkPath)) { + if (acceptFolder(jdkPath)) { return createJdk(jdkPath); } return null; @@ -96,12 +95,12 @@ public List listInstalled() { * available. This only needs to be implemented for providers that are * updatable. * - * @param jdk The identifier of the JDK to install + * @param id The identifier of the JDK to install * @return A path to the requested JDK */ @NonNull - protected Path getJdkPath(@NonNull String jdk) { - return jdksRoot.resolve(jdk); + protected Path getJdkPath(@NonNull String id) { + return jdksRoot.resolve(id); } protected Predicate sameJdk(Path jdkRoot) { @@ -115,6 +114,7 @@ protected Predicate sameJdk(Path jdkRoot) { }; } + @NonNull protected Stream listJdkPaths() throws IOException { if (Files.isDirectory(jdksRoot)) { return Files.list(jdksRoot).filter(this::acceptFolder); @@ -123,27 +123,33 @@ protected Stream listJdkPaths() throws IOException { } protected Jdk.@Nullable InstalledJdk createJdk(Path home) { - return createJdk(home, true); - } - - protected Jdk.@Nullable InstalledJdk createJdk(Path home, boolean fixedVersion) { - String name = home.getFileName().toString(); if (acceptFolder(home)) { - return createJdk(jdkId(name), home, null, fixedVersion, null); + return createJdk(jdkId(home), home, null, null); } return null; } - protected String jdkId(String name) { - return name + "-" + name(); - } - protected boolean acceptFolder(@NonNull Path jdkFolder) { - return isValidId(jdkFolder.getFileName().toString()) && JavaUtils.hasJavacCmd(jdkFolder); + return jdkFolder.startsWith(jdksRoot) && isValidId(jdkFolder.getFileName().toString()) + && JavaUtils.hasJavacCmd(jdkFolder); } + private final Pattern validId = Pattern.compile("^[a-zA-Z0-9._+-]+$"); + @Override public boolean isValidId(@NonNull String id) { - return id.endsWith("-" + name()); + return id.endsWith("-" + name()) && validId.matcher(id).matches(); + } + + /** + * Returns a JDK id for the given JDK folder. By default, the id is a + * combination of the folder name and the provider name. This should only be + * called with paths that have passed the {@link #acceptFolder(Path)} check. + * + * @param jdkFolder The folder of the JDK + * @return A valid JDK id + */ + public String jdkId(@NonNull Path jdkFolder) { + return jdkFolder.getFileName().toString(); } } diff --git a/src/main/java/dev/jbang/devkitman/jdkproviders/CurrentJdkProvider.java b/src/main/java/dev/jbang/devkitman/jdkproviders/CurrentJdkProvider.java index 7d83e57..6feea2f 100644 --- a/src/main/java/dev/jbang/devkitman/jdkproviders/CurrentJdkProvider.java +++ b/src/main/java/dev/jbang/devkitman/jdkproviders/CurrentJdkProvider.java @@ -2,8 +2,7 @@ import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Collections; -import java.util.List; +import java.util.stream.Stream; import org.jspecify.annotations.NonNull; @@ -29,17 +28,22 @@ public class CurrentJdkProvider extends BaseJdkProvider { } @Override - public @NonNull List listInstalled() { + public @NonNull Stream listInstalled() { String jh = System.getProperty("java.home"); if (jh != null) { Path jdkHome = Paths.get(jh); jdkHome = JavaUtils.jre2jdk(jdkHome); - Jdk.InstalledJdk jdk = createJdk(Discovery.PROVIDER_ID, jdkHome, null, false, null); + Jdk.InstalledJdk jdk = createJdk(Discovery.PROVIDER_ID, jdkHome, null, null); if (jdk != null) { - return Collections.singletonList(jdk); + return Stream.of(jdk); } } - return Collections.emptyList(); + return Stream.empty(); + } + + @Override + public boolean hasFixedVersions() { + return false; } public static class Discovery implements JdkDiscovery { diff --git a/src/main/java/dev/jbang/devkitman/jdkproviders/DefaultJdkProvider.java b/src/main/java/dev/jbang/devkitman/jdkproviders/DefaultJdkProvider.java index b483ed3..a79b83a 100644 --- a/src/main/java/dev/jbang/devkitman/jdkproviders/DefaultJdkProvider.java +++ b/src/main/java/dev/jbang/devkitman/jdkproviders/DefaultJdkProvider.java @@ -2,12 +2,13 @@ import static dev.jbang.devkitman.Jdk.InstalledJdk.Default.determineTagsFromJdkHome; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Collections; -import java.util.List; import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; @@ -24,11 +25,13 @@ * configured for this default JDK should be stable and unchanging so it can be * added to the user's PATH. */ -public class DefaultJdkProvider extends BaseJdkProvider { +public class DefaultJdkProvider extends BaseFoldersJdkProvider { + @NonNull protected final Path defaultJdkLink; - public DefaultJdkProvider(@NonNull Path defaultJdkLink) { + public DefaultJdkProvider(@NonNull Path defaultJdkLink, @NonNull Path defaultJdkDir) { + super(defaultJdkDir); this.defaultJdkLink = defaultJdkLink; } @@ -40,59 +43,162 @@ public String name() { @Override public @NonNull String description() { - return "The JDK that is set as the default JDK."; + return "The JDKs that have been set as the default (globally or per major version)."; } /** - * This is a very special implementation that takes a path to another JDK. No - * major validation is done except for the fact that the path exists and - * contains a JDK. Any other validations must have been performed beforehand by - * the caller. + * This is a very special implementation that takes token of the form "id@path", + * where id is either the word "default" or an integer followed by "-default" + * and path is a path to another JDK. No major validation is done except for the + * fact that the path exists and contains a JDK. Any other validations must have + * been performed beforehand by the caller. * * @param idOrToken A string containing a path to an existing JDK * @return A jdk object or null */ @Override public Jdk.@Nullable AvailableJdk getAvailableByIdOrToken(String idOrToken) { - Path home = Paths.get(idOrToken); - if (Files.isDirectory(home)) { - Optional v = JavaUtils.resolveJavaVersionStringFromPath(home); - if (v.isPresent()) { - return new Jdk.AvailableJdk.Default(this, idOrToken, v.get(), determineTagsFromJdkHome(home)); + // Check if the token follows our special format + String[] parts = idOrToken.split("@", 2); + if (parts.length == 2 && isValidId(parts[0]) && FileUtils.isValidPath(parts[1])) { + Path jdkPath = Paths.get(parts[1]); + if (JavaUtils.hasJavacCmd(jdkPath)) { + Optional version = JavaUtils.resolveJavaVersionStringFromPath(jdkPath); + if (!version.isPresent()) { + throw new IllegalArgumentException( + "Unable to determine Java version in given path: " + jdkPath); + } + return new AvailableDefaultJdk(this, parts[0], version.get(), jdkPath, + determineTagsFromJdkHome(jdkPath)); } + return null; + } else { + return super.getAvailableByIdOrToken(idOrToken); } - return null; } - @NonNull @Override - public List listInstalled() { - if (Files.isDirectory(defaultJdkLink)) { - Jdk.InstalledJdk jdk = createJdk(Discovery.PROVIDER_ID, defaultJdkLink, null, false, null); - if (jdk != null) { - return Collections.singletonList(jdk); + public Jdk.@Nullable InstalledJdk getInstalledByVersion(int version, boolean openVersion) { + // First we check if the "default" link exists and has the correct version + if (acceptFolder(defaultJdkLink)) { + Optional v = JavaUtils.resolveJavaVersionStringFromPath(defaultJdkLink); + if (v.isPresent() && JavaUtils.parseJavaVersion(v.get()) == version) { + return createJdk(defaultJdkLink); } } - return Collections.emptyList(); + // Then we check if there's a link with the exact number matching the version + Path jdk = jdksRoot.resolve(Integer.toString(version)); + if (Files.isDirectory(jdk)) { + return createJdk(jdk); + } else { + // Finally we fall back to the default implementation + return super.getInstalledByVersion(version, true); + } } @Override public Jdk.@NonNull InstalledJdk install(Jdk.@NonNull AvailableJdk jdk) { - Path home = Paths.get(jdk.id()); - Jdk.InstalledJdk defJdk = getInstalledById(Discovery.PROVIDER_ID); - if (defJdk != null && defJdk.isInstalled() && !home.equals(defJdk.home())) { - uninstall(defJdk); + if (!(jdk instanceof AvailableDefaultJdk)) { + throw new IllegalArgumentException( + "DefaultJdkInstaller can only install JDKs listed as available by itself"); } + AvailableDefaultJdk availJdk = (AvailableDefaultJdk) jdk; + Jdk.InstalledJdk existingJdk = getInstalledById(availJdk.id()); + if (existingJdk != null && existingJdk.isInstalled() && !availJdk.home.equals(existingJdk.home())) { + uninstall(existingJdk); + } + Path linkPath = getJdkPath(availJdk.id()); // Remove anything that might be in the way - FileUtils.deletePath(defaultJdkLink); + FileUtils.deletePath(linkPath); // Now create the new link - FileUtils.createLink(defaultJdkLink, home); - return defJdk; + FileUtils.createLink(linkPath, availJdk.home); + Jdk.InstalledJdk newJdk = createJdk(linkPath); + if (newJdk == null) { + throw new IllegalStateException("Failed to find JDK in: " + linkPath); + } + return newJdk; } @Override public void uninstall(Jdk.@NonNull InstalledJdk jdk) { - FileUtils.deletePath(defaultJdkLink); + JavaUtils.safeDeleteJdk(jdk.home()); + } + + @Override + public boolean canUpdate() { + return true; + } + + @Override + @NonNull + protected Stream listJdkPaths() throws IOException { + if (acceptFolder(defaultJdkLink)) { + return Stream.concat(Stream.of(defaultJdkLink), + super.listJdkPaths().filter(p -> !p.equals(defaultJdkLink))); + } else { + return super.listJdkPaths(); + } + } + + @Override + @NonNull + protected Path getJdkPath(@NonNull String id) { + if (name().equals(id)) { + return defaultJdkLink; + } else { + String name = id.substring(0, id.length() - name().length() - 1); + return jdksRoot.resolve(name); + } + } + + protected boolean acceptFolder(@NonNull Path jdkFolder) { + if (!jdkFolder.equals(defaultJdkLink) && !jdkFolder.startsWith(jdksRoot)) { + return false; + } + String nm = jdkFolder.getFileName().toString(); + // Check the folder is either the default link or it's name is a number + if (!jdkFolder.equals(defaultJdkLink) && JavaUtils.parseToInt(nm, 0) == 0) { + return false; + } + return FileUtils.isLink(jdkFolder) && JavaUtils.hasJavacCmd(jdkFolder); + } + + @Override + public boolean isValidId(@NonNull String id) { + if (id.equals(name())) { + return true; + } else if (id.endsWith("-" + name())) { + String version = id.substring(0, id.length() - name().length() - 1); + return JavaUtils.parseToInt(version, 0) > 0; + } + return false; + } + + @Override + public String jdkId(@NonNull Path jdkFolder) { + if (jdkFolder.equals(defaultJdkLink)) { + return "default"; + } else { + // For backward compatibility, the default folders are named with a number + // (e.g. "11", "17", etc.) but for the id we need to add the provider name + String name = jdkFolder.getFileName().toString(); + return name + "-" + name(); + } + } + + @Override + public boolean hasFixedVersions() { + return false; + } + + static class AvailableDefaultJdk extends Jdk.AvailableJdk.Default { + public final Path home; + + AvailableDefaultJdk(@NonNull JdkProvider provider, @NonNull String id, @NonNull String version, + @NonNull Path home, @NonNull Set tags) { + super(provider, id, version, tags); + this.home = home; + } } public static class Discovery implements JdkDiscovery { @@ -109,7 +215,10 @@ public JdkProvider create(Config config) { String defaultLink = config.properties() .computeIfAbsent("link", k -> config.installPath().resolve(PROVIDER_ID).toString()); - return new DefaultJdkProvider(Paths.get(defaultLink)); + String defaultDir = config.properties() + .computeIfAbsent("dir", + k -> config.installPath().toString()); + return new DefaultJdkProvider(Paths.get(defaultLink), Paths.get(defaultDir)); } } } diff --git a/src/main/java/dev/jbang/devkitman/jdkproviders/JBangJdkProvider.java b/src/main/java/dev/jbang/devkitman/jdkproviders/JBangJdkProvider.java index c50b758..84fd50d 100644 --- a/src/main/java/dev/jbang/devkitman/jdkproviders/JBangJdkProvider.java +++ b/src/main/java/dev/jbang/devkitman/jdkproviders/JBangJdkProvider.java @@ -1,9 +1,9 @@ package dev.jbang.devkitman.jdkproviders; -import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.*; +import java.util.stream.Stream; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; @@ -30,7 +30,7 @@ public JBangJdkProvider() { public JBangJdkProvider(Path jdksRoot) { super(jdksRoot); - jdkInstaller = new FoojayJdkInstaller(this, this::jdkId); + jdkInstaller = new FoojayJdkInstaller(this); } @Override @@ -45,7 +45,7 @@ public JBangJdkProvider installer(@NonNull JdkInstaller jdkInstaller) { @NonNull @Override - public List listAvailable() { + public Stream listAvailable() { return jdkInstaller.listAvailable(); } @@ -66,63 +66,33 @@ public List listAvailable() { @Override public void uninstall(Jdk.@NonNull InstalledJdk jdk) { - super.uninstall(jdk); jdkInstaller.uninstall(jdk); } -// @Override -// public Jdk.InstalledJdk supplyJdk(@NonNull String id, @Nullable Path home, @Nullable String version) { -// return super.createJdk(id, home, version, true, null); -// } - - @Override - public Jdk.@Nullable InstalledJdk getInstalledByVersion(int version, boolean openVersion) { - Path jdk = jdksRoot.resolve(Integer.toString(version)); - if (Files.isDirectory(jdk)) { - return createJdk(jdk); - } else if (openVersion) { - return super.getInstalledByVersion(version, openVersion); - } - return null; - } - @Override public boolean canUpdate() { return true; } - @NonNull - public Path getJdksPath() { - return jdksRoot; - } - - @NonNull - @Override - public String jdkId(String name) { - int majorVersion = JavaUtils.parseJavaVersion(name); - return super.jdkId(Integer.toString(majorVersion)); - } - @Override - public boolean isValidId(@NonNull String id) { - return JavaUtils.parseToInt(id, 0) > 0; - } - - @NonNull - @Override - protected Path getJdkPath(@NonNull String jdk) { - return getJdksPath().resolve(Integer.toString(jdkVersion(jdk))); - } - - private static int jdkVersion(String jdk) { - return JavaUtils.parseJavaVersion(jdk); + protected boolean acceptFolder(@NonNull Path jdkFolder) { + // We additionally allow folders that are named with a number + // (e.g. "11", "17", etc.) for backwards compatibility with older + // JBang versions + return (super.acceptFolder(jdkFolder) || JavaUtils.parseToInt(jdkFolder.getFileName().toString(), 0) > 0) + && !FileUtils.isLink(jdkFolder); } @Override - protected boolean acceptFolder(@NonNull Path jdkFolder) { - return isValidId(jdkFolder.getFileName().toString()) - && super.acceptFolder(jdkFolder) - && !FileUtils.isLink(jdkFolder); + public String jdkId(@NonNull Path jdkFolder) { + String name = jdkFolder.getFileName().toString(); + if (JavaUtils.parseToInt(name, 0) > 0) { + // If the folder is named with a number, it means it's probably a + // JDK installed by an older JBang version so we append the + // provider name to the id to avoid naming conflicts + return name + "-" + name(); + } + return super.jdkId(jdkFolder); } public static Path getJBangJdkDir() { @@ -171,7 +141,7 @@ public String name() { public JdkProvider create(Config config) { JBangJdkProvider prov = new JBangJdkProvider(config.installPath()); return prov - .installer(new FoojayJdkInstaller(prov, prov::jdkId) + .installer(new FoojayJdkInstaller(prov) .distro(config.properties().getOrDefault("distro", null))); // TODO make RAP configurable // .remoteAccessProvider(RemoteAccessProvider.createDefaultRemoteAccessProvider(config.cachePath)); diff --git a/src/main/java/dev/jbang/devkitman/jdkproviders/JavaHomeJdkProvider.java b/src/main/java/dev/jbang/devkitman/jdkproviders/JavaHomeJdkProvider.java index 85c0d1d..9ba413f 100644 --- a/src/main/java/dev/jbang/devkitman/jdkproviders/JavaHomeJdkProvider.java +++ b/src/main/java/dev/jbang/devkitman/jdkproviders/JavaHomeJdkProvider.java @@ -2,8 +2,7 @@ import java.nio.file.Files; import java.nio.file.Path; -import java.util.Collections; -import java.util.List; +import java.util.stream.Stream; import org.jspecify.annotations.NonNull; @@ -31,15 +30,20 @@ public String name() { @NonNull @Override - public List listInstalled() { + public Stream listInstalled() { Path jdkHome = JavaUtils.getJavaHomeEnv(); if (jdkHome != null && Files.isDirectory(jdkHome)) { - Jdk.InstalledJdk jdk = createJdk(Discovery.PROVIDER_ID, jdkHome, null, false, null); + Jdk.InstalledJdk jdk = createJdk(Discovery.PROVIDER_ID, jdkHome, null, null); if (jdk != null) { - return Collections.singletonList(jdk); + return Stream.of(jdk); } } - return Collections.emptyList(); + return Stream.empty(); + } + + @Override + public boolean hasFixedVersions() { + return false; } public static class Discovery implements JdkDiscovery { diff --git a/src/main/java/dev/jbang/devkitman/jdkproviders/LinkedJdkProvider.java b/src/main/java/dev/jbang/devkitman/jdkproviders/LinkedJdkProvider.java index 4e9fe9d..e6410a2 100644 --- a/src/main/java/dev/jbang/devkitman/jdkproviders/LinkedJdkProvider.java +++ b/src/main/java/dev/jbang/devkitman/jdkproviders/LinkedJdkProvider.java @@ -2,11 +2,10 @@ import static dev.jbang.devkitman.Jdk.InstalledJdk.Default.determineTagsFromJdkHome; -import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; @@ -32,24 +31,16 @@ public LinkedJdkProvider(Path jdksRoot) { super(jdksRoot); } - protected Jdk.@Nullable InstalledJdk createJdk(Path home) { - return createJdk(home, false); - } - @Override public @NonNull String description() { - return "Any unmanaged JDKs that the user has linked to."; - } - - @Override - public boolean canUse() { - return true; + return "Any unmanaged JDKs that have been linked to."; } @Override public Jdk.@Nullable AvailableJdk getAvailableByIdOrToken(String idOrToken) { + // Check if the token follows our special format String[] parts = idOrToken.split("@", 2); - if (parts.length == 2 && isValidId(parts[0]) && isValidPath(parts[1])) { + if (parts.length == 2 && isValidId(parts[0]) && FileUtils.isValidPath(parts[1])) { Path jdkPath = Paths.get(parts[1]); if (JavaUtils.hasJavacCmd(jdkPath)) { Optional version = JavaUtils.resolveJavaVersionStringFromPath(jdkPath); @@ -57,7 +48,8 @@ public boolean canUse() { throw new IllegalArgumentException( "Unable to determine Java version in given path: " + jdkPath); } - return new Jdk.AvailableJdk.Default(this, idOrToken, version.get(), determineTagsFromJdkHome(jdkPath)); + return new AvailableLinkedJdk(this, parts[0], version.get(), jdkPath, + determineTagsFromJdkHome(jdkPath)); } return null; } else { @@ -65,74 +57,62 @@ public boolean canUse() { } } - private static boolean isValidPath(String path) { - try { - Paths.get(path); - return true; - } catch (InvalidPathException e) { - return false; - } - } - @Override protected boolean acceptFolder(@NonNull Path jdkFolder) { - return isValidId(jdkFolder.getFileName().toString()) - && super.acceptFolder(jdkFolder) - && FileUtils.isLink(jdkFolder); + return super.acceptFolder(jdkFolder) && FileUtils.isLink(jdkFolder); } @Override public Jdk.@NonNull InstalledJdk install(Jdk.@NonNull AvailableJdk jdk) { - // Check this Jdk's id follows our special format - String[] parts = jdk.id().split("@", 2); - if (parts.length != 2 || !isValidPath(parts[1])) { - throw new IllegalStateException("Invalid linked Jdk id: " + jdk.id()); + if (!(jdk instanceof AvailableLinkedJdk)) { + throw new IllegalArgumentException( + "LinkedJdkInstaller can only install JDKs listed as available by itself"); } - String id = parts[0]; - Path jdkPath = Paths.get(parts[1]); + AvailableLinkedJdk availJdk = (AvailableLinkedJdk) jdk; // If there's an existing installed Jdk with the same id, uninstall it - Jdk.InstalledJdk existingJdk = getInstalledById(jdkId(id)); - if (existingJdk != null && existingJdk.isInstalled() && !jdk.equals(existingJdk)) { + Jdk.InstalledJdk existingJdk = getInstalledById(availJdk.id()); + if (existingJdk != null && !FileUtils.isSameFile(availJdk.home, existingJdk.home())) { LOGGER.log( Level.FINE, "A managed JDK already exists, it must be deleted to make sure linking works"); uninstall(existingJdk); } - Path linkPath = getJdkPath(id); + Path linkPath = getJdkPath(availJdk.id()); // Remove anything that might be in the way FileUtils.deletePath(linkPath); // Now create the new link - FileUtils.createLink(linkPath, jdkPath); - Jdk.InstalledJdk newJdk = Objects.requireNonNull(createJdk(linkPath)); - LOGGER.log(Level.INFO, "JDK {0} has been linked to: {1}", new Object[] { id, jdkPath }); + FileUtils.createLink(linkPath, availJdk.home); + Jdk.InstalledJdk newJdk = createJdk(linkPath); + if (newJdk == null) { + throw new IllegalStateException("Failed to find JDK in: " + availJdk.home); + } + LOGGER.log(Level.INFO, "JDK {0} has been linked to: {1}", new Object[] { availJdk.id(), availJdk.home }); return newJdk; } @Override public void uninstall(Jdk.@NonNull InstalledJdk jdk) { - if (jdk.isInstalled()) { - FileUtils.deletePath(jdk.home()); - LOGGER.log(Level.INFO, "JDK {0} has been uninstalled", new Object[] { jdk.id() }); - } + JavaUtils.safeDeleteJdk(jdk.home()); } - // TODO remove these 3 methods when switching to the new folder structure - @NonNull @Override - public String jdkId(String name) { - int majorVersion = JavaUtils.parseJavaVersion(name); - return Integer.toString(majorVersion); + public boolean canUpdate() { + return true; } @Override - public boolean isValidId(@NonNull String id) { - return JavaUtils.parseToInt(id, 0) > 0; + public boolean hasFixedVersions() { + return false; } - @NonNull - @Override - protected Path getJdkPath(@NonNull String jdk) { - return jdksRoot.resolve(Integer.toString(JavaUtils.parseToInt(jdk, 0))); + static class AvailableLinkedJdk extends Jdk.AvailableJdk.Default { + public final Path home; + + AvailableLinkedJdk(@NonNull JdkProvider provider, @NonNull String id, @NonNull String version, + @NonNull Path home, @NonNull Set tags) { + super(provider, id, version, tags); + this.home = home; + } } public static class Discovery implements JdkDiscovery { diff --git a/src/main/java/dev/jbang/devkitman/jdkproviders/MultiHomeJdkProvider.java b/src/main/java/dev/jbang/devkitman/jdkproviders/MultiHomeJdkProvider.java index 580cdba..6f75b68 100644 --- a/src/main/java/dev/jbang/devkitman/jdkproviders/MultiHomeJdkProvider.java +++ b/src/main/java/dev/jbang/devkitman/jdkproviders/MultiHomeJdkProvider.java @@ -2,9 +2,8 @@ import java.nio.file.Files; import java.nio.file.Paths; -import java.util.List; import java.util.Objects; -import java.util.stream.Collectors; +import java.util.stream.Stream; import org.jspecify.annotations.NonNull; @@ -33,16 +32,20 @@ public String name() { @NonNull @Override - public List listInstalled() { + public Stream listInstalled() { return System.getenv() .entrySet() .stream() .filter(entry -> entry.getKey().startsWith("JAVA_HOME_")) .map(entry -> Paths.get(entry.getValue())) .filter(Files::isDirectory) - .map(jdkHome -> createJdk(Discovery.PROVIDER_ID, jdkHome, null, false, null)) - .filter(Objects::nonNull) - .collect(Collectors.toList()); + .map(jdkHome -> createJdk(Discovery.PROVIDER_ID, jdkHome, null, null)) + .filter(Objects::nonNull); + } + + @Override + public boolean hasFixedVersions() { + return false; } public static class Discovery implements JdkDiscovery { diff --git a/src/main/java/dev/jbang/devkitman/jdkproviders/PathJdkProvider.java b/src/main/java/dev/jbang/devkitman/jdkproviders/PathJdkProvider.java index b96bf87..97c60dc 100644 --- a/src/main/java/dev/jbang/devkitman/jdkproviders/PathJdkProvider.java +++ b/src/main/java/dev/jbang/devkitman/jdkproviders/PathJdkProvider.java @@ -1,8 +1,7 @@ package dev.jbang.devkitman.jdkproviders; import java.nio.file.Path; -import java.util.Collections; -import java.util.List; +import java.util.stream.Stream; import org.jspecify.annotations.NonNull; @@ -30,7 +29,7 @@ public String name() { @NonNull @Override - public List listInstalled() { + public Stream listInstalled() { Path jdkHome = null; Path javac = OsUtils.searchPath("javac"); if (javac != null) { @@ -38,12 +37,17 @@ public List listInstalled() { jdkHome = javac.getParent().getParent(); } if (jdkHome != null) { - Jdk.InstalledJdk jdk = createJdk(Discovery.PROVIDER_ID, jdkHome, null, false, null); + Jdk.InstalledJdk jdk = createJdk(Discovery.PROVIDER_ID, jdkHome, null, null); if (jdk != null) { - return Collections.singletonList(jdk); + return Stream.of(jdk); } } - return Collections.emptyList(); + return Stream.empty(); + } + + @Override + public boolean hasFixedVersions() { + return false; } public static class Discovery implements JdkDiscovery { diff --git a/src/main/java/dev/jbang/devkitman/util/FileUtils.java b/src/main/java/dev/jbang/devkitman/util/FileUtils.java index 911c439..db12665 100644 --- a/src/main/java/dev/jbang/devkitman/util/FileUtils.java +++ b/src/main/java/dev/jbang/devkitman/util/FileUtils.java @@ -122,6 +122,14 @@ public static Path deleteOnExit(Path path) { return path; } + public static boolean isSameFile(Path f1, Path f2) { + try { + return Files.isSameFile(f1, f2); + } catch (IOException e) { + return f1.toAbsolutePath().equals(f2.toAbsolutePath()); + } + } + // Returns true if a path is a (sym)link to an entry in the same folder public static boolean isSameFolderLink(Path jdkFolder) { Path absFolder = jdkFolder.toAbsolutePath(); @@ -135,4 +143,14 @@ public static boolean isSameFolderLink(Path jdkFolder) { } return false; } + + public static boolean isValidPath(String path) { + try { + Paths.get(path); + return true; + } catch (InvalidPathException e) { + return false; + } + } + } diff --git a/src/main/java/dev/jbang/devkitman/util/JavaUtils.java b/src/main/java/dev/jbang/devkitman/util/JavaUtils.java index d39a9c2..0104c9e 100644 --- a/src/main/java/dev/jbang/devkitman/util/JavaUtils.java +++ b/src/main/java/dev/jbang/devkitman/util/JavaUtils.java @@ -156,4 +156,23 @@ public static Path jre2jdk(@NonNull Path jdkHome) { } return jdkHome; } + + static public 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 + try { + Path jdkTmpDir = jdkHome + .getParent() + .resolve("_delete_me_" + jdkHome.getFileName().toString()); + Files.move(jdkHome, jdkTmpDir); + FileUtils.deletePath(jdkTmpDir); + } catch (IOException ex) { + LOGGER.log(Level.WARNING, "Cannot uninstall JDK, it's being used: {0}", jdkHome); + return; + } + } else { + FileUtils.deletePath(jdkHome); + } + } } diff --git a/src/main/java/dev/jbang/devkitman/util/VersionComparator.java b/src/main/java/dev/jbang/devkitman/util/VersionComparator.java new file mode 100644 index 0000000..fce018a --- /dev/null +++ b/src/main/java/dev/jbang/devkitman/util/VersionComparator.java @@ -0,0 +1,29 @@ +package dev.jbang.devkitman.util; + +import java.util.Comparator; + +public class VersionComparator implements Comparator { + public static final Comparator INSTANCE = new VersionComparator(); + + @Override + public int compare(String v1, String v2) { + String[] parts1 = v1.split("[.+-]"); + String[] parts2 = v2.split("[.+-]"); + + int length = Math.max(parts1.length, parts2.length); + for (int i = 0; i < length; i++) { + int p1 = i < parts1.length ? JavaUtils.parseToInt(parts1[i], -1) : -2; + int p2 = i < parts2.length ? JavaUtils.parseToInt(parts2[i], -1) : -2; + if (p1 == -1 && p2 == -1) { + // Both are non-integers, compare as strings + int cmp = parts1[i].compareTo(parts2[i]); + if (cmp != 0) { + return cmp; + } + } else if (p1 != p2) { + return Integer.compare(p1, p2); + } + } + return parts1.length == parts2.length ? 0 : Integer.compare(parts1.length, parts2.length); + } +} diff --git a/src/test/java/dev/jbang/devkitman/BaseTest.java b/src/test/java/dev/jbang/devkitman/BaseTest.java index 8f168a0..3db50dd 100644 --- a/src/test/java/dev/jbang/devkitman/BaseTest.java +++ b/src/test/java/dev/jbang/devkitman/BaseTest.java @@ -4,6 +4,7 @@ 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; @@ -19,6 +20,7 @@ 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; @@ -88,10 +90,17 @@ protected JdkManager jdkManager(String... providerNames) { } 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::createMockJdk, versions); } - protected JdkManager mockJdkManager(Function mockJdk, int... versions) { + protected JdkManager mockJdkManager(Function mockJdk, String... versions) { return JdkManager.builder() .providers(new MockJdkProvider(config.installPath(), mockJdk, versions)) .build(); @@ -101,17 +110,36 @@ 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 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(String.valueOf(jdkVersion)); - init.accept(jdkPath, jdkVersion + ".0.7"); + 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; } @@ -195,7 +223,9 @@ public T resultFromUrl( }; JBangJdkProvider jbang = new JBangJdkProvider(config.installPath()); - FoojayJdkInstaller installer = new FoojayJdkInstaller(jbang, jbang::jdkId); + FoojayJdkInstaller installer = new FoojayJdkInstaller(jbang) + .distro("jbang") + .remoteAccessProvider(rap); installer.remoteAccessProvider(rap); jbang.installer(installer); return jbang; diff --git a/src/test/java/dev/jbang/devkitman/TestJdkInstaller.java b/src/test/java/dev/jbang/devkitman/TestJdkInstaller.java index cb6b05e..e06e60a 100644 --- a/src/test/java/dev/jbang/devkitman/TestJdkInstaller.java +++ b/src/test/java/dev/jbang/devkitman/TestJdkInstaller.java @@ -17,7 +17,7 @@ public class TestJdkInstaller extends BaseTest { void testListAvailable() throws IOException { JdkManager jm = jdkManager("jbang"); List jdks = jm.listAvailableJdks(); - assertThat(jdks, hasSize(18)); + assertThat(jdks, hasSize(21)); assertThat(jdks.get(0).majorVersion(), is(25)); assertThat(jdks.get(jdks.size() - 1).majorVersion(), is(8)); } @@ -27,7 +27,8 @@ void testInstallExact() throws IOException { JdkManager jm = jdkManager("jbang"); Jdk.InstalledJdk jdk = jm.getOrInstallJdk("12"); assertThat(jdk.provider(), instanceOf(JBangJdkProvider.class)); - assertThat(jdk.home().toString(), endsWith(File.separator + "12")); + assertThat(jdk.home().toString(), endsWith(File.separator + "12.0.2-aoj-jbang")); + assertThat(jdk.home().resolve("release").toFile().exists(), is(true)); } @Test @@ -35,6 +36,6 @@ void testInstallOpen() throws IOException { JdkManager jm = jdkManager("jbang"); Jdk.InstalledJdk jdk = jm.getOrInstallJdk("12+"); assertThat(jdk.provider(), instanceOf(JBangJdkProvider.class)); - assertThat(jdk.home().toString(), endsWith(File.separator + "21")); + assertThat(jdk.home().toString(), endsWith(File.separator + "21.0.6+7-temurin-jbang")); } } diff --git a/src/test/java/dev/jbang/devkitman/TestJdkManager.java b/src/test/java/dev/jbang/devkitman/TestJdkManager.java index 6ddb7f7..c1ab75d 100644 --- a/src/test/java/dev/jbang/devkitman/TestJdkManager.java +++ b/src/test/java/dev/jbang/devkitman/TestJdkManager.java @@ -9,6 +9,7 @@ import java.nio.file.Paths; import java.util.Arrays; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -20,6 +21,7 @@ import dev.jbang.devkitman.jdkproviders.LinkedJdkProvider; import dev.jbang.devkitman.jdkproviders.MultiHomeJdkProvider; import dev.jbang.devkitman.jdkproviders.PathJdkProvider; +import dev.jbang.devkitman.util.FileUtils; public class TestJdkManager extends BaseTest { @Test @@ -31,13 +33,18 @@ void testNoJdksInstalled() { void testHasJdksInstalled() { Arrays.asList(11, 12, 13).forEach(this::createMockJdk); List jdks = jdkManager().listInstalledJdks(); - assertThat(jdks, hasSize(4)); + assertThat(jdks, hasSize(7)); assertThat( jdks.stream().map(Jdk::majorVersion).collect(Collectors.toList()), - containsInAnyOrder(11, 11, 12, 13)); + containsInAnyOrder(11, 11, 11, 12, 12, 13, 13)); assertThat( jdks.stream().map(Jdk::version).collect(Collectors.toList()), - containsInAnyOrder("11.0.7", "11.0.7", "12.0.7", "13.0.7")); + containsInAnyOrder("11.0.7", "11.0.7", "11.0.7", "12.0.7", "12.0.7", "13.0.7", "13.0.7")); + assertThat( + jdks.stream().map(Jdk::id).collect(Collectors.toList()), + containsInAnyOrder("default", "11-default", "11.0.7-distro-jbang", "12-default", "12.0.7-distro-jbang", + "13-default", + "13.0.7-distro-jbang")); } @Test @@ -48,16 +55,16 @@ void testHasJdksInstalledWithJavaHome() { environmentVariables.set("JAVA_HOME", jdkPath.toString()); List jdks = jdkManager("default", "javahome", "jbang").listInstalledJdks(); - assertThat(jdks, hasSize(4)); + assertThat(jdks, hasSize(6)); assertThat( jdks.stream().map(Jdk::majorVersion).collect(Collectors.toList()), - containsInAnyOrder(11, 11, 12, 13)); + containsInAnyOrder(11, 11, 11, 12, 12, 13)); assertThat( jdks.stream().map(Jdk::version).collect(Collectors.toList()), - containsInAnyOrder("11.0.7", "11.0.7", "12.0.7", "13.0.7")); + containsInAnyOrder("11.0.7", "11.0.7", "11.0.7", "12.0.7", "12.0.7", "13.0.7")); assertThat( - jdks.stream().map(Jdk.InstalledJdk::isFixedVersion).collect(Collectors.toList()), - containsInAnyOrder(false, true, true, false)); + jdks.stream().map(jdk -> jdk.provider().hasFixedVersions()).collect(Collectors.toList()), + containsInAnyOrder(false, false, true, false, true, false)); } @Test @@ -74,7 +81,7 @@ void testDefault() { JdkManager jm = jdkManager(); assertThat(jm.getDefaultJdk(), not(nullValue())); assertThat(jm.getDefaultJdk().majorVersion(), is(11)); - jm.setDefaultJdk(jm.getInstalledJdk("12")); + jm.setDefaultJdk(Objects.requireNonNull(jm.getInstalledJdk("12"))); assertThat(jm.getDefaultJdk().majorVersion(), is(12)); } @@ -84,8 +91,21 @@ void testDefaultPlus() { JdkManager jm = jdkManager(); assertThat(jm.getDefaultJdk(), not(nullValue())); assertThat(jm.getDefaultJdk().majorVersion(), is(11)); - jm.setDefaultJdk(jm.getInstalledJdk("16+")); + jm.setDefaultJdk(Objects.requireNonNull(jm.getInstalledJdk("16+"))); + assertThat(jm.getDefaultJdk().majorVersion(), is(17)); + } + + @Test + void testDefaultCustomLinkPath() { + Arrays.asList(11, 14, 17).forEach(this::createMockJdk); + Path tempPath = Paths.get(System.getProperty("user.home")).getParent(); + String link = tempPath.resolve("deflink").toAbsolutePath().toString(); + JdkManager jm = jdkManager("default;link=" + link, "linked", "jbang"); + // The following is null because the mocking doesn't create the correct link! + assertThat(jm.getDefaultJdk(), nullValue()); + jm.setDefaultJdk(Objects.requireNonNull(jm.getInstalledJdk("16+"))); assertThat(jm.getDefaultJdk().majorVersion(), is(17)); + assertThat(jm.getDefaultJdk().id(), is("default")); } @Test @@ -108,7 +128,7 @@ void testGetJdkNull() { JdkManager jm = jdkManager(); assertThat(jm.getInstalledJdk(null), not(nullValue())); assertThat(jm.getInstalledJdk(null).majorVersion(), is(11)); - jm.setDefaultJdk(jm.getInstalledJdk("12")); + jm.setDefaultJdk(Objects.requireNonNull(jm.getInstalledJdk("12"))); assertThat(jm.getInstalledJdk(null).majorVersion(), is(12)); } @@ -164,7 +184,7 @@ void testDefaultUninstallNext() { JdkManager jm = jdkManager(); assertThat(jm.getDefaultJdk(), not(nullValue())); assertThat(jm.getDefaultJdk().majorVersion(), is(14)); - jm.getInstalledJdk("14", JdkProvider.Predicates.canUpdate).uninstall(); + jm.getInstalledJdk("14", JdkProvider.Predicates.canInstall).uninstall(); assertThat(jm.getDefaultJdk(), not(nullValue())); assertThat(jm.getDefaultJdk().majorVersion(), is(17)); } @@ -175,7 +195,7 @@ void testDefaultUninstallPrev() { JdkManager jm = jdkManager(); assertThat(jm.getDefaultJdk(), not(nullValue())); assertThat(jm.getDefaultJdk().majorVersion(), is(17)); - jm.getInstalledJdk("17", JdkProvider.Predicates.canUpdate).uninstall(); + jm.getInstalledJdk("17", JdkProvider.Predicates.canInstall).uninstall(); assertThat(jm.getDefaultJdk(), not(nullValue())); assertThat(jm.getDefaultJdk().majorVersion(), is(14)); } @@ -215,7 +235,7 @@ void testDefaultWithJavaHome() throws IOException { environmentVariables.set("JAVA_HOME", jdkPath.toString()); JdkManager jm = jdkManager("default", "javahome", "jbang"); - jm.setDefaultJdk(jm.getInstalledJdk("12")); + jm.setDefaultJdk(Objects.requireNonNull(jm.getInstalledJdk("javahome"))); Jdk.InstalledJdk jdk = jm.getInstalledJdk("12"); assertThat(jdk.provider(), instanceOf(DefaultJdkProvider.class)); assertThat(jdk.home().toString(), endsWith(File.separator + "default")); @@ -237,12 +257,25 @@ void testPath() throws IOException { } @Test - void testLinkToExistingJdkPath() { + void testLinkToJdk() { Path jdkPath = createMockJdkExt(12); - jdkManager().linkToExistingJdk(jdkPath, "12"); + jdkManager().linkToExistingJdk(jdkPath, "mylink"); + List jdks = jdkManager().listInstalledJdks(); + assertThat(jdks, hasSize(1)); + assertThat(jdks.get(0).provider(), instanceOf(LinkedJdkProvider.class)); + assertThat(jdks.get(0).majorVersion(), is(12)); + } + + @Test + void testLinkToExistingLink() { + Path jdk12Path = createMockJdkExt(12); + Path jdk14Path = createMockJdkExt(14); + jdkManager().linkToExistingJdk(jdk12Path, "mylink"); + jdkManager().linkToExistingJdk(jdk14Path, "mylink"); List jdks = jdkManager().listInstalledJdks(); assertThat(jdks, hasSize(1)); assertThat(jdks.get(0).provider(), instanceOf(LinkedJdkProvider.class)); + assertThat(jdks.get(0).majorVersion(), is(14)); } @Test @@ -260,29 +293,16 @@ void testLinkToInvalidJdkPath() { } } - @Test - void testLinkToInvalidVersion() { - try { - Path jdkPath = createMockJdkExt(12); - jdkManager().linkToExistingJdk(jdkPath, "11"); - assertThat("Should have thrown an exception", false); - } catch (IllegalArgumentException ex) { - assertThat( - ex.getMessage(), - startsWith("Linked JDK is not of the correct version: 12 instead of: 11")); - } - } - @Test void testLinkToInvalidId() { try { Path jdkPath = createMockJdkExt(12); - jdkManager().linkToExistingJdk(jdkPath, "11foo"); + jdkManager().linkToExistingJdk(jdkPath, "11%foo"); assertThat("Should have thrown an exception", false); } catch (IllegalArgumentException ex) { assertThat( ex.getMessage(), - startsWith("Invalid JDK id: 11foo, must be a valid major version number")); + startsWith("Unable to create link to JDK in path:")); } } @@ -299,14 +319,14 @@ void testProviderOrder() { } @Test - void testInstallVersion() { + void testGetOrInstallVersion() { Path home = mockJdkManager(11, 14, 17).getOrInstallJdk("17").home(); - assertThat(home.toString(), endsWith(File.separator + "17")); + assertThat(home.toString(), endsWith(File.separator + "17.0.7-distro-jbang")); assertThat(home.resolve("release").toFile().exists(), is(true)); } @Test - void testInstallVersionFail() { + void testGetOrInstallVersionFail() { try { Path home = mockJdkManager(11, 14, 17).getOrInstallJdk("15").home(); } catch (Exception e) { @@ -317,14 +337,14 @@ void testInstallVersionFail() { } @Test - void testInstallVersionPlus() { + void testGetOrInstallVersionPlus() { Path home = mockJdkManager(11, 14, 17).getOrInstallJdk("15+").home(); - assertThat(home.toString(), endsWith(File.separator + "17")); + assertThat(home.toString(), endsWith(File.separator + "17.0.7-distro-jbang")); assertThat(home.resolve("release").toFile().exists(), is(true)); } @Test - void testInstallDefaultVersion() { + void testGetOrInstallDefaultVersion() { Jdk jdk = mockJdkManager(8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24) .getOrInstallJdk( null); @@ -356,4 +376,68 @@ void testMultiHome() { assertThat(jdks, hasSize(2)); assertThat(jdks.stream().map(Jdk::majorVersion).collect(Collectors.toList()), containsInAnyOrder(12, 17)); } + + @Test + void testBackwardCompatList() { + Arrays.asList("8", "11", "17").forEach(v -> createMockJdk(v, v + ".0.7")); + List jdks = jdkManager().listInstalledJdks(); + assertThat(jdks, hasSize(4)); + assertThat(jdks.stream().map(Jdk::majorVersion).collect(Collectors.toList()), containsInAnyOrder(8, 8, 11, 17)); + assertThat(jdks.stream().map(Jdk::id).collect(Collectors.toList()), + containsInAnyOrder("default", "8-jbang", "11-jbang", "17-jbang")); + } + + @Test + void testInstallVersion() { + Jdk.InstalledJdk jdk = jdkManager().getOrInstallJdk("21"); + assertThat(jdk, notNullValue()); + List jdks = jdkManager().listInstalledJdks(); + assertThat(jdks, hasSize(3)); + // Mocked installations are always version 12! + assertThat(jdks.stream().map(Jdk::majorVersion).collect(Collectors.toList()), containsInAnyOrder(12, 12, 12)); + assertThat(jdks.stream().map(Jdk::id).collect(Collectors.toList()), + containsInAnyOrder("default", "12-default", "21.0.6+7-temurin-jbang")); + } + + @Test + void testUninstallNumberLink() { + Path home = createMockJdk("11", "11.0.7"); + FileUtils.createLink(home.getParent().resolve("12"), home); + List jdks = jdkManager().listInstalledJdks(); + assertThat(jdks, hasSize(3)); + assertThat(jdks.stream().map(Jdk::majorVersion).collect(Collectors.toList()), containsInAnyOrder(11, 11, 11)); + assertThat(jdks.stream().map(Jdk::id).collect(Collectors.toList()), + containsInAnyOrder("default", "12-default", "11-jbang")); + jdkManager().getOrInstallJdk("12", JdkProvider.Predicates.canUpdate).uninstall(); + jdks = jdkManager().listInstalledJdks(); + assertThat(jdks, hasSize(2)); + assertThat(jdks.stream().map(Jdk::majorVersion).collect(Collectors.toList()), containsInAnyOrder(11, 11)); + assertThat(jdks.stream().map(Jdk::id).collect(Collectors.toList()), + containsInAnyOrder("default", "11-jbang")); + } + + @Test + void testUninstallAll() { + Arrays.asList(11, 12, 13).forEach(this::createMockJdk); + JdkManager jm = jdkManager(); + List jdks = jm.listInstalledJdks(); + assertThat(jdks, hasSize(7)); + assertThat(jdks.stream().map(Jdk::id).collect(Collectors.toList()), + containsInAnyOrder("default", "11-default", "12-default", "13-default", "11.0.7-distro-jbang", + "12.0.7-distro-jbang", "13.0.7-distro-jbang")); + jm.getOrInstallJdk("11", JdkProvider.Predicates.canInstall).uninstall(); + jdks = jm.listInstalledJdks(); + assertThat(jdks, hasSize(5)); + assertThat(jdks.stream().map(Jdk::id).collect(Collectors.toList()), + containsInAnyOrder("default", "12-default", "13-default", "12.0.7-distro-jbang", + "13.0.7-distro-jbang")); + jm.getOrInstallJdk("12", JdkProvider.Predicates.canInstall).uninstall(); + jdks = jm.listInstalledJdks(); + assertThat(jdks, hasSize(3)); + assertThat(jdks.stream().map(Jdk::id).collect(Collectors.toList()), + containsInAnyOrder("default", "13-default", "13.0.7-distro-jbang")); + jm.getOrInstallJdk("13", JdkProvider.Predicates.canInstall).uninstall(); + jdks = jm.listInstalledJdks(); + assertThat(jdks, empty()); + } } diff --git a/src/test/java/dev/jbang/devkitman/jdkproviders/JdkProviderWrapper.java b/src/test/java/dev/jbang/devkitman/jdkproviders/JdkProviderWrapper.java index bae227a..d1fc804 100644 --- a/src/test/java/dev/jbang/devkitman/jdkproviders/JdkProviderWrapper.java +++ b/src/test/java/dev/jbang/devkitman/jdkproviders/JdkProviderWrapper.java @@ -1,7 +1,7 @@ package dev.jbang.devkitman.jdkproviders; import java.nio.file.Path; -import java.util.List; +import java.util.stream.Stream; import org.jspecify.annotations.NonNull; @@ -37,7 +37,7 @@ public void manager(@NonNull JdkManager manager) { } @Override - public @NonNull List listInstalled() { + public @NonNull Stream listInstalled() { return provider.listInstalled(); } @@ -52,7 +52,7 @@ public Jdk.InstalledJdk getInstalledByPath(@NonNull Path jdkPath) { } @Override - public @NonNull List listAvailable() { + public @NonNull Stream listAvailable() { return provider.listAvailable(); } diff --git a/src/test/java/dev/jbang/devkitman/jdkproviders/MockJdkProvider.java b/src/test/java/dev/jbang/devkitman/jdkproviders/MockJdkProvider.java index 4b7b2f2..3d302f9 100644 --- a/src/test/java/dev/jbang/devkitman/jdkproviders/MockJdkProvider.java +++ b/src/test/java/dev/jbang/devkitman/jdkproviders/MockJdkProvider.java @@ -2,49 +2,45 @@ import java.nio.file.Path; import java.util.Arrays; -import java.util.List; import java.util.Objects; import java.util.function.Function; -import java.util.stream.Collectors; +import java.util.stream.Stream; import org.jspecify.annotations.NonNull; import dev.jbang.devkitman.Jdk; -import dev.jbang.devkitman.util.FileUtils; +import dev.jbang.devkitman.util.JavaUtils; public class MockJdkProvider extends BaseFoldersJdkProvider { - protected final Function mockJdk; - protected final int[] versions; + protected final Function mockJdk; + protected final String[] versions; @Override public @NonNull String description() { return "Dummy JDK provider"; } - public MockJdkProvider(Path root, Function mockJdk, int... versions) { + public MockJdkProvider(Path root, Function mockJdk, String... versions) { super(root); this.mockJdk = mockJdk; this.versions = versions; } @Override - public @NonNull List listAvailable() { + public @NonNull Stream listAvailable() { return Arrays.stream(versions) - .mapToObj(v -> new Jdk.AvailableJdk.Default(this, v + "-dummy", v + ".0.7", null)) - .collect(Collectors.toList()); + .map(v -> new Jdk.AvailableJdk.Default(this, v + "-dummy", v, null)); } @Override public Jdk.@NonNull InstalledJdk install(Jdk.@NonNull AvailableJdk jdk) { - Path jdkPath = mockJdk.apply(jdk.majorVersion()); - return Objects.requireNonNull(createJdk(jdk.id(), jdkPath, jdk.version(), true, null)); + Path jdkPath = mockJdk.apply(jdk.version()); + return Objects.requireNonNull(createJdk(jdk.id(), jdkPath, jdk.version(), null)); } @Override public void uninstall(Jdk.@NonNull InstalledJdk jdk) { - if (jdk.isInstalled()) { - FileUtils.deletePath(jdk.home()); - } + JavaUtils.safeDeleteJdk(jdk.home()); } @Override diff --git a/src/test/resources/jdk-12.zip b/src/test/resources/jdk-12.zip index bec629b..3e983a8 100644 Binary files a/src/test/resources/jdk-12.zip and b/src/test/resources/jdk-12.zip differ diff --git a/src/test/resources/testInstall.json b/src/test/resources/testInstall.json index 3f1f670..ac02936 100644 --- a/src/test/resources/testInstall.json +++ b/src/test/resources/testInstall.json @@ -625,8 +625,8 @@ "archive_type": "zip", "distribution": "temurin", "major_version": 16, - "java_version": "16.0.2+7", - "distribution_version": "16.0.2", + "java_version": "16.0.1", + "distribution_version": "16.0.1", "jdk_version": 16, "latest_build_available": true, "release_status": "ga", @@ -638,7 +638,7 @@ "package_type": "jdk", "javafx_bundled": false, "directly_downloadable": true, - "filename": "OpenJDK16U-jdk_x64_windows_hotspot_16.0.2_7.zip", + "filename": "OpenJDK16U-jdk_x64_windows_hotspot_16.0.1.zip", "links": { "pkg_info_uri": "https://api.foojay.io/disco/v3.0/ids/a443e9c49d95dd39912b30117cbe0b8b", "pkg_download_redirect": "https://api.foojay.io/disco/v3.0/ids/a443e9c49d95dd39912b30117cbe0b8b/redirect" @@ -749,8 +749,8 @@ "archive_type": "zip", "distribution": "temurin", "major_version": 11, - "java_version": "11.0.26+4", - "distribution_version": "11.0.26", + "java_version": "11.0.11", + "distribution_version": "11.0.11", "jdk_version": 11, "latest_build_available": true, "release_status": "ga", @@ -762,7 +762,7 @@ "package_type": "jdk", "javafx_bundled": false, "directly_downloadable": true, - "filename": "OpenJDK11U-jdk_x64_windows_hotspot_11.0.26_4.zip", + "filename": "OpenJDK11U-jdk_x64_windows_hotspot_11.0.11.zip", "links": { "pkg_info_uri": "https://api.foojay.io/disco/v3.0/ids/8b46ef9f1f6122387f6c0a55f0318712", "pkg_download_redirect": "https://api.foojay.io/disco/v3.0/ids/8b46ef9f1f6122387f6c0a55f0318712/redirect"