Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,16 @@ versioner {

dependencies {
implementation 'org.apache.commons:commons-compress:1.28.0'
implementation 'org.apache.httpcomponents:httpclient:4.5.14'
implementation 'org.apache.httpcomponents:httpclient-cache:4.5.14'
implementation 'com.google.code.gson:gson:2.13.2'
implementation 'org.jspecify:jspecify:1.0.0'

implementation('org.apache.httpcomponents.client5:httpclient5:5.6') {
// Optional: Only needed if using DefaultRemoteAccessProvider
}
implementation('org.apache.httpcomponents.client5:httpclient5-cache:5.6') {
// Optional: Only needed if using DefaultRemoteAccessProvider
}

testImplementation platform('org.junit:junit-bom:6.0.1')
testImplementation 'org.junit.jupiter:junit-jupiter'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
Expand Down Expand Up @@ -135,6 +140,19 @@ publishing {
developerConnection = 'scm:git:https://github.com/jbangdev/jbang-devkitman'
url = 'http://github.com/jbangdev/jbang-devkitman'
}

// Mark Apache HttpClient dependencies as optional
withXml {
def dependenciesNode = asNode().dependencies[0]
dependenciesNode.dependency.each { dep ->
def groupId = dep.groupId[0].text()
def artifactId = dep.artifactId[0].text()
if (groupId == 'org.apache.httpcomponents.client5' &&
(artifactId == 'httpclient5' || artifactId == 'httpclient5-cache')) {
dep.appendNode('optional', true)
}
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package dev.jbang.devkitman.jdkinstallers;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.file.Path;
Expand All @@ -13,13 +11,10 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

import dev.jbang.devkitman.Jdk;
import dev.jbang.devkitman.JdkInstaller;
import dev.jbang.devkitman.JdkInstallers;
Expand All @@ -33,7 +28,7 @@
public class FoojayJdkInstaller implements JdkInstaller {
protected final JdkProvider jdkProvider;
protected final Function<JdkResult, String> jdkId;
protected RemoteAccessProvider remoteAccessProvider = RemoteAccessProvider.createDefaultRemoteAccessProvider();
protected RemoteAccessProvider remoteAccessProvider;
protected String distro = DEFAULT_DISTRO;

public static final String FOOJAY_JDK_VERSIONS_URL = "https://api.foojay.io/disco/v3.0/packages?";
Expand Down Expand Up @@ -70,6 +65,13 @@ public FoojayJdkInstaller(@NonNull JdkProvider jdkProvider, @NonNull Function<Jd
this.jdkId = jdkId;
}

protected @NonNull RemoteAccessProvider remoteAccessProvider() {
if (remoteAccessProvider == null) {
remoteAccessProvider = RemoteAccessProvider.createDefaultRemoteAccessProvider();
}
return remoteAccessProvider;
}

public @NonNull FoojayJdkInstaller remoteAccessProvider(@NonNull RemoteAccessProvider remoteAccessProvider) {
this.remoteAccessProvider = remoteAccessProvider;
return this;
Expand Down Expand Up @@ -175,14 +177,7 @@ private Stream<Jdk.AvailableJdk> processPackages(List<JdkResult> jdks, Comparato
}

private VersionsResponse readJsonFromUrl(String url) throws IOException {
return remoteAccessProvider.resultFromUrl(url, is -> {
try (InputStream ignored = is) {
Gson parser = new GsonBuilder().create();
return parser.fromJson(new InputStreamReader(is), VersionsResponse.class);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
return RemoteAccessProvider.readJsonFromUrl(remoteAccessProvider(), url, VersionsResponse.class);
}

// Filter out any EA releases for which a GA with
Expand Down Expand Up @@ -238,7 +233,7 @@ private Comparator<JdkResult> majorVersionSort() {

try {
LOGGER.log(Level.FINE, "Downloading {0}", url);
Path jdkPkg = remoteAccessProvider.downloadFromUrl(url);
Path jdkPkg = remoteAccessProvider().downloadFromUrl(url);

LOGGER.log(Level.INFO, "Installing JDK {0}...", version);
JavaUtils.installJdk(jdkPkg, jdkDir);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package dev.jbang.devkitman.jdkinstallers;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.nio.file.Path;
import java.util.*;
Expand All @@ -12,13 +10,10 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

import dev.jbang.devkitman.Jdk;
import dev.jbang.devkitman.JdkInstaller;
import dev.jbang.devkitman.JdkInstallers;
Expand All @@ -32,8 +27,7 @@
public class MetadataJdkInstaller implements JdkInstaller {
protected final @NonNull JdkProvider jdkProvider;
protected final Function<MetadataResult, String> jdkId;
protected @NonNull RemoteAccessProvider remoteAccessProvider = RemoteAccessProvider
.createDefaultRemoteAccessProvider();
protected RemoteAccessProvider remoteAccessProvider;
protected @NonNull String distro = DEFAULT_DISTRO;
protected String jvmImpl = DEFAULT_JVM_IMPL;

Expand Down Expand Up @@ -80,6 +74,13 @@ public MetadataJdkInstaller(@NonNull JdkProvider jdkProvider, @NonNull Function<
this.jdkId = jdkId;
}

protected @NonNull RemoteAccessProvider remoteAccessProvider() {
if (remoteAccessProvider == null) {
remoteAccessProvider = RemoteAccessProvider.createDefaultRemoteAccessProvider();
}
return remoteAccessProvider;
}

public @NonNull MetadataJdkInstaller remoteAccessProvider(@NonNull RemoteAccessProvider remoteAccessProvider) {
this.remoteAccessProvider = remoteAccessProvider;
return this;
Expand Down Expand Up @@ -256,15 +257,7 @@ private Stream<Jdk.AvailableJdk> processMetadata(List<MetadataResult> jdks, Comp
}

private List<MetadataResult> readJsonFromUrl(String url) throws IOException {
return remoteAccessProvider.resultFromUrl(url, is -> {
try (InputStream ignored = is) {
Gson parser = new GsonBuilder().create();
MetadataResult[] results = parser.fromJson(new InputStreamReader(is), MetadataResult[].class);
return Arrays.asList(results);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
return Arrays.asList(RemoteAccessProvider.readJsonFromUrl(remoteAccessProvider(), url, MetadataResult[].class));
}

// Filter out any EA releases for which a GA with the same major version exists
Expand Down Expand Up @@ -316,7 +309,7 @@ private Comparator<MetadataResult> majorVersionSort() {

try {
LOGGER.log(Level.FINE, "Downloading {0}", url);
Path jdkPkg = remoteAccessProvider.downloadFromUrl(url);
Path jdkPkg = remoteAccessProvider().downloadFromUrl(url);

LOGGER.log(Level.INFO, "Installing JDK {0}...", version);
JavaUtils.installJdk(jdkPkg, jdkDir);
Expand Down
97 changes: 64 additions & 33 deletions src/main/java/dev/jbang/devkitman/util/FileHttpCacheStorage.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,72 +3,103 @@
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import org.apache.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;
import org.apache.hc.client5.http.cache.HttpCacheCASOperation;
import org.apache.hc.client5.http.cache.HttpCacheEntry;
import org.apache.hc.client5.http.cache.HttpCacheStorage;
import org.apache.hc.client5.http.cache.ResourceIOException;

public class FileHttpCacheStorage implements HttpCacheStorage {

private final Path cacheDir;
private final DefaultHttpCacheEntrySerializer serializer;

public FileHttpCacheStorage(Path cacheDir) {
this.cacheDir = cacheDir;
this.serializer = new DefaultHttpCacheEntrySerializer();
}

@Override
public synchronized void putEntry(String key, HttpCacheEntry entry) throws ResourceIOException {
try {
Files.createDirectories(cacheDir);
} catch (IOException e) {
throw new RuntimeException("Failed to create cache directory", e);
}
Path filePath = cacheDir.resolve(encodeKey(key));
try (ObjectOutputStream oos = new ObjectOutputStream(
new BufferedOutputStream(Files.newOutputStream(filePath)))) {
oos.writeObject(entry);
} catch (IOException e) {
throw new ResourceIOException("Failed to write cache entry", e);
}
}

@Override
public synchronized void putEntry(String key, HttpCacheEntry entry) throws IOException {
public synchronized HttpCacheEntry getEntry(String key) throws ResourceIOException {
Path filePath = cacheDir.resolve(encodeKey(key));
try (OutputStream os = Files.newOutputStream(filePath);
BufferedOutputStream bos = new BufferedOutputStream(os)) {
serializer.writeTo(entry, bos);
if (!Files.exists(filePath)) {
return null;
}
try (ObjectInputStream ois = new ObjectInputStream(
new BufferedInputStream(Files.newInputStream(filePath)))) {
return (HttpCacheEntry) ois.readObject();
} catch (IOException | ClassNotFoundException e) {
throw new ResourceIOException("Failed to read cache entry", e);
}
}

@Override
public synchronized HttpCacheEntry getEntry(String key) throws IOException {
public synchronized void removeEntry(String key) {
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);
}
try {
Files.deleteIfExists(filePath);
} catch (IOException e) {
// Ignore errors on removal
}
return null;
}

@Override
public synchronized void removeEntry(String key) throws IOException {
Path filePath = cacheDir.resolve(encodeKey(key));
Files.deleteIfExists(filePath);
public synchronized void updateEntry(String key, HttpCacheCASOperation operation) throws ResourceIOException {
HttpCacheEntry existingEntry = getEntry(key);
HttpCacheEntry updatedEntry = operation.execute(existingEntry);
if (updatedEntry != null) {
putEntry(key, updatedEntry);
}
}

@Override
public synchronized 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);
public synchronized Map<String, HttpCacheEntry> getEntries(Collection<String> keys) throws ResourceIOException {
Map<String, HttpCacheEntry> result = new HashMap<>();
for (String key : keys) {
HttpCacheEntry entry = getEntry(key);
if (entry != null) {
result.put(key, entry);
}
}
HttpCacheEntry updatedEntry = callback.update(existingEntry);
putEntry(key, updatedEntry);
return result;
}

private String encodeKey(String key) {
// You can use more sophisticated encoding if necessary
return key.replaceAll("[^a-zA-Z0-9-_]", "_");
int p = key.indexOf("https://");
if (p == -1) {
p = key.indexOf("http://");
}
if (p != -1) {
String hap = key.substring(p);
p = hap.indexOf("?");
if (p != -1) {
hap = hap.substring(0, p);
}
String encoded = hap.replaceAll("[^a-zA-Z0-9-_]", "_");
if (encoded.length() > 100) {
encoded = encoded.substring(0, 100);
}
encoded = encoded + "_" + Integer.toHexString(key.hashCode()) + ".cache";
return encoded;
} else {
return Integer.toHexString(key.hashCode()) + ".cache";
}
}
}
15 changes: 15 additions & 0 deletions src/main/java/dev/jbang/devkitman/util/FunctionWithError.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package dev.jbang.devkitman.util;

import java.io.IOException;

@FunctionalInterface
public interface FunctionWithError<IN, OUT> {
OUT apply(IN in) throws IOException;

default <NEXT> FunctionWithError<IN, NEXT> andThen(FunctionWithError<OUT, NEXT> next) {
return in -> {
OUT intermediate = this.apply(in);
return next.apply(intermediate);
};
}
}
Loading