-
Notifications
You must be signed in to change notification settings - Fork 161
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Loading status checks…
Separated JDK management code into its own module
Fixes #1857
Showing
43 changed files
with
2,629 additions
and
1,624 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
.classpath | ||
.project | ||
.vscode | ||
.settings | ||
target | ||
.idea | ||
*.iml | ||
/build | ||
.gradle | ||
.factorypath | ||
bin | ||
homebrew-tap | ||
RESULTS | ||
*.db | ||
jbang-action | ||
out | ||
node_modules | ||
package-lock.json | ||
*.jfr | ||
itests/hello.java | ||
*.class | ||
CHANGELOG.md |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
plugins { | ||
id 'java' | ||
} | ||
|
||
group = 'dev.jbang.jvm' | ||
version = '0.1.0' | ||
|
||
sourceCompatibility = '8' | ||
targetCompatibility = '8' | ||
|
||
repositories { | ||
mavenCentral() | ||
} | ||
|
||
dependencies { | ||
implementation 'org.apache.commons:commons-compress:1.26.2' | ||
implementation 'org.apache.httpcomponents:httpclient:4.5.14' | ||
implementation 'org.apache.httpcomponents:httpclient-cache:4.5.14' | ||
implementation 'com.google.code.gson:gson:2.11.0' | ||
|
||
implementation "org.slf4j:slf4j-nop:1.7.30" | ||
implementation "org.slf4j:jcl-over-slf4j:1.7.30" | ||
implementation "org.jspecify:jspecify:1.0.0" | ||
|
||
testImplementation platform('org.junit:junit-bom:5.10.1') | ||
testImplementation 'org.junit.jupiter:junit-jupiter' | ||
} | ||
|
||
test { | ||
useJUnitPlatform() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
package dev.jbang.jvm; | ||
|
||
import java.nio.file.Path; | ||
import java.util.HashSet; | ||
import java.util.Objects; | ||
import java.util.Set; | ||
|
||
import dev.jbang.jvm.util.JavaUtils; | ||
import org.jspecify.annotations.NonNull; | ||
import org.jspecify.annotations.Nullable; | ||
|
||
public interface Jdk extends Comparable<Jdk> { | ||
@NonNull JdkProvider getProvider(); | ||
|
||
@NonNull String getId(); | ||
|
||
@NonNull String getVersion(); | ||
|
||
@Nullable Path getHome(); | ||
|
||
int getMajorVersion(); | ||
|
||
@NonNull Jdk install(); | ||
|
||
void uninstall(); | ||
|
||
boolean isInstalled(); | ||
|
||
class Default implements Jdk { | ||
@NonNull private final transient JdkProvider provider; | ||
@NonNull private final String id; | ||
@NonNull private final String version; | ||
@Nullable private final Path home; | ||
@NonNull private final Set<String> tags = new HashSet<>(); | ||
|
||
Default( | ||
@NonNull JdkProvider provider, | ||
@NonNull String id, | ||
@Nullable Path home, | ||
@NonNull String version, | ||
@NonNull String... tags) { | ||
this.provider = provider; | ||
this.id = id; | ||
this.version = version; | ||
this.home = home; | ||
} | ||
|
||
@Override | ||
@NonNull | ||
public JdkProvider getProvider() { | ||
return provider; | ||
} | ||
|
||
/** Returns the id that is used to uniquely identify this JDK across all providers */ | ||
@Override | ||
@NonNull | ||
public String getId() { | ||
return id; | ||
} | ||
|
||
/** Returns the JDK's version */ | ||
@Override | ||
@NonNull | ||
public String getVersion() { | ||
return version; | ||
} | ||
|
||
/** | ||
* The path to where the JDK is installed. Can be <code>null</code> which means the JDK | ||
* isn't currently installed by that provider | ||
*/ | ||
@Override | ||
@Nullable | ||
public Path getHome() { | ||
return home; | ||
} | ||
|
||
@Override | ||
public int getMajorVersion() { | ||
return JavaUtils.parseJavaVersion(getVersion()); | ||
} | ||
|
||
@Override | ||
@NonNull | ||
public Jdk install() { | ||
return provider.install(this); | ||
} | ||
|
||
@Override | ||
public void uninstall() { | ||
provider.uninstall(this); | ||
} | ||
|
||
@Override | ||
public boolean isInstalled() { | ||
return home != null; | ||
} | ||
|
||
@Override | ||
public boolean equals(Object o) { | ||
if (this == o) return true; | ||
if (o == null || getClass() != o.getClass()) return false; | ||
Default jdk = (Default) o; | ||
return id.equals(jdk.id) && Objects.equals(home, jdk.home); | ||
} | ||
|
||
@Override | ||
public int hashCode() { | ||
return Objects.hash(home, id); | ||
} | ||
|
||
@Override | ||
public int compareTo(Jdk o) { | ||
return Integer.compare(getMajorVersion(), o.getMajorVersion()); | ||
} | ||
|
||
@Override | ||
public String toString() { | ||
return getMajorVersion() + " (" + version + ", " + id + ", " + home + ")"; | ||
} | ||
} | ||
} |
Large diffs are not rendered by default.
Oops, something went wrong.
180 changes: 180 additions & 0 deletions
180
jdkmanager/src/main/java/dev/jbang/jvm/JdkProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
package dev.jbang.jvm; | ||
|
||
import java.nio.file.Path; | ||
import java.util.*; | ||
|
||
import dev.jbang.jvm.util.JavaUtils; | ||
import org.jspecify.annotations.NonNull; | ||
import org.jspecify.annotations.Nullable; | ||
|
||
/** | ||
* This interface must be implemented by providers that are able to give access to JDKs installed on | ||
* the user's system. Some providers will also be able to manage those JDKs by installing and | ||
* uninstalling them at the user's request. In those cases the <code>canUpdate()</code> should | ||
* return <code>true</code>. | ||
* | ||
* <p>The providers deal in JDK identifiers, not in versions. Those identifiers are specific to the | ||
* implementation but should follow two important rules: 1. they must be unique across | ||
* implementations 2. they must start with an integer specifying the main JDK version | ||
*/ | ||
public interface JdkProvider { | ||
|
||
default Jdk createJdk(@NonNull String id, @Nullable Path home, @NonNull String version) { | ||
return new Jdk.Default(this, id, home, version); | ||
} | ||
|
||
default String name() { | ||
String nm = getClass().getSimpleName(); | ||
return nm.substring(0, nm.length() - 11).toLowerCase(); | ||
} | ||
|
||
/** | ||
* For providers that can update this returns a set of JDKs that are available for installation. | ||
* Providers might set the <code>home</code> field of the JDK objects if the respective JDK is | ||
* currently installed on the user's system, but only if they can ensure that it's the exact | ||
* same version, otherwise they should just leave the field <code>null</code>. | ||
* | ||
* @return List of <code>Jdk</code> objects | ||
*/ | ||
@NonNull | ||
default List<Jdk> listAvailable() { | ||
throw new UnsupportedOperationException( | ||
"Listing available JDKs is not supported by " + getClass().getName()); | ||
} | ||
|
||
/** | ||
* Returns a set of JDKs that are currently installed on the user's system. | ||
* | ||
* @return List of <code>Jdk</code> objects, possibly empty | ||
*/ | ||
@NonNull List<Jdk> listInstalled(); | ||
|
||
/** | ||
* Determines if a JDK of the requested version is currently installed by this provider and if | ||
* so returns its respective <code>Jdk</code> object, otherwise it returns <code>null</code>. If | ||
* <code>openVersion</code> is set to true the method will also return the next installed | ||
* version if the exact version was not found. | ||
* | ||
* @param version The specific JDK version to return | ||
* @param openVersion Return newer version if exact is not available | ||
* @return A <code>Jdk</code> object or <code>null</code> | ||
*/ | ||
@Nullable | ||
default Jdk getJdkByVersion(int version, boolean openVersion) { | ||
List<Jdk> jdks = listInstalled(); | ||
Jdk res; | ||
if (openVersion) { | ||
res = | ||
jdks.stream() | ||
.sorted() | ||
.filter(jdk -> jdk.getMajorVersion() >= version) | ||
.findFirst() | ||
.orElse(null); | ||
} else { | ||
res = | ||
jdks.stream() | ||
.filter(jdk -> jdk.getMajorVersion() == version) | ||
.findFirst() | ||
.orElse(null); | ||
} | ||
return res; | ||
} | ||
|
||
/** | ||
* Determines if the given id refers to a JDK managed by this provider and if so returns its | ||
* respective <code>Jdk</code> object, otherwise it returns <code>null</code>. | ||
* | ||
* @param id The id to look for | ||
* @return A <code>Jdk</code> object or <code>null</code> | ||
*/ | ||
@Nullable Jdk getJdkById(@NonNull String id); | ||
|
||
/** | ||
* Determines if the given path belongs to a JDK managed by this provider and if so returns its | ||
* respective <code>Jdk</code> object, otherwise it returns <code>null</code>. | ||
* | ||
* @param jdkPath The path to look for | ||
* @return A <code>Jdk</code> object or <code>null</code> | ||
*/ | ||
@Nullable Jdk getJdkByPath(@NonNull Path jdkPath); | ||
|
||
/** | ||
* For providers that can update this installs the indicated JDK | ||
* | ||
* @param jdk The <code>Jdk</code> object of the JDK to install | ||
* @return A <code>Jdk</code> object | ||
* @throws UnsupportedOperationException if the provider can not update | ||
*/ | ||
@NonNull | ||
default Jdk install(@NonNull Jdk jdk) { | ||
throw new UnsupportedOperationException( | ||
"Installing a JDK is not supported by " + getClass().getName()); | ||
} | ||
|
||
/** | ||
* Uninstalls the indicated JDK | ||
* | ||
* @param jdk The <code>Jdk</code> object of the JDK to uninstall | ||
* @throws UnsupportedOperationException if the provider can not update | ||
*/ | ||
default void uninstall(@NonNull Jdk jdk) { | ||
throw new UnsupportedOperationException( | ||
"Uninstalling a JDK is not supported by " + getClass().getName()); | ||
} | ||
|
||
/** | ||
* Indicates if the provider can be used or not. This can perform sanity checks like the | ||
* availability of certain package being installed on the system or even if the system is | ||
* running a supported operating system. | ||
* | ||
* @return True if the provider can be used, false otherwise | ||
*/ | ||
default boolean canUse() { | ||
return true; | ||
} | ||
|
||
/** | ||
* Indicates if the provider is able to (un)install JDKs or not | ||
* | ||
* @return True if JDKs can be (un)installed, false otherwise | ||
*/ | ||
default boolean canUpdate() { | ||
return false; | ||
} | ||
|
||
/** | ||
* This is a special "dummy" provider that can be used to create <code>Jdk</code> objects for | ||
* JDKs that don't seem to belong to any of the known providers but for which we still want an | ||
* object to represent them. | ||
*/ | ||
class UnknownJdkProvider implements JdkProvider { | ||
private static final UnknownJdkProvider instance = new UnknownJdkProvider(); | ||
|
||
@NonNull | ||
@Override | ||
public List<Jdk> listInstalled() { | ||
return Collections.emptyList(); | ||
} | ||
|
||
@Nullable | ||
@Override | ||
public Jdk getJdkById(@NonNull String id) { | ||
return null; | ||
} | ||
|
||
@Nullable | ||
@Override | ||
public Jdk getJdkByPath(@NonNull Path jdkPath) { | ||
Optional<String> version = JavaUtils.resolveJavaVersionStringFromPath(jdkPath); | ||
if (version.isPresent()) { | ||
return createJdk("unknown", jdkPath, version.get()); | ||
} else { | ||
return null; | ||
} | ||
} | ||
|
||
public static Jdk createJdk(Path jdkPath) { | ||
return instance.getJdkByPath(jdkPath); | ||
} | ||
} | ||
} |
133 changes: 133 additions & 0 deletions
133
jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/BaseFoldersJdkProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
package dev.jbang.jvm.jdkproviders; | ||
|
||
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.Optional; | ||
import java.util.function.Predicate; | ||
import java.util.logging.Level; | ||
import java.util.logging.Logger; | ||
import java.util.stream.Collectors; | ||
import java.util.stream.Stream; | ||
|
||
import dev.jbang.jvm.Jdk; | ||
import dev.jbang.jvm.JdkProvider; | ||
import dev.jbang.jvm.util.JavaUtils; | ||
import dev.jbang.jvm.util.OsUtils; | ||
import org.jspecify.annotations.NonNull; | ||
import org.jspecify.annotations.Nullable; | ||
|
||
public abstract class BaseFoldersJdkProvider implements JdkProvider { | ||
protected final Path jdksRoot; | ||
|
||
private static final Logger LOGGER = Logger.getLogger(BaseFoldersJdkProvider.class.getName()); | ||
|
||
protected BaseFoldersJdkProvider(Path jdksRoot) { | ||
this.jdksRoot = jdksRoot; | ||
} | ||
|
||
@NonNull | ||
@Override | ||
public List<Jdk> listInstalled() { | ||
if (Files.isDirectory(jdksRoot)) { | ||
try (Stream<Path> jdkPaths = listJdkPaths()) { | ||
return jdkPaths.map(this::createJdk) | ||
.filter(Objects::nonNull) | ||
.sorted(Jdk::compareTo) | ||
.collect(Collectors.toList()); | ||
} catch (IOException e) { | ||
LOGGER.log(Level.FINE, "Couldn't list installed JDKs", e); | ||
} | ||
} | ||
return Collections.emptyList(); | ||
} | ||
|
||
@Nullable | ||
@Override | ||
public Jdk getJdkById(@NonNull String id) { | ||
if (isValidId(id)) { | ||
try (Stream<Path> jdkPaths = listJdkPaths()) { | ||
return jdkPaths.filter(p -> jdkId(p.getFileName().toString()).equals(id)) | ||
.map(this::createJdk) | ||
.filter(Objects::nonNull) | ||
.findFirst() | ||
.orElse(null); | ||
} catch (IOException e) { | ||
LOGGER.log(Level.FINE, "Couldn't list installed JDKs", e); | ||
} | ||
} | ||
return null; | ||
} | ||
|
||
@Nullable | ||
@Override | ||
public Jdk getJdkByPath(@NonNull Path jdkPath) { | ||
if (jdkPath.startsWith(jdksRoot)) { | ||
try (Stream<Path> jdkPaths = listJdkPaths()) { | ||
return jdkPaths.filter(jdkPath::startsWith) | ||
.map(this::createJdk) | ||
.filter(Objects::nonNull) | ||
.findFirst() | ||
.orElse(null); | ||
} catch (IOException e) { | ||
LOGGER.log(Level.FINE, "Couldn't list installed JDKs", e); | ||
} | ||
} | ||
return null; | ||
} | ||
|
||
/** | ||
* Returns a path to the requested JDK. This method should never return <code>null</code> and | ||
* should return the path where the requested JDK is either currently installed or where it | ||
* would be installed if it were available. This only needs to be implemented for providers that | ||
* are updatable. | ||
* | ||
* @param jdk 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); | ||
} | ||
|
||
private Predicate<Path> sameJdk(Path jdkRoot) { | ||
Path release = jdkRoot.resolve("release"); | ||
return (Path p) -> { | ||
try { | ||
return Files.isSameFile(p.resolve("release"), release); | ||
} catch (IOException e) { | ||
return false; | ||
} | ||
}; | ||
} | ||
|
||
protected Stream<Path> listJdkPaths() throws IOException { | ||
if (Files.isDirectory(jdksRoot)) { | ||
return Files.list(jdksRoot); | ||
} | ||
return Stream.empty(); | ||
} | ||
|
||
@Nullable | ||
protected Jdk createJdk(Path home) { | ||
String name = home.getFileName().toString(); | ||
Optional<String> version = JavaUtils.resolveJavaVersionStringFromPath(home); | ||
if (version.isPresent()) { | ||
return createJdk(jdkId(name), home, version.get()); | ||
} | ||
return null; | ||
} | ||
|
||
protected boolean acceptFolder(Path jdkFolder) { | ||
return OsUtils.searchPath("javac", jdkFolder.resolve("bin").toString()) != null; | ||
} | ||
|
||
protected boolean isValidId(String id) { | ||
return id.endsWith("-" + name()); | ||
} | ||
|
||
protected abstract String jdkId(String name); | ||
} |
57 changes: 57 additions & 0 deletions
57
jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/CurrentJdkProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
package dev.jbang.jvm.jdkproviders; | ||
|
||
import java.nio.file.Path; | ||
import java.nio.file.Paths; | ||
import java.util.Collections; | ||
import java.util.List; | ||
import java.util.Optional; | ||
|
||
import dev.jbang.jvm.Jdk; | ||
import dev.jbang.jvm.JdkProvider; | ||
import dev.jbang.jvm.util.JavaUtils; | ||
import org.jspecify.annotations.NonNull; | ||
import org.jspecify.annotations.Nullable; | ||
|
||
/** | ||
* This JDK provider returns the "current" JDK, which is the JDK that is currently being used to run | ||
* JBang. | ||
*/ | ||
public class CurrentJdkProvider implements JdkProvider { | ||
@NonNull | ||
@Override | ||
public List<Jdk> listInstalled() { | ||
String jh = System.getProperty("java.home"); | ||
if (jh != null) { | ||
Path jdkHome = Paths.get(jh); | ||
jdkHome = JavaUtils.jre2jdk(jdkHome); | ||
Optional<String> version = JavaUtils.resolveJavaVersionStringFromPath(jdkHome); | ||
if (version.isPresent()) { | ||
String id = "current"; | ||
return Collections.singletonList(createJdk(id, jdkHome, version.get())); | ||
} | ||
} | ||
return Collections.emptyList(); | ||
} | ||
|
||
@Nullable | ||
@Override | ||
public Jdk getJdkById(@NonNull String id) { | ||
if (id.equals(name())) { | ||
List<Jdk> l = listInstalled(); | ||
if (!l.isEmpty()) { | ||
return l.get(0); | ||
} | ||
} | ||
return null; | ||
} | ||
|
||
@Nullable | ||
@Override | ||
public Jdk getJdkByPath(@NonNull Path jdkPath) { | ||
List<Jdk> installed = listInstalled(); | ||
Jdk def = !installed.isEmpty() ? installed.get(0) : null; | ||
return def != null && def.getHome() != null && jdkPath.startsWith(def.getHome()) | ||
? def | ||
: null; | ||
} | ||
} |
77 changes: 77 additions & 0 deletions
77
jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/DefaultJdkProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
package dev.jbang.jvm.jdkproviders; | ||
|
||
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.Optional; | ||
|
||
import dev.jbang.jvm.Jdk; | ||
import dev.jbang.jvm.JdkProvider; | ||
import dev.jbang.jvm.util.FileUtils; | ||
import dev.jbang.jvm.util.JavaUtils; | ||
import org.jspecify.annotations.NonNull; | ||
import org.jspecify.annotations.Nullable; | ||
|
||
/** | ||
* This JDK provider returns the "default" JDK if it was set (using <code>jbang jdk default</code>). | ||
*/ | ||
public class DefaultJdkProvider implements JdkProvider { | ||
@NonNull private final Path defaultJdkLink; | ||
|
||
public static final String DEFAULT_ID = "default"; | ||
|
||
public DefaultJdkProvider(@NonNull Path defaultJdkLink) { | ||
this.defaultJdkLink = defaultJdkLink; | ||
} | ||
|
||
@NonNull | ||
@Override | ||
public List<Jdk> listInstalled() { | ||
if (Files.isDirectory(defaultJdkLink)) { | ||
Optional<String> version = JavaUtils.resolveJavaVersionStringFromPath(defaultJdkLink); | ||
if (version.isPresent()) { | ||
return Collections.singletonList(createJdk(DEFAULT_ID, defaultJdkLink, version.get())); | ||
} | ||
} | ||
return Collections.emptyList(); | ||
} | ||
|
||
@Nullable | ||
@Override | ||
public Jdk getJdkById(@NonNull String id) { | ||
if (id.equals(DEFAULT_ID)) { | ||
List<Jdk> l = listInstalled(); | ||
if (!l.isEmpty()) { | ||
return l.get(0); | ||
} | ||
} | ||
return null; | ||
} | ||
|
||
@Nullable | ||
@Override | ||
public Jdk getJdkByPath(@NonNull Path jdkPath) { | ||
List<Jdk> installed = listInstalled(); | ||
Jdk def = !installed.isEmpty() ? installed.get(0) : null; | ||
return def != null && def.getHome() != null && jdkPath.startsWith(def.getHome()) | ||
? def | ||
: null; | ||
} | ||
|
||
@Override | ||
public @NonNull Jdk install(@NonNull Jdk jdk) { | ||
Jdk defJdk = getJdkById(DEFAULT_ID); | ||
if (defJdk != null && defJdk.isInstalled() && !jdk.equals(defJdk)) { | ||
uninstall(defJdk); | ||
} | ||
FileUtils.createLink(defaultJdkLink, jdk.getHome()); | ||
return defJdk; | ||
} | ||
|
||
@Override | ||
public void uninstall(@NonNull Jdk jdk) { | ||
FileUtils.deletePath(defaultJdkLink); | ||
} | ||
} |
287 changes: 287 additions & 0 deletions
287
jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/FoojayJdkProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,287 @@ | ||
package dev.jbang.jvm.jdkproviders; | ||
|
||
import java.io.IOException; | ||
import java.io.UnsupportedEncodingException; | ||
import java.net.URLEncoder; | ||
import java.nio.charset.StandardCharsets; | ||
import java.nio.file.Files; | ||
import java.nio.file.Path; | ||
import java.util.*; | ||
import java.util.function.Consumer; | ||
import java.util.logging.Level; | ||
import java.util.logging.Logger; | ||
import java.util.stream.Collectors; | ||
|
||
import dev.jbang.jvm.Jdk; | ||
import dev.jbang.jvm.util.*; | ||
import org.jspecify.annotations.NonNull; | ||
import org.jspecify.annotations.Nullable; | ||
|
||
/** | ||
* JVM's main JDK provider that can download and install the JDKs provided by the Foojay Disco API. | ||
* They get installed in JBang's cache folder. | ||
*/ | ||
public class FoojayJdkProvider extends BaseFoldersJdkProvider { | ||
private static final String FOOJAY_JDK_DOWNLOAD_URL = | ||
"https://api.foojay.io/disco/v3.0/directuris?"; | ||
private static final String FOOJAY_JDK_VERSIONS_URL = | ||
"https://api.foojay.io/disco/v3.0/packages?"; | ||
|
||
private static final Logger LOGGER = Logger.getLogger(FoojayJdkProvider.class.getName()); | ||
|
||
private static class JdkResult { | ||
String java_version; | ||
int major_version; | ||
String release_status; | ||
} | ||
|
||
private static class VersionsResponse { | ||
List<JdkResult> result; | ||
} | ||
|
||
public FoojayJdkProvider(Path jdksPath) { | ||
super(jdksPath); | ||
} | ||
|
||
@NonNull | ||
@Override | ||
public List<Jdk> listAvailable() { | ||
try { | ||
List<Jdk> result = new ArrayList<>(); | ||
Consumer<String> addJdk = | ||
version -> { | ||
result.add(createJdk(jdkId(version), null, version)); | ||
}; | ||
String distro = getVendor(); | ||
if (distro == null) { | ||
VersionsResponse res = | ||
NetUtils.readJsonFromUrl( | ||
getVersionsUrl(OsUtils.getOS(), OsUtils.getArch(), "temurin"), | ||
VersionsResponse.class); | ||
filterEA(res.result).forEach(jdk -> addJdk.accept(jdk.java_version)); | ||
res = | ||
NetUtils.readJsonFromUrl( | ||
getVersionsUrl(OsUtils.getOS(), OsUtils.getArch(), "aoj"), | ||
VersionsResponse.class); | ||
filterEA(res.result).forEach(jdk -> addJdk.accept(jdk.java_version)); | ||
} else { | ||
VersionsResponse res = | ||
NetUtils.readJsonFromUrl( | ||
getVersionsUrl(OsUtils.getOS(), OsUtils.getArch(), distro), | ||
VersionsResponse.class); | ||
filterEA(res.result).forEach(jdk -> addJdk.accept(jdk.java_version)); | ||
} | ||
result.sort(Jdk::compareTo); | ||
return Collections.unmodifiableList(result); | ||
} catch (IOException e) { | ||
LOGGER.log(Level.FINE, "Couldn't list available JDKs", e); | ||
} | ||
return Collections.emptyList(); | ||
} | ||
|
||
// Filter out any EA releases for which a GA with | ||
// the same major version exists | ||
private List<JdkResult> filterEA(List<JdkResult> jdks) { | ||
Set<Integer> GAs = | ||
jdks.stream() | ||
.filter(jdk -> jdk.release_status.equals("ga")) | ||
.map(jdk -> jdk.major_version) | ||
.collect(Collectors.toSet()); | ||
|
||
JdkResult[] lastJdk = new JdkResult[] {null}; | ||
return jdks.stream() | ||
.filter( | ||
jdk -> { | ||
if (lastJdk[0] == null | ||
|| lastJdk[0].major_version != jdk.major_version | ||
&& (jdk.release_status.equals("ga") | ||
|| !GAs.contains(jdk.major_version))) { | ||
lastJdk[0] = jdk; | ||
return true; | ||
} else { | ||
return false; | ||
} | ||
}) | ||
.collect(Collectors.toList()); | ||
} | ||
|
||
@Nullable | ||
@Override | ||
public Jdk getJdkByVersion(int version, boolean openVersion) { | ||
Path jdk = jdksRoot.resolve(Integer.toString(version)); | ||
if (Files.isDirectory(jdk)) { | ||
return createJdk(jdk); | ||
} else if (openVersion) { | ||
return super.getJdkByVersion(version, true); | ||
} | ||
return null; | ||
} | ||
|
||
@NonNull | ||
@Override | ||
public Jdk install(@NonNull Jdk jdk) { | ||
int version = jdkVersion(jdk.getId()); | ||
LOGGER.log( | ||
Level.INFO, | ||
"Downloading JDK {0}. Be patient, this can take several minutes...", | ||
version); | ||
String url = getDownloadUrl(version, OsUtils.getOS(), OsUtils.getArch(), getVendor()); | ||
LOGGER.log(Level.FINE, "Downloading {0}", url); | ||
Path jdkDir = getJdkPath(jdk.getId()); | ||
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 = NetUtils.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); | ||
Optional<String> fullVersion = JavaUtils.resolveJavaVersionStringFromPath(jdkDir); | ||
if (!fullVersion.isPresent()) { | ||
throw new IllegalStateException("Cannot obtain version of recently installed JDK"); | ||
} | ||
return createJdk(jdk.getId(), jdkDir, fullVersion.get()); | ||
} 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."; | ||
/* | ||
Jdk defjdk = JdkManager.getJdk(null, false); | ||
if (defjdk != null) { | ||
msg += | ||
" You can run with '--java " | ||
+ defjdk.getMajorVersion() | ||
+ "' to force using the default installed Java."; | ||
} | ||
*/ | ||
LOGGER.log(Level.FINE, msg); | ||
throw new IllegalStateException( | ||
"Unable to download or install JDK version " + version, e); | ||
} | ||
} | ||
|
||
@Override | ||
public void uninstall(@NonNull Jdk jdk) { | ||
Path jdkDir = getJdkPath(jdk.getId()); | ||
FileUtils.deletePath(jdkDir); | ||
} | ||
|
||
@NonNull | ||
@Override | ||
protected Path getJdkPath(@NonNull String jdk) { | ||
return getJdksPath().resolve(Integer.toString(jdkVersion(jdk))); | ||
} | ||
|
||
@Override | ||
public boolean canUpdate() { | ||
return true; | ||
} | ||
|
||
private static String getDownloadUrl( | ||
int version, OsUtils.OS os, OsUtils.Arch arch, String distro) { | ||
return FOOJAY_JDK_DOWNLOAD_URL + getUrlParams(version, os, arch, distro); | ||
} | ||
|
||
private static String getVersionsUrl(OsUtils.OS os, OsUtils.Arch arch, String distro) { | ||
return FOOJAY_JDK_VERSIONS_URL + getUrlParams(null, os, arch, distro); | ||
} | ||
|
||
private static String getUrlParams( | ||
Integer version, OsUtils.OS os, OsUtils.Arch arch, String distro) { | ||
Map<String, String> params = new HashMap<>(); | ||
if (version != null) { | ||
params.put("version", String.valueOf(version)); | ||
} | ||
|
||
if (distro == null) { | ||
if (version == null || version == 8 || version == 11 || version >= 17) { | ||
distro = "temurin"; | ||
} else { | ||
distro = "aoj"; | ||
} | ||
} | ||
params.put("distro", distro); | ||
|
||
String archiveType; | ||
if (os == OsUtils.OS.windows) { | ||
archiveType = "zip"; | ||
} else { | ||
archiveType = "tar.gz"; | ||
} | ||
params.put("archive_type", archiveType); | ||
|
||
params.put("architecture", arch.name()); | ||
params.put("package_type", "jdk"); | ||
params.put("operating_system", os.name()); | ||
|
||
if (os == OsUtils.OS.windows) { | ||
params.put("libc_type", "c_std_lib"); | ||
} else if (os == OsUtils.OS.mac) { | ||
params.put("libc_type", "libc"); | ||
} else { | ||
params.put("libc_type", "glibc"); | ||
} | ||
|
||
params.put("javafx_bundled", "false"); | ||
params.put("latest", "available"); | ||
params.put("release_status", "ga,ea"); | ||
params.put("directly_downloadable", "true"); | ||
|
||
return urlEncodeUTF8(params); | ||
} | ||
|
||
static String urlEncodeUTF8(Map<?, ?> map) { | ||
StringBuilder sb = new StringBuilder(); | ||
for (Map.Entry<?, ?> entry : map.entrySet()) { | ||
if (sb.length() > 0) { | ||
sb.append("&"); | ||
} | ||
sb.append( | ||
String.format( | ||
"%s=%s", | ||
urlEncodeUTF8(entry.getKey().toString()), | ||
urlEncodeUTF8(entry.getValue().toString()))); | ||
} | ||
return sb.toString(); | ||
} | ||
|
||
static String urlEncodeUTF8(String s) { | ||
return URLEncoder.encode(s, StandardCharsets.UTF_8); | ||
} | ||
|
||
@NonNull | ||
public Path getJdksPath() { | ||
return jdksRoot; | ||
} | ||
|
||
@NonNull | ||
@Override | ||
protected String jdkId(String name) { | ||
int majorVersion = JavaUtils.parseJavaVersion(name); | ||
return majorVersion + "-jbang"; | ||
} | ||
|
||
private static int jdkVersion(String jdk) { | ||
return JavaUtils.parseJavaVersion(jdk); | ||
} | ||
|
||
// TODO refactor | ||
private static String getVendor() { | ||
return null; | ||
} | ||
} |
55 changes: 55 additions & 0 deletions
55
jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/JavaHomeJdkProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
package dev.jbang.jvm.jdkproviders; | ||
|
||
import java.nio.file.Files; | ||
import java.nio.file.Path; | ||
import java.util.Collections; | ||
import java.util.List; | ||
import java.util.Optional; | ||
|
||
import dev.jbang.jvm.Jdk; | ||
import dev.jbang.jvm.JdkProvider; | ||
import dev.jbang.jvm.util.JavaUtils; | ||
import org.jspecify.annotations.NonNull; | ||
import org.jspecify.annotations.Nullable; | ||
|
||
/** | ||
* This JDK provider detects if a JDK is already available on the system by looking at <code> | ||
* JAVA_HOME</code> environment variable. | ||
*/ | ||
public class JavaHomeJdkProvider implements JdkProvider { | ||
@NonNull | ||
@Override | ||
public List<Jdk> listInstalled() { | ||
Path jdkHome = JavaUtils.getJavaHomeEnv(); | ||
if (jdkHome != null && Files.isDirectory(jdkHome)) { | ||
Optional<String> version = JavaUtils.resolveJavaVersionStringFromPath(jdkHome); | ||
if (version.isPresent()) { | ||
String id = "javahome"; | ||
return Collections.singletonList(createJdk(id, jdkHome, version.get())); | ||
} | ||
} | ||
return Collections.emptyList(); | ||
} | ||
|
||
@Nullable | ||
@Override | ||
public Jdk getJdkById(@NonNull String id) { | ||
if (id.equals(name())) { | ||
List<Jdk> l = listInstalled(); | ||
if (!l.isEmpty()) { | ||
return l.get(0); | ||
} | ||
} | ||
return null; | ||
} | ||
|
||
@Nullable | ||
@Override | ||
public Jdk getJdkByPath(@NonNull Path jdkPath) { | ||
List<Jdk> installed = listInstalled(); | ||
Jdk def = !installed.isEmpty() ? installed.get(0) : null; | ||
return def != null && def.getHome() != null && jdkPath.startsWith(def.getHome()) | ||
? def | ||
: null; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
60 changes: 60 additions & 0 deletions
60
jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/PathJdkProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
package dev.jbang.jvm.jdkproviders; | ||
|
||
import java.nio.file.Path; | ||
import java.util.Collections; | ||
import java.util.List; | ||
import java.util.Optional; | ||
|
||
import dev.jbang.jvm.Jdk; | ||
import dev.jbang.jvm.JdkProvider; | ||
import dev.jbang.jvm.util.JavaUtils; | ||
import dev.jbang.jvm.util.OsUtils; | ||
import org.jspecify.annotations.NonNull; | ||
import org.jspecify.annotations.Nullable; | ||
|
||
/** | ||
* This JDK provider detects if a JDK is already available on the system by first looking at the | ||
* user's <code>PATH</code>. | ||
*/ | ||
public class PathJdkProvider implements JdkProvider { | ||
@NonNull | ||
@Override | ||
public List<Jdk> listInstalled() { | ||
Path jdkHome = null; | ||
Path javac = OsUtils.searchPath("javac"); | ||
if (javac != null) { | ||
javac = javac.toAbsolutePath(); | ||
jdkHome = javac.getParent().getParent(); | ||
} | ||
if (jdkHome != null) { | ||
Optional<String> version = JavaUtils.resolveJavaVersionStringFromPath(jdkHome); | ||
if (version.isPresent()) { | ||
String id = "path"; | ||
return Collections.singletonList(createJdk(id, jdkHome, version.get())); | ||
} | ||
} | ||
return Collections.emptyList(); | ||
} | ||
|
||
@Nullable | ||
@Override | ||
public Jdk getJdkById(@NonNull String id) { | ||
if (id.equals(name())) { | ||
List<Jdk> l = listInstalled(); | ||
if (!l.isEmpty()) { | ||
return l.get(0); | ||
} | ||
} | ||
return null; | ||
} | ||
|
||
@Nullable | ||
@Override | ||
public Jdk getJdkByPath(@NonNull Path jdkPath) { | ||
List<Jdk> installed = listInstalled(); | ||
Jdk def = !installed.isEmpty() ? installed.get(0) : null; | ||
return def != null && def.getHome() != null && jdkPath.startsWith(def.getHome()) | ||
? def | ||
: null; | ||
} | ||
} |
59 changes: 59 additions & 0 deletions
59
jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/ScoopJdkProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
package dev.jbang.jvm.jdkproviders; | ||
|
||
import java.io.IOException; | ||
import java.nio.file.Files; | ||
import java.nio.file.Path; | ||
import java.nio.file.Paths; | ||
import java.util.stream.Stream; | ||
|
||
import dev.jbang.jvm.Jdk; | ||
import dev.jbang.jvm.util.OsUtils; | ||
import org.jspecify.annotations.NonNull; | ||
import org.jspecify.annotations.Nullable; | ||
|
||
/** | ||
* This JDK provider detects any JDKs that have been installed using the Scoop package manager. | ||
* Windows only. | ||
*/ | ||
public class ScoopJdkProvider extends BaseFoldersJdkProvider { | ||
private static final Path SCOOP_APPS = | ||
Paths.get(System.getProperty("user.home")).resolve("scoop/apps"); | ||
|
||
public ScoopJdkProvider() { | ||
super(SCOOP_APPS); | ||
} | ||
|
||
@NonNull | ||
@Override | ||
protected Stream<Path> listJdkPaths() throws IOException { | ||
if (Files.isDirectory(jdksRoot)) { | ||
try (Stream<Path> paths = Files.list(jdksRoot)) { | ||
return paths.filter(p -> p.getFileName().startsWith("openjdk")) | ||
.map(p -> p.resolve("current")); | ||
} | ||
} | ||
return Stream.empty(); | ||
} | ||
|
||
@Override | ||
protected String jdkId(String name) { | ||
return name + "-scoop"; | ||
} | ||
|
||
@Nullable | ||
@Override | ||
protected Jdk createJdk(Path home) { | ||
try { | ||
// Try to resolve any links | ||
home = home.toRealPath(); | ||
} catch (IOException e) { | ||
throw new IllegalStateException("Couldn't resolve 'current' link: " + home, e); | ||
} | ||
return super.createJdk(home); | ||
} | ||
|
||
@Override | ||
public boolean canUse() { | ||
return OsUtils.isWindows() && Files.isDirectory(SCOOP_APPS); | ||
} | ||
} |
28 changes: 28 additions & 0 deletions
28
jdkmanager/src/main/java/dev/jbang/jvm/jdkproviders/SdkmanJdkProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package dev.jbang.jvm.jdkproviders; | ||
|
||
import java.nio.file.Files; | ||
import java.nio.file.Path; | ||
import java.nio.file.Paths; | ||
import org.jspecify.annotations.NonNull; | ||
import org.jspecify.annotations.Nullable; | ||
|
||
/** This JDK provider detects any JDKs that have been installed using the SDKMAN package manager. */ | ||
public class SdkmanJdkProvider extends BaseFoldersJdkProvider { | ||
private static final Path JDKS_ROOT = | ||
Paths.get(System.getProperty("user.home")).resolve(".sdkman/candidates/java"); | ||
|
||
public SdkmanJdkProvider() { | ||
super(JDKS_ROOT); | ||
} | ||
|
||
@Nullable | ||
@Override | ||
protected String jdkId(String name) { | ||
return name + "-sdkman"; | ||
} | ||
|
||
@Override | ||
public boolean canUse() { | ||
return Files.isDirectory(JDKS_ROOT); | ||
} | ||
} |
73 changes: 73 additions & 0 deletions
73
jdkmanager/src/main/java/dev/jbang/jvm/util/FileHttpCacheStorage.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
package dev.jbang.jvm.util; | ||
|
||
import java.io.*; | ||
import java.nio.file.Files; | ||
import java.nio.file.Path; | ||
import org.apache.http.client.cache.HttpCacheEntry; | ||
import org.apache.http.client.cache.HttpCacheStorage; | ||
import org.apache.http.client.cache.HttpCacheUpdateCallback; | ||
import org.apache.http.client.cache.HttpCacheUpdateException; | ||
import org.apache.http.impl.client.cache.DefaultHttpCacheEntrySerializer; | ||
|
||
public class FileHttpCacheStorage implements HttpCacheStorage { | ||
|
||
private final Path cacheDir; | ||
private final DefaultHttpCacheEntrySerializer serializer; | ||
|
||
public FileHttpCacheStorage(Path cacheDir) { | ||
this.cacheDir = cacheDir; | ||
this.serializer = new DefaultHttpCacheEntrySerializer(); | ||
try { | ||
Files.createDirectories(cacheDir); | ||
} catch (IOException e) { | ||
throw new RuntimeException("Failed to create cache directory", e); | ||
} | ||
} | ||
|
||
@Override | ||
public synchronized void putEntry(String key, HttpCacheEntry entry) throws IOException { | ||
Path filePath = cacheDir.resolve(encodeKey(key)); | ||
try (OutputStream os = Files.newOutputStream(filePath); | ||
BufferedOutputStream bos = new BufferedOutputStream(os)) { | ||
serializer.writeTo(entry, bos); | ||
} | ||
} | ||
|
||
@Override | ||
public synchronized HttpCacheEntry getEntry(String key) throws IOException { | ||
Path filePath = cacheDir.resolve(encodeKey(key)); | ||
if (Files.exists(filePath)) { | ||
try (InputStream is = Files.newInputStream(filePath); | ||
BufferedInputStream bis = new BufferedInputStream(is)) { | ||
return serializer.readFrom(bis); | ||
} | ||
} | ||
return null; | ||
} | ||
|
||
@Override | ||
public synchronized void removeEntry(String key) throws IOException { | ||
Path filePath = cacheDir.resolve(encodeKey(key)); | ||
Files.deleteIfExists(filePath); | ||
} | ||
|
||
@Override | ||
public synchronized void updateEntry(String key, HttpCacheUpdateCallback callback) | ||
throws IOException, HttpCacheUpdateException { | ||
Path filePath = cacheDir.resolve(encodeKey(key)); | ||
HttpCacheEntry existingEntry = null; | ||
if (Files.exists(filePath)) { | ||
try (InputStream is = Files.newInputStream(filePath); | ||
BufferedInputStream bis = new BufferedInputStream(is)) { | ||
existingEntry = serializer.readFrom(bis); | ||
} | ||
} | ||
HttpCacheEntry updatedEntry = callback.update(existingEntry); | ||
putEntry(key, updatedEntry); | ||
} | ||
|
||
private String encodeKey(String key) { | ||
// You can use more sophisticated encoding if necessary | ||
return key.replaceAll("[^a-zA-Z0-9-_]", "_"); | ||
} | ||
} |
122 changes: 122 additions & 0 deletions
122
jdkmanager/src/main/java/dev/jbang/jvm/util/FileUtils.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
package dev.jbang.jvm.util; | ||
|
||
import java.io.IOException; | ||
import java.nio.file.AccessDeniedException; | ||
import java.nio.file.Files; | ||
import java.nio.file.LinkOption; | ||
import java.nio.file.Path; | ||
import java.util.Comparator; | ||
import java.util.logging.Level; | ||
import java.util.logging.Logger; | ||
import java.util.stream.Stream; | ||
|
||
public class FileUtils { | ||
private static final Logger LOGGER = Logger.getLogger(JavaUtils.class.getName()); | ||
|
||
public static void createLink(Path link, Path target) { | ||
if (!Files.exists(link)) { | ||
// On Windows we use junction for directories because their | ||
// creation doesn't require any special privileges. | ||
if (OsUtils.isWindows() && Files.isDirectory(target)) { | ||
if (createJunction(link, target.toAbsolutePath())) { | ||
return; | ||
} | ||
} else { | ||
if (createSymbolicLink(link, target.toAbsolutePath())) { | ||
return; | ||
} | ||
} | ||
throw new IllegalStateException("Failed to create link " + link + " -> " + target); | ||
} | ||
} | ||
|
||
private static boolean createSymbolicLink(Path link, Path target) { | ||
try { | ||
mkdirs(link.getParent()); | ||
Files.createSymbolicLink(link, target); | ||
return true; | ||
} catch (IOException e) { | ||
if (OsUtils.isWindows() | ||
&& e instanceof AccessDeniedException | ||
&& e.getMessage().contains("privilege")) { | ||
LOGGER.log( | ||
Level.INFO, | ||
"Creation of symbolic link failed {0} -> {1}}", | ||
new Object[] {link, target}); | ||
LOGGER.info( | ||
"This is a known issue with trying to create symbolic links on Windows."); | ||
LOGGER.info("See the information available at the link below for a solution:"); | ||
LOGGER.info( | ||
"https://www.jbang.dev/documentation/guide/latest/usage.html#usage-on-windows"); | ||
} | ||
LOGGER.log(Level.FINE, "Failed to create symbolic link " + link + " -> " + target, e); | ||
} | ||
return false; | ||
} | ||
|
||
private static boolean createJunction(Path link, Path target) { | ||
if (!Files.exists(link) && Files.exists(link, LinkOption.NOFOLLOW_LINKS)) { | ||
// We automatically remove broken links | ||
deletePath(link); | ||
} | ||
mkdirs(link.getParent()); | ||
return OsUtils.runCommand( | ||
"cmd.exe", "/c", "mklink", "/j", link.toString(), target.toString()) | ||
!= null; | ||
} | ||
|
||
/** | ||
* Returns true if the final part of the path is a symbolic link. | ||
* @param path The path to check | ||
* @return true if the final part of the path is a symbolic link | ||
* @throws IOException if an I/O error occurs | ||
*/ | ||
public static boolean isLink(Path path) throws IOException { | ||
Path parent = path.toAbsolutePath().getParent().toRealPath(); | ||
Path absPath = parent.resolve(path.getFileName()); | ||
return !absPath.toRealPath().equals(absPath.toRealPath(LinkOption.NOFOLLOW_LINKS)); | ||
} | ||
|
||
public static void mkdirs(Path p) { | ||
try { | ||
Files.createDirectories(p); | ||
} catch (IOException e) { | ||
throw new IllegalStateException("Failed to create directory " + p, e); | ||
} | ||
} | ||
|
||
public static void deletePath(Path path) { | ||
try { | ||
if (isLink(path)) { | ||
LOGGER.log(Level.FINE, "Deleting link {0}", path); | ||
Files.delete(path); | ||
} else if (Files.isDirectory(path)) { | ||
LOGGER.log(Level.FINE, "Deleting folder {0}", path); | ||
try (Stream<Path> s = Files.walk(path)) { | ||
s.sorted(Comparator.reverseOrder()) | ||
.forEach( | ||
f -> { | ||
try { | ||
Files.delete(f); | ||
} catch (IOException e) { | ||
throw new IllegalStateException("Failed to delete " + f, e); | ||
} | ||
}); | ||
} | ||
} else if (Files.exists(path)) { | ||
LOGGER.log(Level.FINE, "Deleting file {0}", path); | ||
Files.delete(path); | ||
} else if (Files.exists(path, LinkOption.NOFOLLOW_LINKS)) { | ||
LOGGER.log(Level.FINE, "Deleting broken link {0}", path); | ||
Files.delete(path); | ||
} | ||
} catch (Exception e) { | ||
throw new IllegalStateException("Failed to delete " + path, e); | ||
} | ||
} | ||
|
||
public static String extension(String name) { | ||
int p = name.lastIndexOf('.'); | ||
return p > 0 ? name.substring(p + 1) : ""; | ||
} | ||
} |
130 changes: 130 additions & 0 deletions
130
jdkmanager/src/main/java/dev/jbang/jvm/util/JavaUtils.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
package dev.jbang.jvm.util; | ||
|
||
import static java.lang.System.getenv; | ||
|
||
import java.io.IOException; | ||
import java.nio.file.Files; | ||
import java.nio.file.Path; | ||
import java.nio.file.Paths; | ||
import java.util.Optional; | ||
import java.util.logging.Level; | ||
import java.util.logging.Logger; | ||
import java.util.regex.Matcher; | ||
import java.util.regex.Pattern; | ||
import java.util.stream.Stream; | ||
|
||
public class JavaUtils { | ||
|
||
private static final Pattern javaVersionPattern = Pattern.compile("\"([^\"]+)\""); | ||
|
||
private static final Logger LOGGER = Logger.getLogger(JavaUtils.class.getName()); | ||
|
||
public static boolean isRequestedVersion(String rv) { | ||
return rv.matches("\\d+[+]?"); | ||
} | ||
|
||
public static int minRequestedVersion(String rv) { | ||
return Integer.parseInt(isOpenVersion(rv) ? rv.substring(0, rv.length() - 1) : rv); | ||
} | ||
|
||
public static boolean isOpenVersion(String version) { | ||
return version.endsWith("+"); | ||
} | ||
|
||
public static int parseJavaVersion(String version) { | ||
if (version != null) { | ||
try { | ||
String[] nums = version.split("[-.+]"); | ||
String num = nums.length > 1 && nums[0].equals("1") ? nums[1] : nums[0]; | ||
return Integer.parseInt(num); | ||
} catch (NumberFormatException ex) { | ||
// Ignore | ||
} | ||
} | ||
return 0; | ||
} | ||
|
||
public static Optional<Integer> resolveJavaVersionFromPath(Path home) { | ||
return resolveJavaVersionStringFromPath(home).map(JavaUtils::parseJavaVersion); | ||
} | ||
|
||
public static Optional<String> resolveJavaVersionStringFromPath(Path home) { | ||
Optional<String> res = readJavaVersionStringFromReleaseFile(home); | ||
if (!res.isPresent()) { | ||
res = readJavaVersionStringFromJavaCommand(home); | ||
} | ||
return res; | ||
} | ||
|
||
public static Optional<String> readJavaVersionStringFromReleaseFile(Path home) { | ||
try (Stream<String> lines = Files.lines(home.resolve("release"))) { | ||
return lines.filter( | ||
l -> | ||
l.startsWith("JAVA_VERSION=") | ||
|| l.startsWith("JAVA_RUNTIME_VERSION=")) | ||
.map(JavaUtils::parseJavaOutput) | ||
.findAny(); | ||
} catch (IOException e) { | ||
LOGGER.fine("Unable to read 'release' file in path: " + home); | ||
return Optional.empty(); | ||
} | ||
} | ||
|
||
public static Optional<String> readJavaVersionStringFromJavaCommand(Path home) { | ||
Optional<String> res; | ||
Path javaCmd = OsUtils.searchPath("java", home.resolve("bin").toString()); | ||
if (javaCmd != null) { | ||
String output = OsUtils.runCommand(javaCmd.toString(), "-version"); | ||
res = Optional.ofNullable(parseJavaOutput(output)); | ||
} else { | ||
res = Optional.empty(); | ||
} | ||
if (!res.isPresent()) { | ||
LOGGER.log(Level.FINE, "Unable to obtain version from: '{0} -version'", javaCmd); | ||
} | ||
return res; | ||
} | ||
|
||
public static String parseJavaOutput(String output) { | ||
if (output != null) { | ||
Matcher m = javaVersionPattern.matcher(output); | ||
if (m.find() && m.groupCount() == 1) { | ||
return m.group(1); | ||
} | ||
} | ||
return null; | ||
} | ||
|
||
/** | ||
* Returns the Path to JAVA_HOME | ||
* | ||
* @return A Path pointing to JAVA_HOME or null if it isn't defined | ||
*/ | ||
public static Path getJavaHomeEnv() { | ||
if (getenv("JAVA_HOME") != null) { | ||
return Paths.get(getenv("JAVA_HOME")); | ||
} else { | ||
return null; | ||
} | ||
} | ||
|
||
/** | ||
* Method takes the given path which might point to a Java home directory or to the `jre` | ||
* directory inside it and makes sure to return the path to the actual home directory. | ||
*/ | ||
public static Path jre2jdk(Path jdkHome) { | ||
// Detect if the current JDK is a JRE and try to find the real home | ||
if (!Files.isRegularFile(jdkHome.resolve("release"))) { | ||
Path jh = jdkHome.toAbsolutePath(); | ||
try { | ||
jh = jh.toRealPath(); | ||
} catch (IOException e) { | ||
// Ignore error | ||
} | ||
if (jh.endsWith("jre") && Files.isRegularFile(jh.getParent().resolve("release"))) { | ||
jdkHome = jh.getParent(); | ||
} | ||
} | ||
return jdkHome; | ||
} | ||
} |
115 changes: 115 additions & 0 deletions
115
jdkmanager/src/main/java/dev/jbang/jvm/util/NetUtils.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
package dev.jbang.jvm.util; | ||
|
||
import com.google.gson.Gson; | ||
import com.google.gson.GsonBuilder; | ||
import java.io.IOException; | ||
import java.io.InputStream; | ||
import java.io.InputStreamReader; | ||
import java.io.UncheckedIOException; | ||
import java.nio.file.Path; | ||
import java.nio.file.Paths; | ||
import java.util.function.Function; | ||
import org.apache.http.HttpEntity; | ||
import org.apache.http.HttpResponse; | ||
import org.apache.http.client.config.RequestConfig; | ||
import org.apache.http.client.methods.CloseableHttpResponse; | ||
import org.apache.http.client.methods.HttpGet; | ||
import org.apache.http.entity.ContentType; | ||
import org.apache.http.impl.client.CloseableHttpClient; | ||
import org.apache.http.impl.client.HttpClientBuilder; | ||
import org.apache.http.impl.client.cache.CacheConfig; | ||
import org.apache.http.impl.client.cache.CachingHttpClientBuilder; | ||
|
||
public class NetUtils { | ||
|
||
public static final RequestConfig DEFAULT_REQUEST_CONFIG = | ||
RequestConfig.custom() | ||
.setConnectionRequestTimeout(10000) | ||
.setConnectTimeout(10000) | ||
.setSocketTimeout(30000) | ||
.build(); | ||
|
||
public static <T> T readJsonFromUrl(String url, Class<T> klass) throws IOException { | ||
HttpClientBuilder builder = createDefaultHttpClientBuilder(); | ||
return readJsonFromUrl(builder, url, klass); | ||
} | ||
|
||
public static <T> T readJsonFromUrl(HttpClientBuilder builder, String url, Class<T> klass) | ||
throws IOException { | ||
return requestUrl(builder, url, response -> handleJsonResult(klass, response)); | ||
} | ||
|
||
public static Path downloadFromUrl(String url) throws IOException { | ||
HttpClientBuilder builder = createDefaultHttpClientBuilder(); | ||
return downloadFromUrl(builder, url); | ||
} | ||
|
||
public static Path downloadFromUrl(HttpClientBuilder builder, String url) throws IOException { | ||
return requestUrl(builder, url, NetUtils::handleDownloadResult); | ||
} | ||
|
||
public static HttpClientBuilder createDefaultHttpClientBuilder() { | ||
CacheConfig cacheConfig = CacheConfig.custom().setMaxCacheEntries(1000).build(); | ||
|
||
FileHttpCacheStorage cacheStorage = new FileHttpCacheStorage(Paths.get("http-cache")); | ||
|
||
// return HttpClientBuilder.create().setDefaultRequestConfig(DEFAULT_REQUEST_CONFIG); | ||
return CachingHttpClientBuilder.create() | ||
.setCacheConfig(cacheConfig) | ||
.setHttpCacheStorage(cacheStorage) | ||
.setDefaultRequestConfig(DEFAULT_REQUEST_CONFIG); | ||
} | ||
|
||
public static <T> T requestUrl( | ||
HttpClientBuilder builder, String url, Function<HttpResponse, T> responseHandler) | ||
throws IOException { | ||
try (CloseableHttpClient httpClient = builder.build()) { | ||
HttpGet httpGet = new HttpGet(url); | ||
try (CloseableHttpResponse response = httpClient.execute(httpGet)) { | ||
int responseCode = response.getStatusLine().getStatusCode(); | ||
if (responseCode != 200) { | ||
throw new IOException( | ||
"Failed to read from URL: " | ||
+ url | ||
+ ", response code: #" | ||
+ responseCode); | ||
} | ||
HttpEntity entity = response.getEntity(); | ||
if (entity == null) { | ||
throw new IOException("Failed to read from URL: " + url + ", no content"); | ||
} | ||
return responseHandler.apply(response); | ||
} | ||
} catch (UncheckedIOException e) { | ||
throw new IOException("Failed to read from URL: " + url + ", " + e.getMessage(), e); | ||
} | ||
} | ||
|
||
private static <T> T handleJsonResult(Class<T> klass, HttpResponse response) { | ||
try { | ||
String mimeType = ContentType.getOrDefault(response.getEntity()).getMimeType(); | ||
if (!mimeType.equals("application/json")) { | ||
throw new IOException("Unexpected MIME type: " + mimeType); | ||
} | ||
HttpEntity entity = response.getEntity(); | ||
try (InputStream is = entity.getContent()) { | ||
Gson parser = new GsonBuilder().create(); | ||
return parser.fromJson(new InputStreamReader(is), klass); | ||
} | ||
} catch (IOException e) { | ||
throw new UncheckedIOException(e); | ||
} | ||
} | ||
|
||
private static Path handleDownloadResult(HttpResponse response) { | ||
try { | ||
HttpEntity entity = response.getEntity(); | ||
try (InputStream is = entity.getContent()) { | ||
// TODO implement | ||
return null; | ||
} | ||
} catch (IOException e) { | ||
throw new UncheckedIOException(e); | ||
} | ||
} | ||
} |
181 changes: 181 additions & 0 deletions
181
jdkmanager/src/main/java/dev/jbang/jvm/util/OsUtils.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,181 @@ | ||
package dev.jbang.jvm.util; | ||
|
||
import java.io.BufferedReader; | ||
import java.io.File; | ||
import java.io.IOException; | ||
import java.io.InputStreamReader; | ||
import java.nio.file.Files; | ||
import java.nio.file.Path; | ||
import java.nio.file.Paths; | ||
import java.util.Arrays; | ||
import java.util.Locale; | ||
import java.util.logging.Level; | ||
import java.util.logging.Logger; | ||
import java.util.stream.Collectors; | ||
import java.util.stream.Stream; | ||
|
||
public class OsUtils { | ||
|
||
private static final Logger LOGGER = Logger.getLogger(OsUtils.class.getName()); | ||
|
||
public enum OS { | ||
linux, | ||
mac, | ||
windows, | ||
aix, | ||
unknown | ||
} | ||
|
||
public enum Arch { | ||
x32, | ||
x64, | ||
aarch64, | ||
arm, | ||
arm64, | ||
ppc64, | ||
ppc64le, | ||
s390x, | ||
riscv64, | ||
unknown | ||
} | ||
|
||
public static OS getOS() { | ||
String os = | ||
System.getProperty("os.name") | ||
.toLowerCase(Locale.ENGLISH) | ||
.replaceAll("[^a-z0-9]+", ""); | ||
if (os.startsWith("mac") || os.startsWith("osx")) { | ||
return OS.mac; | ||
} else if (os.startsWith("linux")) { | ||
return OS.linux; | ||
} else if (os.startsWith("win")) { | ||
return OS.windows; | ||
} else if (os.startsWith("aix")) { | ||
return OS.aix; | ||
} else { | ||
LOGGER.log(Level.FINE, "Unknown OS: {0}", os); | ||
return OS.unknown; | ||
} | ||
} | ||
|
||
public static Arch getArch() { | ||
String arch = | ||
System.getProperty("os.arch") | ||
.toLowerCase(Locale.ENGLISH) | ||
.replaceAll("[^a-z0-9]+", ""); | ||
if (arch.matches("^(x8664|amd64|ia32e|em64t|x64)$")) { | ||
return Arch.x64; | ||
} else if (arch.matches("^(x8632|x86|i[3-6]86|ia32|x32)$")) { | ||
return Arch.x32; | ||
} else if (arch.matches("^(aarch64)$")) { | ||
return Arch.aarch64; | ||
} else if (arch.matches("^(arm)$")) { | ||
return Arch.arm; | ||
} else if (arch.matches("^(ppc64)$")) { | ||
return Arch.ppc64; | ||
} else if (arch.matches("^(ppc64le)$")) { | ||
return Arch.ppc64le; | ||
} else if (arch.matches("^(s390x)$")) { | ||
return Arch.s390x; | ||
} else if (arch.matches("^(arm64)$")) { | ||
return Arch.arm64; | ||
} else if (arch.matches("^(riscv64)$")) { | ||
return Arch.riscv64; | ||
} else { | ||
LOGGER.log(Level.FINE, "Unknown Arch: {0}", arch); | ||
return Arch.unknown; | ||
} | ||
} | ||
|
||
public static boolean isWindows() { | ||
return getOS() == OS.windows; | ||
} | ||
|
||
public static boolean isMac() { | ||
return getOS() == OS.mac; | ||
} | ||
|
||
/** | ||
* Searches the locations defined by PATH for the given executable | ||
* | ||
* @param cmd The name of the executable to look for | ||
* @return A Path to the executable, if found, null otherwise | ||
*/ | ||
public static Path searchPath(String cmd) { | ||
String envPath = System.getenv("PATH"); | ||
envPath = envPath != null ? envPath : ""; | ||
return searchPath(cmd, envPath); | ||
} | ||
|
||
/** | ||
* Searches the locations defined by `paths` for the given executable | ||
* | ||
* @param cmd The name of the executable to look for | ||
* @param paths A string containing the paths to search | ||
* @return A Path to the executable, if found, null otherwise | ||
*/ | ||
public static Path searchPath(String cmd, String paths) { | ||
return Arrays.stream(paths.split(File.pathSeparator)) | ||
.map(dir -> Paths.get(dir).resolve(cmd)) | ||
.flatMap(OsUtils::executables) | ||
.filter(OsUtils::isExecutable) | ||
.findFirst() | ||
.orElse(null); | ||
} | ||
|
||
private static Stream<Path> executables(Path base) { | ||
if (isWindows()) { | ||
return Stream.of( | ||
Paths.get(base.toString() + ".exe"), | ||
Paths.get(base.toString() + ".bat"), | ||
Paths.get(base.toString() + ".cmd"), | ||
Paths.get(base.toString() + ".ps1")); | ||
} else { | ||
return Stream.of(base); | ||
} | ||
} | ||
|
||
private static boolean isExecutable(Path file) { | ||
if (Files.isRegularFile(file)) { | ||
if (isWindows()) { | ||
String nm = file.getFileName().toString().toLowerCase(); | ||
return nm.endsWith(".exe") | ||
|| nm.endsWith(".bat") | ||
|| nm.endsWith(".cmd") | ||
|| nm.endsWith(".ps1"); | ||
} else { | ||
return Files.isExecutable(file); | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
/** | ||
* Runs the given command + arguments and returns its output (both stdout and stderr) as a | ||
* string | ||
* | ||
* @param cmd The command to execute | ||
* @return The output of the command or null if anything went wrong | ||
*/ | ||
public static String runCommand(String... cmd) { | ||
try { | ||
ProcessBuilder pb = new ProcessBuilder(cmd); | ||
pb.redirectErrorStream(true); | ||
Process p = pb.start(); | ||
BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream())); | ||
String cmdOutput = br.lines().collect(Collectors.joining("\n")); | ||
int exitCode = p.waitFor(); | ||
if (exitCode == 0) { | ||
return cmdOutput; | ||
} else { | ||
LOGGER.log( | ||
Level.FINE, | ||
"Command failed: #{0} - {1}", | ||
new Object[] {exitCode, cmdOutput}); | ||
} | ||
} catch (IOException | InterruptedException ex) { | ||
LOGGER.log(Level.FINE, "Error running: " + String.join(" ", cmd), ex); | ||
} | ||
return null; | ||
} | ||
} |
219 changes: 219 additions & 0 deletions
219
jdkmanager/src/main/java/dev/jbang/jvm/util/UnpackUtils.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,219 @@ | ||
package dev.jbang.jvm.util; | ||
|
||
import java.io.IOException; | ||
import java.io.InputStream; | ||
import java.nio.file.Files; | ||
import java.nio.file.Path; | ||
import java.nio.file.Paths; | ||
import java.nio.file.StandardCopyOption; | ||
import java.nio.file.attribute.PosixFilePermission; | ||
import java.util.*; | ||
import org.apache.commons.compress.archivers.tar.TarArchiveEntry; | ||
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; | ||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; | ||
import org.apache.commons.compress.archivers.zip.ZipFile; | ||
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; | ||
|
||
public class UnpackUtils { | ||
|
||
public static void unpackJdk(Path archive, Path outputDir) throws IOException { | ||
String name = archive.toString().toLowerCase(Locale.ENGLISH); | ||
Path selectFolder = OsUtils.isMac() ? Paths.get("Contents/Home") : null; | ||
if (name.endsWith(".zip")) { | ||
unzip(archive, outputDir, true, selectFolder, UnpackUtils::defaultZipEntryCopy); | ||
} else if (name.endsWith(".tar.gz") || name.endsWith(".tgz")) { | ||
untargz(archive, outputDir, true, selectFolder); | ||
} | ||
} | ||
|
||
public static void unpack(Path archive, Path outputDir) throws IOException { | ||
unpack(archive, outputDir, false); | ||
} | ||
|
||
public static void unpack(Path archive, Path outputDir, boolean stripRootFolder) | ||
throws IOException { | ||
unpack(archive, outputDir, stripRootFolder, null); | ||
} | ||
|
||
public static void unpack( | ||
Path archive, Path outputDir, boolean stripRootFolder, Path selectFolder) | ||
throws IOException { | ||
String name = archive.toString().toLowerCase(Locale.ENGLISH); | ||
if (name.endsWith(".zip") || name.endsWith(".jar")) { | ||
unzip( | ||
archive, | ||
outputDir, | ||
stripRootFolder, | ||
selectFolder, | ||
UnpackUtils::defaultZipEntryCopy); | ||
} else if (name.endsWith(".tar.gz") || name.endsWith(".tgz")) { | ||
untargz(archive, outputDir, stripRootFolder, selectFolder); | ||
} else { | ||
throw new IllegalArgumentException( | ||
"Unsupported archive format: " + FileUtils.extension(archive.toString())); | ||
} | ||
} | ||
|
||
public static void unzip( | ||
Path zip, | ||
Path outputDir, | ||
boolean stripRootFolder, | ||
Path selectFolder, | ||
ExistingZipFileHandler onExisting) | ||
throws IOException { | ||
try (ZipFile zipFile = new ZipFile(zip.toFile())) { | ||
Enumeration<ZipArchiveEntry> entries = zipFile.getEntries(); | ||
while (entries.hasMoreElements()) { | ||
ZipArchiveEntry zipEntry = entries.nextElement(); | ||
Path entry = Paths.get(zipEntry.getName()); | ||
if (stripRootFolder) { | ||
if (entry.getNameCount() == 1) { | ||
continue; | ||
} | ||
entry = entry.subpath(1, entry.getNameCount()); | ||
} | ||
if (selectFolder != null) { | ||
if (!entry.startsWith(selectFolder) || entry.equals(selectFolder)) { | ||
continue; | ||
} | ||
entry = entry.subpath(selectFolder.getNameCount(), entry.getNameCount()); | ||
} | ||
entry = outputDir.resolve(entry).normalize(); | ||
if (!entry.startsWith(outputDir)) { | ||
throw new IOException( | ||
"Entry is outside of the target dir: " + zipEntry.getName()); | ||
} | ||
if (zipEntry.isDirectory()) { | ||
Files.createDirectories(entry); | ||
} else if (zipEntry.isUnixSymlink()) { | ||
Scanner s = new Scanner(zipFile.getInputStream(zipEntry)).useDelimiter("\\A"); | ||
String result = s.hasNext() ? s.next() : ""; | ||
Files.createSymbolicLink(entry, Paths.get(result)); | ||
} else { | ||
if (!Files.isDirectory(entry.getParent())) { | ||
Files.createDirectories(entry.getParent()); | ||
} | ||
if (Files.isRegularFile(entry)) { | ||
onExisting.handle(zipFile, zipEntry, entry); | ||
} else { | ||
defaultZipEntryCopy(zipFile, zipEntry, entry); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
public interface ExistingZipFileHandler { | ||
void handle(ZipFile zipFile, ZipArchiveEntry zipEntry, Path outFile) throws IOException; | ||
} | ||
|
||
public static void defaultZipEntryCopy(ZipFile zipFile, ZipArchiveEntry zipEntry, Path outFile) | ||
throws IOException { | ||
try (InputStream zis = zipFile.getInputStream(zipEntry)) { | ||
Files.copy(zis, outFile, StandardCopyOption.REPLACE_EXISTING); | ||
} | ||
int mode = zipEntry.getUnixMode(); | ||
if (mode != 0 && !OsUtils.isWindows()) { | ||
Set<PosixFilePermission> permissions = | ||
PosixFilePermissionSupport.toPosixFilePermissions(mode); | ||
Files.setPosixFilePermissions(outFile, permissions); | ||
} | ||
} | ||
|
||
public static void untargz( | ||
Path targz, Path outputDir, boolean stripRootFolder, Path selectFolder) | ||
throws IOException { | ||
try (TarArchiveInputStream tarArchiveInputStream = | ||
new TarArchiveInputStream( | ||
new GzipCompressorInputStream( | ||
Files.newInputStream(targz.toFile().toPath())))) { | ||
TarArchiveEntry targzEntry; | ||
while ((targzEntry = tarArchiveInputStream.getNextEntry()) != null) { | ||
Path entry = Paths.get(targzEntry.getName()).normalize(); | ||
if (stripRootFolder) { | ||
if (entry.getNameCount() == 1) { | ||
continue; | ||
} | ||
entry = entry.subpath(1, entry.getNameCount()); | ||
} | ||
if (selectFolder != null) { | ||
if (!entry.startsWith(selectFolder) || entry.equals(selectFolder)) { | ||
continue; | ||
} | ||
entry = entry.subpath(selectFolder.getNameCount(), entry.getNameCount()); | ||
} | ||
entry = outputDir.resolve(entry).normalize(); | ||
if (!entry.startsWith(outputDir)) { | ||
throw new IOException( | ||
"Entry is outside of the target dir: " + targzEntry.getName()); | ||
} | ||
if (targzEntry.isDirectory()) { | ||
Files.createDirectories(entry); | ||
} else { | ||
if (!Files.isDirectory(entry.getParent())) { | ||
Files.createDirectories(entry.getParent()); | ||
} | ||
Files.copy(tarArchiveInputStream, entry, StandardCopyOption.REPLACE_EXISTING); | ||
int mode = targzEntry.getMode(); | ||
if (mode != 0 && !OsUtils.isWindows()) { | ||
Set<PosixFilePermission> permissions = | ||
PosixFilePermissionSupport.toPosixFilePermissions(mode); | ||
Files.setPosixFilePermissions(entry, permissions); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
class PosixFilePermissionSupport { | ||
|
||
private static final int OWNER_READ_FILEMODE = 0b100_000_000; | ||
private static final int OWNER_WRITE_FILEMODE = 0b010_000_000; | ||
private static final int OWNER_EXEC_FILEMODE = 0b001_000_000; | ||
|
||
private static final int GROUP_READ_FILEMODE = 0b000_100_000; | ||
private static final int GROUP_WRITE_FILEMODE = 0b000_010_000; | ||
private static final int GROUP_EXEC_FILEMODE = 0b000_001_000; | ||
|
||
private static final int OTHERS_READ_FILEMODE = 0b000_000_100; | ||
private static final int OTHERS_WRITE_FILEMODE = 0b000_000_010; | ||
private static final int OTHERS_EXEC_FILEMODE = 0b000_000_001; | ||
|
||
private PosixFilePermissionSupport() {} | ||
|
||
static Set<PosixFilePermission> toPosixFilePermissions(int octalFileMode) { | ||
Set<PosixFilePermission> permissions = new LinkedHashSet<>(); | ||
// Owner | ||
if ((octalFileMode & OWNER_READ_FILEMODE) == OWNER_READ_FILEMODE) { | ||
permissions.add(PosixFilePermission.OWNER_READ); | ||
} | ||
if ((octalFileMode & OWNER_WRITE_FILEMODE) == OWNER_WRITE_FILEMODE) { | ||
permissions.add(PosixFilePermission.OWNER_WRITE); | ||
} | ||
if ((octalFileMode & OWNER_EXEC_FILEMODE) == OWNER_EXEC_FILEMODE) { | ||
permissions.add(PosixFilePermission.OWNER_EXECUTE); | ||
} | ||
// Group | ||
if ((octalFileMode & GROUP_READ_FILEMODE) == GROUP_READ_FILEMODE) { | ||
permissions.add(PosixFilePermission.GROUP_READ); | ||
} | ||
if ((octalFileMode & GROUP_WRITE_FILEMODE) == GROUP_WRITE_FILEMODE) { | ||
permissions.add(PosixFilePermission.GROUP_WRITE); | ||
} | ||
if ((octalFileMode & GROUP_EXEC_FILEMODE) == GROUP_EXEC_FILEMODE) { | ||
permissions.add(PosixFilePermission.GROUP_EXECUTE); | ||
} | ||
// Others | ||
if ((octalFileMode & OTHERS_READ_FILEMODE) == OTHERS_READ_FILEMODE) { | ||
permissions.add(PosixFilePermission.OTHERS_READ); | ||
} | ||
if ((octalFileMode & OTHERS_WRITE_FILEMODE) == OTHERS_WRITE_FILEMODE) { | ||
permissions.add(PosixFilePermission.OTHERS_WRITE); | ||
} | ||
if ((octalFileMode & OTHERS_EXEC_FILEMODE) == OTHERS_EXEC_FILEMODE) { | ||
permissions.add(PosixFilePermission.OTHERS_EXECUTE); | ||
} | ||
return permissions; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
132 changes: 0 additions & 132 deletions
132
src/main/java/dev/jbang/net/jdkproviders/BaseFoldersJdkProvider.java
This file was deleted.
Oops, something went wrong.
57 changes: 0 additions & 57 deletions
57
src/main/java/dev/jbang/net/jdkproviders/CurrentJdkProvider.java
This file was deleted.
Oops, something went wrong.
55 changes: 0 additions & 55 deletions
55
src/main/java/dev/jbang/net/jdkproviders/DefaultJdkProvider.java
This file was deleted.
Oops, something went wrong.
275 changes: 0 additions & 275 deletions
275
src/main/java/dev/jbang/net/jdkproviders/JBangJdkProvider.java
This file was deleted.
Oops, something went wrong.
55 changes: 0 additions & 55 deletions
55
src/main/java/dev/jbang/net/jdkproviders/JavaHomeJdkProvider.java
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.