diff --git a/build.gradle b/build.gradle index 72bb7e5..d0a2558 100644 --- a/build.gradle +++ b/build.gradle @@ -55,11 +55,16 @@ versioner { dependencies { implementation 'org.apache.commons:commons-compress:1.28.0' - implementation 'org.apache.httpcomponents: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' @@ -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) + } + } + } } } } diff --git a/src/main/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstaller.java b/src/main/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstaller.java index 0700593..58f6e4b 100644 --- a/src/main/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstaller.java +++ b/src/main/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstaller.java @@ -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; @@ -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; @@ -33,7 +28,7 @@ public class FoojayJdkInstaller implements JdkInstaller { protected final JdkProvider jdkProvider; protected final Function 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?"; @@ -70,6 +65,13 @@ public FoojayJdkInstaller(@NonNull JdkProvider jdkProvider, @NonNull Function processPackages(List 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 @@ -238,7 +233,7 @@ private Comparator 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); diff --git a/src/main/java/dev/jbang/devkitman/jdkinstallers/MetadataJdkInstaller.java b/src/main/java/dev/jbang/devkitman/jdkinstallers/MetadataJdkInstaller.java index 69ef656..47a9b00 100644 --- a/src/main/java/dev/jbang/devkitman/jdkinstallers/MetadataJdkInstaller.java +++ b/src/main/java/dev/jbang/devkitman/jdkinstallers/MetadataJdkInstaller.java @@ -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.*; @@ -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; @@ -32,8 +27,7 @@ public class MetadataJdkInstaller implements JdkInstaller { protected final @NonNull JdkProvider jdkProvider; protected final Function jdkId; - protected @NonNull RemoteAccessProvider remoteAccessProvider = RemoteAccessProvider - .createDefaultRemoteAccessProvider(); + protected RemoteAccessProvider remoteAccessProvider; protected @NonNull String distro = DEFAULT_DISTRO; protected String jvmImpl = DEFAULT_JVM_IMPL; @@ -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; @@ -256,15 +257,7 @@ private Stream processMetadata(List jdks, Comp } private List readJsonFromUrl(String url) throws IOException { - return remoteAccessProvider.resultFromUrl(url, is -> { - try (InputStream ignored = is) { - Gson parser = new GsonBuilder().create(); - MetadataResult[] results = parser.fromJson(new InputStreamReader(is), MetadataResult[].class); - return Arrays.asList(results); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); + return Arrays.asList(RemoteAccessProvider.readJsonFromUrl(remoteAccessProvider(), url, MetadataResult[].class)); } // Filter out any EA releases for which a GA with the same major version exists @@ -316,7 +309,7 @@ private Comparator 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); diff --git a/src/main/java/dev/jbang/devkitman/util/FileHttpCacheStorage.java b/src/main/java/dev/jbang/devkitman/util/FileHttpCacheStorage.java index 2670a10..17b8824 100644 --- a/src/main/java/dev/jbang/devkitman/util/FileHttpCacheStorage.java +++ b/src/main/java/dev/jbang/devkitman/util/FileHttpCacheStorage.java @@ -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 getEntries(Collection keys) throws ResourceIOException { + Map 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"; + } } } diff --git a/src/main/java/dev/jbang/devkitman/util/FunctionWithError.java b/src/main/java/dev/jbang/devkitman/util/FunctionWithError.java new file mode 100644 index 0000000..b65ca90 --- /dev/null +++ b/src/main/java/dev/jbang/devkitman/util/FunctionWithError.java @@ -0,0 +1,15 @@ +package dev.jbang.devkitman.util; + +import java.io.IOException; + +@FunctionalInterface +public interface FunctionWithError { + OUT apply(IN in) throws IOException; + + default FunctionWithError andThen(FunctionWithError next) { + return in -> { + OUT intermediate = this.apply(in); + return next.apply(intermediate); + }; + } +} diff --git a/src/main/java/dev/jbang/devkitman/util/NetUtils.java b/src/main/java/dev/jbang/devkitman/util/NetUtils.java index 7c555f5..e675629 100644 --- a/src/main/java/dev/jbang/devkitman/util/NetUtils.java +++ b/src/main/java/dev/jbang/devkitman/util/NetUtils.java @@ -5,26 +5,26 @@ import java.io.UncheckedIOException; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.function.Function; - -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.client.config.RequestConfig; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.entity.ContentType; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; -import org.apache.http.impl.client.cache.CacheConfig; -import org.apache.http.impl.client.cache.CachingHttpClientBuilder; +import java.util.Arrays; + +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.cache.CacheConfig; +import org.apache.hc.client5.http.impl.cache.CachingHttpClients; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.apache.hc.core5.http.ClassicHttpResponse; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.util.Timeout; import org.jspecify.annotations.NonNull; public class NetUtils { public static final RequestConfig DEFAULT_REQUEST_CONFIG = RequestConfig.custom() - .setConnectionRequestTimeout(10000) - .setConnectTimeout(10000) - .setSocketTimeout(30000) + .setConnectionRequestTimeout(Timeout.ofMilliseconds(10000)) + .setConnectTimeout(Timeout.ofMilliseconds(10000)) + .setResponseTimeout(Timeout.ofMilliseconds(30000)) .build(); public static Path downloadFromUrl(String url) throws IOException { @@ -36,21 +36,21 @@ public static Path downloadFromUrl(HttpClientBuilder builder, String url) throws return requestUrl(builder, url, NetUtils::handleDownloadResult); } - public static T resultFromUrl(String url, Function streamToObject) + public static T resultFromUrl(String url, FunctionWithError streamToObject) throws IOException { HttpClientBuilder builder = createDefaultHttpClientBuilder(); return resultFromUrl(builder, url, streamToObject); } public static T resultFromUrl( - HttpClientBuilder builder, String url, Function streamToObject) + HttpClientBuilder builder, String url, FunctionWithError streamToObject) throws IOException { return requestUrl( builder, url, - mimetypeChecker("application/json") + mimetypeChecker("application/json", "text/plain") .andThen(NetUtils::responseStreamer) - .andThen(streamToObject)); + .andThen(is -> streamToObject.apply(is))); } public static HttpClientBuilder createDefaultHttpClientBuilder() { @@ -58,23 +58,35 @@ public static HttpClientBuilder createDefaultHttpClientBuilder() { } public static HttpClientBuilder createCachingHttpClientBuilder(@NonNull Path cacheDir) { - CacheConfig cacheConfig = CacheConfig.custom().setMaxCacheEntries(1000).build(); + CacheConfig cacheConfig = CacheConfig.custom() + .setMaxCacheEntries(1000) + .setSharedCache(false) + .build(); FileHttpCacheStorage cacheStorage = new FileHttpCacheStorage(cacheDir); - return CachingHttpClientBuilder.create() + return CachingHttpClients.custom() .setCacheConfig(cacheConfig) .setHttpCacheStorage(cacheStorage) + .addResponseInterceptorFirst((response, entity, context) -> { + // Force cache headers on all 200 OK responses to make them cacheable + if (response.getCode() == 200) { + response.setHeader("Cache-Control", "max-age=3600, public"); + if (!response.containsHeader("Date")) { + response.setHeader("Date", java.time.Instant.now().toString()); + } + } + }) .setDefaultRequestConfig(DEFAULT_REQUEST_CONFIG); } public static T requestUrl( - HttpClientBuilder builder, String url, Function responseHandler) + HttpClientBuilder builder, String url, FunctionWithError responseHandler) throws IOException { try (CloseableHttpClient httpClient = builder.build()) { HttpGet httpGet = new HttpGet(url); - try (CloseableHttpResponse response = httpClient.execute(httpGet)) { - int responseCode = response.getStatusLine().getStatusCode(); + return httpClient.execute(httpGet, response -> { + int responseCode = response.getCode(); if (responseCode != 200) { throw new IOException( "Failed to read from URL: " @@ -87,23 +99,27 @@ public static T requestUrl( throw new IOException("Failed to read from URL: " + url + ", no content"); } return responseHandler.apply(response); - } + }); } catch (UncheckedIOException e) { throw new IOException("Failed to read from URL: " + url + ", " + e.getMessage(), e); } } - private static Function mimetypeChecker(String expectedMimeType) { + private static FunctionWithError mimetypeChecker( + String... expectedMimeTypes) { return response -> { - String mimeType = ContentType.getOrDefault(response.getEntity()).getMimeType(); - if (expectedMimeType != null && !mimeType.equals(expectedMimeType)) { + ContentType contentType = ContentType.parse(response.getEntity().getContentType()); + String mimeType = contentType != null ? contentType.getMimeType() : "application/octet-stream"; + if (expectedMimeTypes != null && + expectedMimeTypes.length != 0 && + !Arrays.asList(expectedMimeTypes).contains(mimeType)) { throw new RuntimeException("Unexpected MIME type: " + mimeType); } return response; }; } - private static InputStream responseStreamer(HttpResponse response) { + private static InputStream responseStreamer(ClassicHttpResponse response) { try { HttpEntity entity = response.getEntity(); return entity.getContent(); @@ -112,7 +128,7 @@ private static InputStream responseStreamer(HttpResponse response) { } } - private static Path handleDownloadResult(HttpResponse response) { + private static Path handleDownloadResult(ClassicHttpResponse response) { try { HttpEntity entity = response.getEntity(); try (InputStream is = entity.getContent()) { diff --git a/src/main/java/dev/jbang/devkitman/util/RemoteAccessProvider.java b/src/main/java/dev/jbang/devkitman/util/RemoteAccessProvider.java index 811efa7..a2c55ad 100644 --- a/src/main/java/dev/jbang/devkitman/util/RemoteAccessProvider.java +++ b/src/main/java/dev/jbang/devkitman/util/RemoteAccessProvider.java @@ -2,17 +2,20 @@ import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.nio.file.Files; import java.nio.file.Path; -import java.util.function.Function; -import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; public interface RemoteAccessProvider { Path downloadFromUrl(String url) throws IOException; - default T resultFromUrl(String url, Function streamToObject) + default T resultFromUrl(String url, FunctionWithError streamToObject) throws IOException { Path file = downloadFromUrl(url); try (InputStream is = Files.newInputStream(file)) { @@ -20,10 +23,25 @@ default T resultFromUrl(String url, Function streamToObject) } } + static T readJsonFromUrl(RemoteAccessProvider rap, String url, Class klass) throws IOException { + return rap.resultFromUrl(url, is -> { + try (InputStream ignored = is) { + Gson parser = new GsonBuilder().create(); + return parser.fromJson(new InputStreamReader(is), klass); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + static RemoteAccessProvider createDefaultRemoteAccessProvider() { return new DefaultRemoteAccessProvider(); } + static RemoteAccessProvider createDefaultRemoteAccessProvider(Path cacheDir) { + return new DefaultRemoteAccessProvider(cacheDir); + } + static RemoteAccessProvider createDefaultRemoteAccessProvider(HttpClientBuilder clientBuilder) { if (clientBuilder != null) { return new DefaultRemoteAccessProvider(clientBuilder); @@ -39,6 +57,10 @@ public DefaultRemoteAccessProvider() { this.clientBuilder = NetUtils.createDefaultHttpClientBuilder(); } + public DefaultRemoteAccessProvider(Path cacheDir) { + this.clientBuilder = NetUtils.createCachingHttpClientBuilder(cacheDir); + } + public DefaultRemoteAccessProvider(HttpClientBuilder clientBuilder) { this.clientBuilder = clientBuilder; } @@ -49,7 +71,7 @@ public Path downloadFromUrl(String url) throws IOException { } @Override - public T resultFromUrl(String url, Function streamToObject) + public T resultFromUrl(String url, FunctionWithError streamToObject) throws IOException { return NetUtils.resultFromUrl(clientBuilder, url, streamToObject); } diff --git a/src/test/java/dev/jbang/devkitman/BaseTest.java b/src/test/java/dev/jbang/devkitman/BaseTest.java index 6f799cb..2e9b657 100644 --- a/src/test/java/dev/jbang/devkitman/BaseTest.java +++ b/src/test/java/dev/jbang/devkitman/BaseTest.java @@ -21,6 +21,7 @@ import dev.jbang.devkitman.jdkproviders.JBangJdkProvider; import dev.jbang.devkitman.jdkproviders.MockJdkProvider; import dev.jbang.devkitman.util.FileUtils; +import dev.jbang.devkitman.util.FunctionWithError; import dev.jbang.devkitman.util.JavaUtils; import dev.jbang.devkitman.util.RemoteAccessProvider; @@ -220,7 +221,7 @@ public Path downloadFromUrl(String url) throws IOException { @Override public T resultFromUrl( - String url, Function streamToObject) + String url, FunctionWithError streamToObject) throws IOException { if (url.startsWith(FoojayJdkInstaller.FOOJAY_JDK_VERSIONS_URL)) { return streamToObject.apply( diff --git a/src/test/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstallerTest.java b/src/test/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstallerTest.java index fac8bdd..3c2c6bc 100644 --- a/src/test/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstallerTest.java +++ b/src/test/java/dev/jbang/devkitman/jdkinstallers/FoojayJdkInstallerTest.java @@ -10,7 +10,6 @@ import java.nio.file.Path; import java.util.List; import java.util.Set; -import java.util.function.Function; import java.util.stream.Collectors; import org.junit.jupiter.api.BeforeEach; @@ -21,6 +20,7 @@ import dev.jbang.devkitman.Jdk; import dev.jbang.devkitman.JdkManager; import dev.jbang.devkitman.jdkproviders.JBangJdkProvider; +import dev.jbang.devkitman.util.FunctionWithError; import dev.jbang.devkitman.util.RemoteAccessProvider; public class FoojayJdkInstallerTest extends BaseTest { @@ -66,7 +66,7 @@ public Path downloadFromUrl(String url) throws IOException { } @Override - public T resultFromUrl(String url, Function streamToObject) + public T resultFromUrl(String url, FunctionWithError streamToObject) throws IOException { // Verify the URL format matches expected Foojay API pattern if (!url.startsWith(FoojayJdkInstaller.FOOJAY_JDK_VERSIONS_URL)) { diff --git a/src/test/java/dev/jbang/devkitman/jdkinstallers/MetadataJdkInstallerTest.java b/src/test/java/dev/jbang/devkitman/jdkinstallers/MetadataJdkInstallerTest.java index aae7d28..9c8e440 100644 --- a/src/test/java/dev/jbang/devkitman/jdkinstallers/MetadataJdkInstallerTest.java +++ b/src/test/java/dev/jbang/devkitman/jdkinstallers/MetadataJdkInstallerTest.java @@ -10,7 +10,6 @@ import java.nio.file.Path; import java.util.List; import java.util.Set; -import java.util.function.Function; import java.util.stream.Collectors; import org.junit.jupiter.api.BeforeEach; @@ -21,6 +20,7 @@ import dev.jbang.devkitman.Jdk; import dev.jbang.devkitman.JdkManager; import dev.jbang.devkitman.jdkproviders.JBangJdkProvider; +import dev.jbang.devkitman.util.FunctionWithError; import dev.jbang.devkitman.util.RemoteAccessProvider; public class MetadataJdkInstallerTest extends BaseTest { @@ -64,7 +64,7 @@ public Path downloadFromUrl(String url) throws IOException { } @Override - public T resultFromUrl(String url, Function streamToObject) + public T resultFromUrl(String url, FunctionWithError streamToObject) throws IOException { // Verify the URL format matches expected metadata API pattern if (!url.startsWith(MetadataJdkInstaller.METADATA_BASE_URL)) { diff --git a/src/test/java/dev/jbang/devkitman/util/TestRemoteAccessProvider.java b/src/test/java/dev/jbang/devkitman/util/TestRemoteAccessProvider.java new file mode 100644 index 0000000..1d39b28 --- /dev/null +++ b/src/test/java/dev/jbang/devkitman/util/TestRemoteAccessProvider.java @@ -0,0 +1,35 @@ +package dev.jbang.devkitman.util; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import dev.jbang.devkitman.BaseTest; + +public class TestRemoteAccessProvider extends BaseTest { + + @Test + void testDefaultReadJsonFromUrl(@TempDir Path cacheDir) throws IOException { + RemoteAccessProvider rap = RemoteAccessProvider.createDefaultRemoteAccessProvider(cacheDir); + String url = "https://raw.githubusercontent.com/jbangdev/jbang-devkitman/refs/heads/main/renovate.json"; + Object json = RemoteAccessProvider.readJsonFromUrl(rap, url, Object.class); + assertThat(json, instanceOf(Map.class)); + } + + @Test + void testDefaultCache(@TempDir Path cacheDir) throws IOException { + RemoteAccessProvider rap = RemoteAccessProvider.createDefaultRemoteAccessProvider(cacheDir); + String url = "https://raw.githubusercontent.com/jbangdev/jbang-devkitman/refs/heads/main/renovate.json"; + Object json = RemoteAccessProvider.readJsonFromUrl(rap, url, Object.class); + assertThat(json, instanceOf(Map.class)); + assertThat(Files.exists(cacheDir), is(true)); + assertThat(Files.list(cacheDir).count(), greaterThan(0L)); + } +}