diff --git a/download-utils/build.gradle b/download-utils/build.gradle index 2590abd..7c7b03e 100644 --- a/download-utils/build.gradle +++ b/download-utils/build.gradle @@ -7,6 +7,7 @@ plugins { alias libs.plugins.gradleutils alias libs.plugins.gitversion alias libs.plugins.changelog + alias libs.plugins.multi.release } gradleutils.displayName = 'Download Utils' @@ -23,33 +24,26 @@ java { dependencies { compileOnly libs.nulls - - implementation projects.logUtils } -final java11 = configurations.detachedConfiguration( - dependencies.create(projects.downloadUtils.java11) { - transitive = false - } -) - tasks.named('jar', Jar) { - dependsOn java11.buildDependencies - manifest { attributes([ - 'Automatic-Module-Name': 'net.minecraftforge.utils.download', - 'Multi-Release' : 'true' + 'Automatic-Module-Name': 'net.minecraftforge.utils.download' ]) gradleutils.manifestDefaults(it, 'net/minecraftforge/util/download/') } - into('META-INF/versions/11') { - from(provider { zipTree(java11.singleFile) }) { - exclude 'META-INF/**' - } - } + archiveClassifier = 'java8' +} + +multiRelease.register { + add(JavaLanguageVersion.of(11), projects.downloadUtils.downloadUtilsJ11) +} + +tasks.named('multiReleaseJar', Jar) { + archiveClassifier = '' } license { @@ -68,7 +62,7 @@ publishing { } publications.register('mavenJava', MavenPublication).configure { - from components.java + from multiRelease.component changelog.publish(it) gradleutils.promote(it) diff --git a/download-utils/java11/build.gradle b/download-utils/download-utils-j11/build.gradle similarity index 90% rename from download-utils/java11/build.gradle rename to download-utils/download-utils-j11/build.gradle index 7419529..3208abc 100644 --- a/download-utils/java11/build.gradle +++ b/download-utils/download-utils-j11/build.gradle @@ -10,8 +10,6 @@ java.toolchain.languageVersion = JavaLanguageVersion.of(11) dependencies { compileOnly libs.nulls - - implementation projects.logUtils } license { diff --git a/download-utils/download-utils-j11/src/main/java/net/minecraftforge/util/download/DownloadUtilsImpl.java b/download-utils/download-utils-j11/src/main/java/net/minecraftforge/util/download/DownloadUtilsImpl.java new file mode 100644 index 0000000..af8a509 --- /dev/null +++ b/download-utils/download-utils-j11/src/main/java/net/minecraftforge/util/download/DownloadUtilsImpl.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) Forge Development LLC + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.util.download; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.ArrayList; + +final class DownloadUtilsImpl { + private DownloadUtilsImpl() { } + + private static final Duration TIMEOUT = Duration.ofSeconds(5); + private static final int MAX_REDIRECTS = 3; + + private static final HttpClient CLIENT = HttpClient + .newBuilder() + .connectTimeout(TIMEOUT) + .followRedirects(HttpClient.Redirect.NEVER) + .build(); + + private static final HttpRequest.Builder REQUEST_BUILDER = HttpRequest + .newBuilder() + .timeout(TIMEOUT) + .header("User-Agent", "MinecraftForge-Utils") + .header("Accept", "application/json"); + + static InputStream connect(String address) throws IOException, InterruptedException { + URI uri; + try { + uri = new URI(address); + } catch (URISyntaxException e) { + throw new IOException(e); + } + + // if not http, then try a URLConnection. we might be trying to make a file or jar connection. + // see jdk.internal.net.http.HttpRequestBuilderImpl#checkUri + var scheme = uri.getScheme(); + if (scheme == null || !(scheme.equalsIgnoreCase("https") || scheme.equalsIgnoreCase("http"))) { + return uri.toURL().openStream(); + } + + var redirections = new ArrayList(); + HttpResponse con; + for (int redirects = 0; ; redirects++) { + con = CLIENT.send( + REQUEST_BUILDER.uri(uri).build(), + info -> HttpResponse.BodySubscribers.ofInputStream() + ); + + int res = con.statusCode(); + if (res == HttpURLConnection.HTTP_MOVED_PERM || res == HttpURLConnection.HTTP_MOVED_TEMP) { + var header = con.headers().firstValue("Location"); + if (header.isEmpty()) + throw new IOException(String.format( + "No location header found in redirect response: %s -- previous redirections: [%s]", + uri, String.join(", ", redirections) + )); + + var location = header.get(); + redirections.add(location); + + if (redirects == MAX_REDIRECTS - 1) { + throw new IOException(String.format( + "Too many redirects: %s -- redirections: [%s]", + address, String.join(", ", redirections) + )); + } else { + uri = uri.resolve(location); + } + } else if (res == HttpURLConnection.HTTP_NOT_FOUND) { + throw new FileNotFoundException("Returned 404: " + address); + } else { + break; + } + } + + return con.body(); + } + + static byte[] readInputStream(InputStream stream) throws IOException { + return stream.readAllBytes(); + } +} diff --git a/download-utils/java11/src/main/java/net/minecraftforge/util/download/DownloadUtils.java b/download-utils/java11/src/main/java/net/minecraftforge/util/download/DownloadUtils.java deleted file mode 100644 index cefc52f..0000000 --- a/download-utils/java11/src/main/java/net/minecraftforge/util/download/DownloadUtils.java +++ /dev/null @@ -1,219 +0,0 @@ -/* - * Copyright (c) Forge Development LLC - * SPDX-License-Identifier: LGPL-2.1-only - */ -package net.minecraftforge.util.download; - -import net.minecraftforge.util.logging.Log; -import org.jetbrains.annotations.Nullable; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URLConnection; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardCopyOption; -import java.time.Duration; -import java.util.ArrayList; -import java.util.Locale; -import java.util.function.Supplier; - -public final class DownloadUtils { - private static final Duration TIMEOUT = Duration.ofSeconds(5); - private static final int MAX_REDIRECTS = 3; - - private static final HttpClient CLIENT = HttpClient - .newBuilder() - .connectTimeout(TIMEOUT) - .followRedirects(HttpClient.Redirect.NEVER) - .build(); - - private static final HttpRequest.Builder REQUEST_BUILDER = HttpRequest - .newBuilder() - .timeout(TIMEOUT) - .header("User-Agent", "MinecraftForge-Utils") - .header("Accept", "application/json"); - - private static InputStream connect(String address) throws IOException, InterruptedException { - URI uri; - try { - uri = new URI(address); - } catch (URISyntaxException e) { - throw new IOException(e); - } - - // if not http, then try a URLConnection. we might be trying to make a file or jar connection. - // see jdk.internal.net.http.HttpRequestBuilderImpl#checkUri - var scheme = uri.getScheme(); - if (scheme == null || !(scheme.equalsIgnoreCase("https") || scheme.equalsIgnoreCase("http"))) { - return uri.toURL().openStream(); - } - - var redirections = new ArrayList(); - HttpResponse con; - for (int redirects = 0; ; redirects++) { - con = CLIENT.send( - REQUEST_BUILDER.uri(uri).build(), - info -> HttpResponse.BodySubscribers.ofInputStream() - ); - - int res = con.statusCode(); - if (res == HttpURLConnection.HTTP_MOVED_PERM || res == HttpURLConnection.HTTP_MOVED_TEMP) { - var header = con.headers().firstValue("Location"); - if (header.isEmpty()) - throw new IOException(String.format( - "No location header found in redirect response: %s -- previous redirections: [%s]", - uri, String.join(", ", redirections) - )); - - var location = header.get(); - redirections.add(location); - - if (redirects == MAX_REDIRECTS - 1) { - throw new IOException(String.format( - "Too many redirects: %s -- redirections: [%s]", - address, String.join(", ", redirections) - )); - } else { - Log.debug("Following redirect: " + location); - uri = uri.resolve(location); - } - } else if (res == HttpURLConnection.HTTP_NOT_FOUND) { - throw new FileNotFoundException("Returned 404: " + address); - } else { - break; - } - } - - return con.body(); - } - - /** - * Downloads raw bytes from the given URL. - * - * @param url The URL to download from - * @return The downloaded bytes, stored in memory - * @throws IOException If the download failed - */ - public static byte[] downloadBytes(String url) throws IOException { - try (InputStream stream = connect(url)) { - return stream.readAllBytes(); - } catch (Exception e) { - throw new DownloadFailedException(url, e); - } - } - - /** - * Downloads raw bytes from the given URL. - *

Returns {@code null} on failure.

- * - * @param url The URL to download from - * @return The downloaded bytes, stored in memory, or {@code null} if the download failed - */ - public static byte @Nullable [] tryDownloadBytes(boolean silent, String url) { - try { - return downloadBytes(url); - } catch (IOException e) { - if (!silent) { - e.printStackTrace(Log.WARN); - } - return null; - } - } - - /** - * Downloads a string from the given URL, effectively acting as {@code curl}. - * - * @param url The URL to download from - * @return The downloaded string - * @throws IOException If the download failed - */ - public static String downloadString(String url) throws IOException { - return new String(downloadBytes(url), StandardCharsets.UTF_8); - } - - /** - * Downloads a string from the given URL, effectively acting as {@code curl}. - *

Returns {@code null} on failure.

- * - * @param url The URL to download from - * @return The downloaded string, or {@code null} if the download failed - */ - public static @Nullable String tryDownloadString(boolean silent, String url) { - try { - return downloadString(url); - } catch (IOException e) { - if (!silent) { - e.printStackTrace(Log.WARN); - } - - return null; - } - } - - /** - * Downloads a file from the given URL into the target file, effectively acting as {@code wget}. - * - * @param target The file to download to - * @param url The URL to download from - * @throws IOException If the download failed - */ - public static void downloadFile(File target, String url) throws IOException { - downloadFile(false, target, url); - } - - /** - * Downloads a file from the given URL into the target file, effectively acting as {@code wget}. - * - * @param silent If no log messages should be sent - * @param target The file to download to - * @param url The URL to download from - * @throws IOException If the download failed - */ - public static void downloadFile(boolean silent, File target, String url) throws IOException { - if (!silent) Log.quiet("Downloading " + url); - - try (InputStream stream = connect(url)) { - Path path = target.toPath(); - Files.createDirectories(path.getParent()); - Files.copy(stream, path, StandardCopyOption.REPLACE_EXISTING); - } catch (Exception e) { - throw new DownloadFailedException(url, e); - } - } - - /** - * Attempts to download a file from the given URL into the target file, effectively acting as {@code wget}. - * - * @param target The file to download to - * @param url The URL to download from - * @return {@code true} if the download was successful - */ - public static boolean tryDownloadFile(boolean silent, File target, String url) { - try { - downloadFile(silent, target, url); - return true; - } catch (IOException e) { - if (!silent) { - e.printStackTrace(Log.WARN); - } - - return false; - } - } - - private static final class DownloadFailedException extends IOException { - public DownloadFailedException(String url, Throwable cause) { - super("Failed to download " + url, cause); - } - } -} diff --git a/download-utils/src/main/java/net/minecraftforge/util/download/DownloadUtils.java b/download-utils/src/main/java/net/minecraftforge/util/download/DownloadUtils.java index 0bbe0ce..f644a36 100644 --- a/download-utils/src/main/java/net/minecraftforge/util/download/DownloadUtils.java +++ b/download-utils/src/main/java/net/minecraftforge/util/download/DownloadUtils.java @@ -4,93 +4,21 @@ */ package net.minecraftforge.util.download; -import net.minecraftforge.util.logging.Log; import org.jetbrains.annotations.Nullable; -import java.io.ByteArrayOutputStream; import java.io.File; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.net.URLConnection; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; -import java.util.ArrayList; -import java.util.List; +/** + * Static utilities for downloading files. + */ public final class DownloadUtils { - private static final int TIMEOUT = 5 * 1000; - private static final int MAX_REDIRECTS = 3; - - private static URLConnection openConnection(URL url) throws IOException { - URLConnection con = url.openConnection(); - con.setConnectTimeout(TIMEOUT); - con.setReadTimeout(TIMEOUT); - - if (con instanceof HttpURLConnection) { - HttpURLConnection hcon = (HttpURLConnection) con; - hcon.setRequestProperty("User-Agent", "MinecraftForge-Utils"); - hcon.setRequestProperty("Accept", "application/json"); - hcon.setInstanceFollowRedirects(false); - } - - return con; - } - - private static InputStream connect(String address) throws IOException { - URI uri; - try { - uri = new URI(address); - } catch (URISyntaxException e) { - throw new IOException(e); - } - - URL url; - List redirections = new ArrayList<>(); - URLConnection con; - for (int redirects = 0; ; redirects++) { - url = uri.toURL(); - con = openConnection(url); - - if (!(con instanceof HttpURLConnection)) break; - HttpURLConnection hcon = (HttpURLConnection) con; - - int res = hcon.getResponseCode(); - if (res == HttpURLConnection.HTTP_MOVED_PERM || res == HttpURLConnection.HTTP_MOVED_TEMP) { - String location = hcon.getHeaderField("Location"); - redirections.add(location); - hcon.disconnect(); - - if (redirects == MAX_REDIRECTS - 1) { - throw new IOException(String.format( - "Too many redirects: %s -- redirections: [%s]", - address, String.join(", ", redirections) - )); - } else { - Log.debug("Following redirect: " + location); - uri = uri.resolve(location); - } - } else if (res == 404) { - throw new FileNotFoundException("Returned 404: " + address); - } else { - break; - } - } - - final URLConnection connection = con; - return connection.getInputStream(); - } - - @SuppressWarnings("unchecked") - private static R sneak(Throwable t) throws E { - throw (E) t; - } + private DownloadUtils() { } /** * Downloads raw bytes from the given URL. @@ -100,18 +28,10 @@ private static R sneak(Throwable t) throws E { * @throws IOException If the download failed */ public static byte[] downloadBytes(String url) throws IOException { - try (InputStream stream = connect(url); - ByteArrayOutputStream out = new ByteArrayOutputStream() - ) { - // NOTE: main source uses InputStream#readAllBytes - byte[] buf = new byte[1024]; - int n; - while ((n = stream.read(buf)) > 0) { - out.write(buf, 0, n); - } - - return out.toByteArray(); + try (InputStream stream = DownloadUtilsImpl.connect(url)) { + return DownloadUtilsImpl.readInputStream(stream); } catch (Exception e) { + // We catch "Exception" here since it might not be an IOException. throw new DownloadFailedException(url, e); } } @@ -122,14 +42,13 @@ public static byte[] downloadBytes(String url) throws IOException { * * @param url The URL to download from * @return The downloaded bytes, stored in memory, or {@code null} if the download failed + * @apiNote This method will swallow any exceptions thrown by {@link #downloadBytes(String)}. For proper debugging, + * use that method directly and handle any exceptions yourself. */ - public static byte @Nullable [] tryDownloadBytes(boolean silent, String url) { + public static byte @Nullable [] tryDownloadBytes(String url) { try { return downloadBytes(url); } catch (IOException e) { - if (!silent) { - e.printStackTrace(Log.WARN); - } return null; } } @@ -151,15 +70,13 @@ public static String downloadString(String url) throws IOException { * * @param url The URL to download from * @return The downloaded string, or {@code null} if the download failed + * @apiNote This method will swallow any exceptions thrown by {@link #downloadString(String)}. For proper debugging, + * use that method directly and handle any exceptions yourself. */ - public static @Nullable String tryDownloadString(boolean silent, String url) { + public static @Nullable String tryDownloadString(String url) { try { return downloadString(url); } catch (IOException e) { - if (!silent) { - e.printStackTrace(Log.WARN); - } - return null; } } @@ -172,25 +89,12 @@ public static String downloadString(String url) throws IOException { * @throws IOException If the download failed */ public static void downloadFile(File target, String url) throws IOException { - downloadFile(false, target, url); - } - - /** - * Downloads a file from the given URL into the target file, effectively acting as {@code wget}. - * - * @param silent If no log messages should be sent - * @param target The file to download to - * @param url The URL to download from - * @throws IOException If the download failed - */ - public static void downloadFile(boolean silent, File target, String url) throws IOException { - if (!silent) Log.quiet("Downloading " + url); - - try (InputStream stream = connect(url)) { + try (InputStream stream = DownloadUtilsImpl.connect(url)) { Path path = target.toPath(); Files.createDirectories(path.getParent()); Files.copy(stream, path, StandardCopyOption.REPLACE_EXISTING); - } catch (DownloadFailedException e) { + } catch (Exception e) { + // We catch "Exception" here since it might not be an IOException. throw new IOException(url, e); } } @@ -201,16 +105,14 @@ public static void downloadFile(boolean silent, File target, String url) throws * @param target The file to download to * @param url The URL to download from * @return {@code true} if the download was successful + * @apiNote This method will swallow any exceptions thrown by {@link #downloadFile(File, String)}. For proper + * debugging, use that method directly and handle any exceptions yourself. */ - public static boolean tryDownloadFile(boolean silent, File target, String url) { + public static boolean tryDownloadFile(File target, String url) { try { - downloadFile(silent, target, url); + downloadFile(target, url); return true; } catch (IOException e) { - if (!silent) { - e.printStackTrace(Log.WARN); - } - return false; } } diff --git a/download-utils/src/main/java/net/minecraftforge/util/download/DownloadUtilsImpl.java b/download-utils/src/main/java/net/minecraftforge/util/download/DownloadUtilsImpl.java new file mode 100644 index 0000000..99dc248 --- /dev/null +++ b/download-utils/src/main/java/net/minecraftforge/util/download/DownloadUtilsImpl.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) Forge Development LLC + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.util.download; + +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.List; + +final class DownloadUtilsImpl { + private DownloadUtilsImpl() { } + + private static final int TIMEOUT = 5 * 1000; + private static final int MAX_REDIRECTS = 3; + + private static URLConnection openConnection(URL url) throws IOException { + URLConnection con = url.openConnection(); + con.setConnectTimeout(TIMEOUT); + con.setReadTimeout(TIMEOUT); + + if (con instanceof HttpURLConnection) { + HttpURLConnection hcon = (HttpURLConnection) con; + hcon.setRequestProperty("User-Agent", "MinecraftForge-Utils"); + hcon.setRequestProperty("Accept", "application/json"); + hcon.setInstanceFollowRedirects(false); + } + + return con; + } + + static InputStream connect(String address) throws IOException { + URI uri; + try { + uri = new URI(address); + } catch (URISyntaxException e) { + throw new IOException(e); + } + + URL url; + List redirections = new ArrayList<>(); + URLConnection con; + for (int redirects = 0; ; redirects++) { + url = uri.toURL(); + con = openConnection(url); + + if (!(con instanceof HttpURLConnection)) break; + HttpURLConnection hcon = (HttpURLConnection) con; + + int res = hcon.getResponseCode(); + if (res == HttpURLConnection.HTTP_MOVED_PERM || res == HttpURLConnection.HTTP_MOVED_TEMP) { + String location = hcon.getHeaderField("Location"); + redirections.add(location); + hcon.disconnect(); + + if (redirects == MAX_REDIRECTS - 1) { + throw new IOException(String.format( + "Too many redirects: %s -- redirections: [%s]", + address, String.join(", ", redirections) + )); + } else { + uri = uri.resolve(location); + } + } else if (res == 404) { + throw new FileNotFoundException("Returned 404: " + address); + } else { + break; + } + } + + final URLConnection connection = con; + return connection.getInputStream(); + } + + static byte[] readInputStream(InputStream stream) throws IOException { + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + byte[] buf = new byte[1024]; + int n; + while ((n = stream.read(buf)) > 0) { + out.write(buf, 0, n); + } + + return out.toByteArray(); + } + } +} diff --git a/settings.gradle b/settings.gradle index 5851a5d..b938374 100644 --- a/settings.gradle +++ b/settings.gradle @@ -11,7 +11,7 @@ include 'json-data-utils' include 'file-utils' include 'log-utils' include 'os-utils' -include 'download-utils', 'download-utils:java11' +include 'download-utils', 'download-utils:download-utils-j11' // Applying plugins causes them to not have any IDE support when also applied to any build.gradle files // The workaround for now is to use this listener here so that it can stay in settings.gradle @@ -30,13 +30,14 @@ gradle.beforeProject { Project project -> //@formatter:off dependencyResolutionManagement.versionCatalogs.register('libs') { // Plugins - plugin 'licenser', 'net.minecraftforge.licenser' version '1.2.0' - plugin 'gradleutils', 'net.minecraftforge.gradleutils' version '3.3.18' - plugin 'gitversion', 'net.minecraftforge.gitversion' version '3.1.1' - plugin 'changelog', 'net.minecraftforge.changelog' version '3.1.2' + plugin 'licenser', 'net.minecraftforge.licenser' version '1.2.0' + plugin 'gradleutils', 'net.minecraftforge.gradleutils' version '3.3.21' + plugin 'gitversion', 'net.minecraftforge.gitversion' version '3.1.6' + plugin 'changelog', 'net.minecraftforge.changelog' version '3.1.3' + plugin 'multi-release', 'net.minecraftforge.multi-release' version '0.1.4' // Static Analysis - library 'nulls', 'org.jetbrains', 'annotations' version '26.0.2' + library 'nulls', 'org.jetbrains', 'annotations' version '26.0.2-1' // Files library 'commons-io', 'commons-io', 'commons-io' version '2.18.0'