将代码中的 URL 替换为 URI (#4131)

This commit is contained in:
Glavo
2025-07-30 18:19:46 +08:00
committed by GitHub
parent 7918b333e6
commit 3adb3a67e9
58 changed files with 577 additions and 462 deletions

View File

@@ -53,7 +53,7 @@ import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.net.SocketTimeoutException; import java.net.SocketTimeoutException;
import java.net.URL; import java.net.URI;
import java.nio.file.AccessDeniedException; import java.nio.file.AccessDeniedException;
import java.util.*; import java.util.*;
import java.util.concurrent.*; import java.util.concurrent.*;
@@ -267,27 +267,27 @@ public final class LauncherHelper {
if (ex.getCause() instanceof ResponseCodeException) { if (ex.getCause() instanceof ResponseCodeException) {
ResponseCodeException rce = (ResponseCodeException) ex.getCause(); ResponseCodeException rce = (ResponseCodeException) ex.getCause();
int responseCode = rce.getResponseCode(); int responseCode = rce.getResponseCode();
URL url = rce.getUrl(); URI uri = rce.getUri();
if (responseCode == 404) if (responseCode == 404)
message += i18n("download.code.404", url); message += i18n("download.code.404", uri);
else else
message += i18n("download.failed", url, responseCode); message += i18n("download.failed", uri, responseCode);
} else { } else {
message += StringUtils.getStackTrace(ex.getCause()); message += StringUtils.getStackTrace(ex.getCause());
} }
} else if (ex instanceof DownloadException) { } else if (ex instanceof DownloadException) {
URL url = ((DownloadException) ex).getUrl(); URI uri = ((DownloadException) ex).getUri();
if (ex.getCause() instanceof SocketTimeoutException) { if (ex.getCause() instanceof SocketTimeoutException) {
message = i18n("install.failed.downloading.timeout", url); message = i18n("install.failed.downloading.timeout", uri);
} else if (ex.getCause() instanceof ResponseCodeException) { } else if (ex.getCause() instanceof ResponseCodeException) {
ResponseCodeException responseCodeException = (ResponseCodeException) ex.getCause(); ResponseCodeException responseCodeException = (ResponseCodeException) ex.getCause();
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) { if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
message = i18n("download.code." + responseCodeException.getResponseCode(), url); message = i18n("download.code." + responseCodeException.getResponseCode(), uri);
} else { } else {
message = i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(ex.getCause()); message = i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(ex.getCause());
} }
} else { } else {
message = i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(ex.getCause()); message = i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(ex.getCause());
} }
} else if (ex instanceof GameAssetIndexDownloadTask.GameAssetIndexMalformedException) { } else if (ex instanceof GameAssetIndexDownloadTask.GameAssetIndexMalformedException) {
message = i18n("assets.index.malformed"); message = i18n("assets.index.malformed");
@@ -298,11 +298,11 @@ public final class LauncherHelper {
} else if (ex instanceof ResponseCodeException) { } else if (ex instanceof ResponseCodeException) {
ResponseCodeException rce = (ResponseCodeException) ex; ResponseCodeException rce = (ResponseCodeException) ex;
int responseCode = rce.getResponseCode(); int responseCode = rce.getResponseCode();
URL url = rce.getUrl(); URI uri = rce.getUri();
if (responseCode == 404) if (responseCode == 404)
message = i18n("download.code.404", url); message = i18n("download.code.404", uri);
else else
message = i18n("download.failed", url, responseCode); message = i18n("download.failed", uri, responseCode);
} else if (ex instanceof CommandTooLongException) { } else if (ex instanceof CommandTooLongException) {
message = i18n("launch.failed.command_too_long"); message = i18n("launch.failed.command_too_long");
} else if (ex instanceof ExecutionPolicyLimitException) { } else if (ex instanceof ExecutionPolicyLimitException) {

View File

@@ -45,7 +45,7 @@ import org.jackhuang.hmcl.util.javafx.BindingMapping;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.net.URL; import java.net.URI;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.*; import java.util.*;
@@ -110,7 +110,7 @@ public final class TexturesLoader {
if (!Files.isRegularFile(file)) { if (!Files.isRegularFile(file)) {
// download it // download it
try { try {
new FileDownloadTask(new URL(texture.getUrl()), file.toFile()).run(); new FileDownloadTask(URI.create(texture.getUrl()), file).run();
LOG.info("Texture downloaded: " + texture.getUrl()); LOG.info("Texture downloaded: " + texture.getUrl());
} catch (Exception e) { } catch (Exception e) {
if (Files.isRegularFile(file)) { if (Files.isRegularFile(file)) {

View File

@@ -29,7 +29,7 @@ import org.jackhuang.hmcl.util.io.ResponseCodeException;
import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLHandshakeException;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.net.SocketTimeoutException; import java.net.SocketTimeoutException;
import java.net.URL; import java.net.URI;
import java.nio.file.AccessDeniedException; import java.nio.file.AccessDeniedException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Map; import java.util.Map;
@@ -137,26 +137,26 @@ public final class DownloadProviders {
public static String localizeErrorMessage(Throwable exception) { public static String localizeErrorMessage(Throwable exception) {
if (exception instanceof DownloadException) { if (exception instanceof DownloadException) {
URL url = ((DownloadException) exception).getUrl(); URI uri = ((DownloadException) exception).getUri();
if (exception.getCause() instanceof SocketTimeoutException) { if (exception.getCause() instanceof SocketTimeoutException) {
return i18n("install.failed.downloading.timeout", url); return i18n("install.failed.downloading.timeout", uri);
} else if (exception.getCause() instanceof ResponseCodeException) { } else if (exception.getCause() instanceof ResponseCodeException) {
ResponseCodeException responseCodeException = (ResponseCodeException) exception.getCause(); ResponseCodeException responseCodeException = (ResponseCodeException) exception.getCause();
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) { if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
return i18n("download.code." + responseCodeException.getResponseCode(), url); return i18n("download.code." + responseCodeException.getResponseCode(), uri);
} else { } else {
return i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause()); return i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(exception.getCause());
} }
} else if (exception.getCause() instanceof FileNotFoundException) { } else if (exception.getCause() instanceof FileNotFoundException) {
return i18n("download.code.404", url); return i18n("download.code.404", uri);
} else if (exception.getCause() instanceof AccessDeniedException) { } else if (exception.getCause() instanceof AccessDeniedException) {
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.access_denied", ((AccessDeniedException) exception.getCause()).getFile()); return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.access_denied", ((AccessDeniedException) exception.getCause()).getFile());
} else if (exception.getCause() instanceof ArtifactMalformedException) { } else if (exception.getCause() instanceof ArtifactMalformedException) {
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.artifact_malformed"); return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.artifact_malformed");
} else if (exception.getCause() instanceof SSLHandshakeException) { } else if (exception.getCause() instanceof SSLHandshakeException) {
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.ssl_handshake"); return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.ssl_handshake");
} else { } else {
return i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause()); return i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(exception.getCause());
} }
} else if (exception instanceof ArtifactMalformedException) { } else if (exception instanceof ArtifactMalformedException) {
return i18n("exception.artifact_malformed"); return i18n("exception.artifact_malformed");

View File

@@ -18,13 +18,15 @@
package org.jackhuang.hmcl.setting; package org.jackhuang.hmcl.setting;
import javafx.beans.InvalidationListener; import javafx.beans.InvalidationListener;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException; import java.io.IOException;
import java.net.*; import java.net.*;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects;
import static org.jackhuang.hmcl.setting.ConfigHolder.config; import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.logging.Logger.LOG; import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -32,7 +34,10 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class ProxyManager { public final class ProxyManager {
private static final ProxySelector NO_PROXY = new SimpleProxySelector(Proxy.NO_PROXY); private static final ProxySelector NO_PROXY = new SimpleProxySelector(Proxy.NO_PROXY);
private static final ProxySelector SYSTEM_DEFAULT = Lang.requireNonNullElse(ProxySelector.getDefault(), NO_PROXY); private static final ProxySelector SYSTEM_DEFAULT = Objects.requireNonNullElse(ProxySelector.getDefault(), NO_PROXY);
private static volatile @NotNull ProxySelector defaultProxySelector = SYSTEM_DEFAULT;
private static volatile @Nullable SimpleAuthenticator defaultAuthenticator = null;
private static ProxySelector getProxySelector() { private static ProxySelector getProxySelector() {
if (config().hasProxy()) { if (config().hasProxy()) {
@@ -53,7 +58,7 @@ public final class ProxyManager {
} }
} }
private static Authenticator getAuthenticator() { private static SimpleAuthenticator getAuthenticator() {
if (config().hasProxy() && config().hasProxyAuth()) { if (config().hasProxy() && config().hasProxyAuth()) {
String username = config().getProxyUser(); String username = config().getProxyUser();
String password = config().getProxyPass(); String password = config().getProxyPass();
@@ -67,15 +72,34 @@ public final class ProxyManager {
} }
static void init() { static void init() {
ProxySelector.setDefault(getProxySelector()); ProxySelector.setDefault(new ProxySelector() {
InvalidationListener updateProxySelector = observable -> ProxySelector.setDefault(getProxySelector()); @Override
public List<Proxy> select(URI uri) {
return defaultProxySelector.select(uri);
}
@Override
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
defaultProxySelector.connectFailed(uri, sa, ioe);
}
});
Authenticator.setDefault(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
var defaultAuthenticator = ProxyManager.defaultAuthenticator;
return defaultAuthenticator != null ? defaultAuthenticator.getPasswordAuthentication() : null;
}
});
defaultProxySelector = getProxySelector();
InvalidationListener updateProxySelector = observable -> defaultProxySelector = getProxySelector();
config().proxyTypeProperty().addListener(updateProxySelector); config().proxyTypeProperty().addListener(updateProxySelector);
config().proxyHostProperty().addListener(updateProxySelector); config().proxyHostProperty().addListener(updateProxySelector);
config().proxyPortProperty().addListener(updateProxySelector); config().proxyPortProperty().addListener(updateProxySelector);
config().hasProxyProperty().addListener(updateProxySelector); config().hasProxyProperty().addListener(updateProxySelector);
Authenticator.setDefault(getAuthenticator()); defaultAuthenticator = getAuthenticator();
InvalidationListener updateAuthenticator = observable -> Authenticator.setDefault(getAuthenticator()); InvalidationListener updateAuthenticator = observable -> defaultAuthenticator = getAuthenticator();
config().hasProxyProperty().addListener(updateAuthenticator); config().hasProxyProperty().addListener(updateAuthenticator);
config().hasProxyAuthProperty().addListener(updateAuthenticator); config().hasProxyAuthProperty().addListener(updateAuthenticator);
config().proxyUserProperty().addListener(updateAuthenticator); config().proxyUserProperty().addListener(updateAuthenticator);
@@ -124,7 +148,7 @@ public final class ProxyManager {
} }
@Override @Override
protected PasswordAuthentication getPasswordAuthentication() { public PasswordAuthentication getPasswordAuthentication() {
return getRequestorType() == RequestorType.PROXY ? new PasswordAuthentication(username, password) : null; return getRequestorType() == RequestorType.PROXY ? new PasswordAuthentication(username, password) : null;
} }
} }

View File

@@ -844,14 +844,14 @@ public final class FXUtils {
} }
} }
public static Image loadImage(URL url) throws Exception { public static Image loadImage(URI uri) throws Exception {
URLConnection connection = NetworkUtils.createConnection(url); URLConnection connection = NetworkUtils.createConnection(uri);
if (connection instanceof HttpURLConnection) { if (connection instanceof HttpURLConnection) {
connection = NetworkUtils.resolveConnection((HttpURLConnection) connection); connection = NetworkUtils.resolveConnection((HttpURLConnection) connection);
} }
try (InputStream input = connection.getInputStream()) { try (InputStream input = connection.getInputStream()) {
String path = url.getPath(); String path = uri.getPath();
if (path != null && "webp".equalsIgnoreCase(StringUtils.substringAfterLast(path, '.'))) if (path != null && "webp".equalsIgnoreCase(StringUtils.substringAfterLast(path, '.')))
return loadWebPImage(input); return loadWebPImage(input);
else { else {

View File

@@ -31,10 +31,10 @@ import org.jackhuang.hmcl.ui.construct.DialogAware;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent; import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.SpinnerPane; import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import javax.net.ssl.SSLException; import javax.net.ssl.SSLException;
import java.io.IOException; import java.io.IOException;
import java.net.URI;
import static org.jackhuang.hmcl.setting.ConfigHolder.config; import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
@@ -196,7 +196,7 @@ public final class AddAuthlibInjectorServerPane extends TransitionPane implement
lblServerName.setText(serverBeingAdded.getName()); lblServerName.setText(serverBeingAdded.getName());
lblServerUrl.setText(serverBeingAdded.getUrl()); lblServerUrl.setText(serverBeingAdded.getUrl());
lblServerWarning.setVisible("http".equals(NetworkUtils.toURL(serverBeingAdded.getUrl()).getProtocol())); lblServerWarning.setVisible("http".equals(URI.create(serverBeingAdded.getUrl()).getScheme()));
this.setContent(confirmServerPane, ContainerAnimations.SWIPE_LEFT); this.setContent(confirmServerPane, ContainerAnimations.SWIPE_LEFT);
} else { } else {

View File

@@ -26,9 +26,7 @@ import javafx.scene.control.ScrollPane;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.*;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.task.TaskListener;
import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction; import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@@ -43,7 +41,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class TaskExecutorDialogPane extends BorderPane { public class TaskExecutorDialogPane extends BorderPane {
private TaskExecutor executor; private TaskExecutor executor;
private TaskCancellationAction onCancel; private TaskCancellationAction onCancel;
private final Consumer<FileDownloadTask.SpeedEvent> speedEventHandler; private final Consumer<FetchTask.SpeedEvent> speedEventHandler;
private final Label lblTitle; private final Label lblTitle;
private final Label lblProgress; private final Label lblProgress;
@@ -108,7 +106,7 @@ public class TaskExecutorDialogPane extends BorderPane {
String finalUnit = unit; String finalUnit = unit;
Platform.runLater(() -> lblProgress.setText(String.format("%.1f %s", finalSpeed, finalUnit))); Platform.runLater(() -> lblProgress.setText(String.format("%.1f %s", finalSpeed, finalUnit)));
}; };
FileDownloadTask.speedEvent.channel(FileDownloadTask.SpeedEvent.class).registerWeak(speedEventHandler); FileDownloadTask.speedEvent.channel(FetchTask.SpeedEvent.class).registerWeak(speedEventHandler);
onEscPressed(this, btnCancel::fire); onEscPressed(this, btnCancel::fire);
} }

View File

@@ -58,7 +58,7 @@ import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URI;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
@@ -214,7 +214,7 @@ public class DecoratorController {
String backgroundImageUrl = config().getBackgroundImageUrl(); String backgroundImageUrl = config().getBackgroundImageUrl();
if (backgroundImageUrl != null) { if (backgroundImageUrl != null) {
try { try {
image = FXUtils.loadImage(new URL(backgroundImageUrl)); image = FXUtils.loadImage(URI.create(backgroundImageUrl));
} catch (Exception e) { } catch (Exception e) {
LOG.warning("Couldn't load background image", e); LOG.warning("Couldn't load background image", e);
} }

View File

@@ -52,10 +52,10 @@ import org.jackhuang.hmcl.ui.wizard.Navigation;
import org.jackhuang.hmcl.ui.wizard.WizardController; import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider; import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.TaskCancellationAction; import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem; import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.net.URI;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.HashMap; import java.util.HashMap;
import java.util.Locale; import java.util.Locale;
@@ -156,7 +156,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
Path dest = runDirectory.resolve(subdirectoryName).resolve(result); Path dest = runDirectory.resolve(subdirectoryName).resolve(result);
Controllers.taskDialog(Task.composeAsync(() -> { Controllers.taskDialog(Task.composeAsync(() -> {
FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(file.getFile().getUrl()), dest.toFile()); var task = new FileDownloadTask(URI.create(file.getFile().getUrl()), dest);
task.setName(file.getName()); task.setName(file.getName());
return task; return task;
}).whenComplete(Schedulers.javafx(), exception -> { }).whenComplete(Schedulers.javafx(), exception -> {

View File

@@ -29,9 +29,7 @@ import javafx.scene.shape.SVGPath;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.ModpackHelper; import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.mod.server.ServerModpackManifest; import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.*;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.SVG;
@@ -43,7 +41,7 @@ import org.jackhuang.hmcl.util.gson.JsonUtils;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URI;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Map; import java.util.Map;
@@ -128,7 +126,7 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
private void onChooseRemoteFile() { private void onChooseRemoteFile() {
Controllers.prompt(i18n("modpack.choose.remote.tooltip"), (urlString, resolve, reject) -> { Controllers.prompt(i18n("modpack.choose.remote.tooltip"), (urlString, resolve, reject) -> {
try { try {
URL url = new URL(urlString); URI url = URI.create(urlString);
if (urlString.endsWith("server-manifest.json")) { if (urlString.endsWith("server-manifest.json")) {
// if urlString ends with .json, we assume that the url is server-manifest.json // if urlString ends with .json, we assume that the url is server-manifest.json
Controllers.taskDialog(new GetTask(url).whenComplete(Schedulers.javafx(), (result, e) -> { Controllers.taskDialog(new GetTask(url).whenComplete(Schedulers.javafx(), (result, e) -> {
@@ -150,7 +148,7 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
resolve.run(); resolve.run();
Controllers.taskDialog( Controllers.taskDialog(
new FileDownloadTask(url, modpack.toFile(), null) new FileDownloadTask(url, modpack, null)
.whenComplete(Schedulers.javafx(), e -> { .whenComplete(Schedulers.javafx(), e -> {
if (e == null) { if (e == null) {
resolve.run(); resolve.run();

View File

@@ -37,7 +37,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.net.SocketTimeoutException; import java.net.SocketTimeoutException;
import java.net.URL; import java.net.URI;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -133,28 +133,28 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
if (exception.getCause() instanceof ResponseCodeException) { if (exception.getCause() instanceof ResponseCodeException) {
ResponseCodeException rce = (ResponseCodeException) exception.getCause(); ResponseCodeException rce = (ResponseCodeException) exception.getCause();
int responseCode = rce.getResponseCode(); int responseCode = rce.getResponseCode();
URL url = rce.getUrl(); URI uri = rce.getUri();
if (responseCode == 404) if (responseCode == 404)
message += i18n("download.code.404", url); message += i18n("download.code.404", uri);
else else
message += i18n("download.failed", url, responseCode); message += i18n("download.failed", uri, responseCode);
} else { } else {
message += StringUtils.getStackTrace(exception.getCause()); message += StringUtils.getStackTrace(exception.getCause());
} }
Controllers.dialog(message, i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next); Controllers.dialog(message, i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
} else if (exception instanceof DownloadException) { } else if (exception instanceof DownloadException) {
URL url = ((DownloadException) exception).getUrl(); URI uri = ((DownloadException) exception).getUri();
if (exception.getCause() instanceof SocketTimeoutException) { if (exception.getCause() instanceof SocketTimeoutException) {
Controllers.dialog(i18n("install.failed.downloading.timeout", url), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next); Controllers.dialog(i18n("install.failed.downloading.timeout", uri), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
} else if (exception.getCause() instanceof ResponseCodeException) { } else if (exception.getCause() instanceof ResponseCodeException) {
ResponseCodeException responseCodeException = (ResponseCodeException) exception.getCause(); ResponseCodeException responseCodeException = (ResponseCodeException) exception.getCause();
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) { if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
Controllers.dialog(i18n("download.code." + responseCodeException.getResponseCode(), url), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next); Controllers.dialog(i18n("download.code." + responseCodeException.getResponseCode(), uri), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
} else { } else {
Controllers.dialog(i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next); Controllers.dialog(i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
} }
} else { } else {
Controllers.dialog(i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next); Controllers.dialog(i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
} }
} else if (exception instanceof UnsupportedInstallationException) { } else if (exception instanceof UnsupportedInstallationException) {
switch (((UnsupportedInstallationException) exception).getReason()) { switch (((UnsupportedInstallationException) exception).getReason()) {

View File

@@ -363,7 +363,7 @@ public final class JavaDownloadDialog extends StackPane {
return getIntegrityCheck return getIntegrityCheck
.thenComposeAsync(integrityCheck -> .thenComposeAsync(integrityCheck ->
new FileDownloadTask(downloadProvider.injectURLWithCandidates(fileInfo.getDirectDownloadUri()), new FileDownloadTask(downloadProvider.injectURLWithCandidates(fileInfo.getDirectDownloadUri()),
targetFile, integrityCheck).setName(fileInfo.getFileName())) targetFile.toPath(), integrityCheck).setName(fileInfo.getFileName()))
.thenSupplyAsync(targetFile::toPath); .thenSupplyAsync(targetFile::toPath);
}) })
.whenComplete(Schedulers.javafx(), ((result, exception) -> { .whenComplete(Schedulers.javafx(), ((result, exception) -> {

View File

@@ -49,12 +49,12 @@ import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.*; import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping; import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.io.File; import java.io.File;
import java.net.URI;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@@ -180,7 +180,7 @@ public class DownloadPage extends Control implements DecoratorPage {
Controllers.taskDialog( Controllers.taskDialog(
Task.composeAsync(() -> { Task.composeAsync(() -> {
FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(file.getFile().getUrl()), dest, file.getFile().getIntegrityCheck()); var task = new FileDownloadTask(URI.create(file.getFile().getUrl()), dest.toPath(), file.getFile().getIntegrityCheck());
task.setName(file.getName()); task.setName(file.getName());
return task; return task;
}), }),

View File

@@ -45,7 +45,7 @@ import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.TaskCancellationAction; import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.CSVTable; import org.jackhuang.hmcl.util.io.CSVTable;
import java.net.URL; import java.net.URI;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -293,9 +293,9 @@ public class ModUpdatesPage extends BorderPane implements DecoratorPage {
if (isDisabled) if (isDisabled)
fileName += ModManager.DISABLED_EXTENSION; fileName += ModManager.DISABLED_EXTENSION;
FileDownloadTask task = new FileDownloadTask( var task = new FileDownloadTask(
new URL(remote.getFile().getUrl()), URI.create(remote.getFile().getUrl()),
modManager.getModsDirectory().resolve(fileName).toFile()); modManager.getModsDirectory().resolve(fileName));
task.setName(remote.getName()); task.setName(remote.getName());
return task; return task;

View File

@@ -28,10 +28,7 @@ import org.jackhuang.hmcl.game.GameRepository;
import org.jackhuang.hmcl.game.LauncherHelper; import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.mod.RemoteMod; import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.setting.*; import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.*;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.account.CreateAccountPane; import org.jackhuang.hmcl.ui.account.CreateAccountPane;
@@ -47,7 +44,8 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.concurrent.CancellationException; import java.util.concurrent.CancellationException;
@@ -75,18 +73,18 @@ public final class Versions {
public static void downloadModpackImpl(Profile profile, String version, RemoteMod.Version file) { public static void downloadModpackImpl(Profile profile, String version, RemoteMod.Version file) {
Path modpack; Path modpack;
URL downloadURL; URI downloadURL;
try { try {
modpack = Files.createTempFile("modpack", ".zip"); modpack = Files.createTempFile("modpack", ".zip");
downloadURL = new URL(file.getFile().getUrl()); downloadURL = new URI(file.getFile().getUrl());
} catch (IOException e) { } catch (IOException | URISyntaxException e) {
Controllers.dialog( Controllers.dialog(
i18n("install.failed.downloading.detail", file.getFile().getUrl()) + "\n" + StringUtils.getStackTrace(e), i18n("install.failed.downloading.detail", file.getFile().getUrl()) + "\n" + StringUtils.getStackTrace(e),
i18n("download.failed.no_code"), MessageDialogPane.MessageType.ERROR); i18n("download.failed.no_code"), MessageDialogPane.MessageType.ERROR);
return; return;
} }
Controllers.taskDialog( Controllers.taskDialog(
new FileDownloadTask(downloadURL, modpack.toFile()) new FileDownloadTask(downloadURL, modpack)
.whenComplete(Schedulers.javafx(), e -> { .whenComplete(Schedulers.javafx(), e -> {
if (e == null) { if (e == null) {
if (version != null) { if (version != null) {

View File

@@ -19,21 +19,21 @@ package org.jackhuang.hmcl.upgrade;
import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.util.Pack200Utils; import org.jackhuang.hmcl.util.Pack200Utils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.tukaani.xz.XZInputStream; import org.tukaani.xz.XZInputStream;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.InputStream; import java.io.InputStream;
import java.net.URI;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.jar.JarOutputStream; import java.util.jar.JarOutputStream;
class HMCLDownloadTask extends FileDownloadTask { final class HMCLDownloadTask extends FileDownloadTask {
private RemoteVersion.Type archiveFormat; private final RemoteVersion.Type archiveFormat;
public HMCLDownloadTask(RemoteVersion version, Path target) { public HMCLDownloadTask(RemoteVersion version, Path target) {
super(NetworkUtils.toURL(version.getUrl()), target.toFile(), version.getIntegrityCheck()); super(URI.create(version.getUrl()), target, version.getIntegrityCheck());
archiveFormat = version.getType(); archiveFormat = version.getType();
} }
@@ -42,8 +42,7 @@ class HMCLDownloadTask extends FileDownloadTask {
super.execute(); super.execute();
try { try {
Path target = getFile().toPath(); Path target = getPath();
switch (archiveFormat) { switch (archiveFormat) {
case JAR: case JAR:
break; break;
@@ -51,7 +50,7 @@ class HMCLDownloadTask extends FileDownloadTask {
case PACK_XZ: case PACK_XZ:
byte[] raw = Files.readAllBytes(target); byte[] raw = Files.readAllBytes(target);
try (InputStream in = new XZInputStream(new ByteArrayInputStream(raw)); try (InputStream in = new XZInputStream(new ByteArrayInputStream(raw));
JarOutputStream out = new JarOutputStream(Files.newOutputStream(target))) { JarOutputStream out = new JarOutputStream(Files.newOutputStream(target))) {
Pack200Utils.unpack(in, out); Pack200Utils.unpack(in, out);
} }
break; break;
@@ -60,7 +59,11 @@ class HMCLDownloadTask extends FileDownloadTask {
throw new IllegalArgumentException("Unknown format: " + archiveFormat); throw new IllegalArgumentException("Unknown format: " + archiveFormat);
} }
} catch (Throwable e) { } catch (Throwable e) {
getFile().delete(); try {
Files.deleteIfExists(getPath());
} catch (Throwable e2) {
e.addSuppressed(e2);
}
throw e; throw e;
} }
} }

View File

@@ -26,13 +26,14 @@ import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils; import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException; import java.io.IOException;
import java.net.URI;
import java.util.Optional; import java.util.Optional;
public class RemoteVersion { public class RemoteVersion {
public static RemoteVersion fetch(UpdateChannel channel, String url) throws IOException { public static RemoteVersion fetch(UpdateChannel channel, String url) throws IOException {
try { try {
JsonObject response = JsonUtils.fromNonNullJson(NetworkUtils.doGet(NetworkUtils.toURL(url)), JsonObject.class); JsonObject response = JsonUtils.fromNonNullJson(NetworkUtils.doGet(URI.create(url)), JsonObject.class);
String version = Optional.ofNullable(response.get("version")).map(JsonElement::getAsString).orElseThrow(() -> new IOException("version is missing")); String version = Optional.ofNullable(response.get("version")).map(JsonElement::getAsString).orElseThrow(() -> new IOException("version is missing"));
String jarUrl = Optional.ofNullable(response.get("jar")).map(JsonElement::getAsString).orElse(null); String jarUrl = Optional.ofNullable(response.get("jar")).map(JsonElement::getAsString).orElse(null);
String jarHash = Optional.ofNullable(response.get("jarsha1")).map(JsonElement::getAsString).orElse(null); String jarHash = Optional.ofNullable(response.get("jarsha1")).map(JsonElement::getAsString).orElse(null);

View File

@@ -28,6 +28,7 @@ import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.util.io.NetworkUtils; import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException; import java.io.IOException;
import java.net.URI;
import java.util.HashMap; import java.util.HashMap;
import static org.jackhuang.hmcl.util.logging.Logger.LOG; import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -122,7 +123,7 @@ public final class CrashReporter implements Thread.UncaughtExceptionHandler {
map.put("version", Metadata.VERSION); map.put("version", Metadata.VERSION);
map.put("log", LOG.getLogs()); map.put("log", LOG.getLogs());
try { try {
String response = NetworkUtils.doPost(NetworkUtils.toURL(Metadata.PUBLISH_URL + "/hmcl/crash.php"), map); String response = NetworkUtils.doPost(URI.create(Metadata.PUBLISH_URL + "/hmcl/crash.php"), map);
if (StringUtils.isNotBlank(response)) if (StringUtils.isNotBlank(response))
LOG.error("Crash server response: " + response); LOG.error("Crash server response: " + response);
} catch (IOException ex) { } catch (IOException ex) {

View File

@@ -25,7 +25,7 @@ import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.util.io.HttpRequest; import org.jackhuang.hmcl.util.io.HttpRequest;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URI;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Map; import java.util.Map;
@@ -96,7 +96,7 @@ public class AuthlibInjectorDownloader implements AuthlibInjectorArtifactProvide
} }
try { try {
new FileDownloadTask(downloadProvider.get().injectURLWithCandidates(latest.downloadUrl), artifactLocation.toFile(), new FileDownloadTask(downloadProvider.get().injectURLWithCandidates(latest.downloadUrl), artifactLocation,
Optional.ofNullable(latest.checksums.get("sha256")) Optional.ofNullable(latest.checksums.get("sha256"))
.map(checksum -> new IntegrityCheck("SHA-256", checksum)) .map(checksum -> new IntegrityCheck("SHA-256", checksum))
.orElse(null)) .orElse(null))
@@ -110,9 +110,9 @@ public class AuthlibInjectorDownloader implements AuthlibInjectorArtifactProvide
private AuthlibInjectorVersionInfo getLatestArtifactInfo() throws IOException { private AuthlibInjectorVersionInfo getLatestArtifactInfo() throws IOException {
IOException exception = null; IOException exception = null;
for (URL url : downloadProvider.get().injectURLWithCandidates(LATEST_BUILD_URL)) { for (URI url : downloadProvider.get().injectURLWithCandidates(LATEST_BUILD_URL)) {
try { try {
return HttpRequest.GET(url.toExternalForm()).getJson(AuthlibInjectorVersionInfo.class); return HttpRequest.GET(url.toString()).getJson(AuthlibInjectorVersionInfo.class);
} catch (IOException | JsonParseException e) { } catch (IOException | JsonParseException e) {
if (exception == null) { if (exception == null) {
exception = new IOException("Failed to fetch authlib-injector artifact info"); exception = new IOException("Failed to fetch authlib-injector artifact info");

View File

@@ -17,13 +17,11 @@
*/ */
package org.jackhuang.hmcl.auth.authlibinjector; package org.jackhuang.hmcl.auth.authlibinjector;
import static org.jackhuang.hmcl.util.io.NetworkUtils.toURL;
import org.jackhuang.hmcl.auth.AuthenticationException; import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilProvider; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilProvider;
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter; import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
import java.net.URL; import java.net.URI;
import java.util.UUID; import java.util.UUID;
public class AuthlibInjectorProvider implements YggdrasilProvider { public class AuthlibInjectorProvider implements YggdrasilProvider {
@@ -35,33 +33,33 @@ public class AuthlibInjectorProvider implements YggdrasilProvider {
} }
@Override @Override
public URL getAuthenticationURL() throws AuthenticationException { public URI getAuthenticationURL() throws AuthenticationException {
return toURL(apiRoot + "authserver/authenticate"); return URI.create(apiRoot + "authserver/authenticate");
} }
@Override @Override
public URL getRefreshmentURL() throws AuthenticationException { public URI getRefreshmentURL() throws AuthenticationException {
return toURL(apiRoot + "authserver/refresh"); return URI.create(apiRoot + "authserver/refresh");
} }
@Override @Override
public URL getValidationURL() throws AuthenticationException { public URI getValidationURL() throws AuthenticationException {
return toURL(apiRoot + "authserver/validate"); return URI.create(apiRoot + "authserver/validate");
} }
@Override @Override
public URL getInvalidationURL() throws AuthenticationException { public URI getInvalidationURL() throws AuthenticationException {
return toURL(apiRoot + "authserver/invalidate"); return URI.create(apiRoot + "authserver/invalidate");
} }
@Override @Override
public URL getSkinUploadURL(UUID uuid) throws UnsupportedOperationException { public URI getSkinUploadURL(UUID uuid) throws UnsupportedOperationException {
return toURL(apiRoot + "api/user/profile/" + UUIDTypeAdapter.fromUUID(uuid) + "/skin"); return URI.create(apiRoot + "api/user/profile/" + UUIDTypeAdapter.fromUUID(uuid) + "/skin");
} }
@Override @Override
public URL getProfilePropertiesURL(UUID uuid) throws AuthenticationException { public URI getProfilePropertiesURL(UUID uuid) throws AuthenticationException {
return toURL(apiRoot + "sessionserver/session/minecraft/profile/" + UUIDTypeAdapter.fromUUID(uuid)); return URI.create(apiRoot + "sessionserver/session/minecraft/profile/" + UUIDTypeAdapter.fromUUID(uuid));
} }
@Override @Override

View File

@@ -17,7 +17,6 @@
*/ */
package org.jackhuang.hmcl.auth.authlibinjector; package org.jackhuang.hmcl.auth.authlibinjector;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Collections.emptyMap; import static java.util.Collections.emptyMap;
import static org.jackhuang.hmcl.util.Lang.tryCast; import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.logging.Logger.LOG; import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -25,14 +24,15 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URL; import java.net.URI;
import java.net.URISyntaxException;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.util.io.HttpRequest; import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.javafx.ObservableHelper; import org.jackhuang.hmcl.util.javafx.ObservableHelper;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@@ -58,17 +58,14 @@ public class AuthlibInjectorServer implements Observable {
public static AuthlibInjectorServer locateServer(String url) throws IOException { public static AuthlibInjectorServer locateServer(String url) throws IOException {
try { try {
url = addHttpsIfMissing(url); url = addHttpsIfMissing(url);
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); HttpURLConnection conn = NetworkUtils.createHttpConnection(URI.create(url));
conn.setRequestProperty("Accept-Language", Locale.getDefault().toLanguageTag());
String ali = conn.getHeaderField("x-authlib-injector-api-location"); String ali = conn.getHeaderField("x-authlib-injector-api-location");
if (ali != null) { if (ali != null) {
URL absoluteAli = new URL(conn.getURL(), ali); URI absoluteAli = conn.getURL().toURI().resolve(ali);
if (!urlEqualsIgnoreSlash(url, absoluteAli.toString())) { if (!urlEqualsIgnoreSlash(url, absoluteAli.toString())) {
conn.disconnect(); conn.disconnect();
url = absoluteAli.toString(); url = absoluteAli.toString();
conn = (HttpURLConnection) absoluteAli.openConnection(); conn = NetworkUtils.createHttpConnection(absoluteAli);
conn.setRequestProperty("Accept-Language", Locale.getDefault().toLanguageTag());
} }
} }
@@ -77,22 +74,26 @@ public class AuthlibInjectorServer implements Observable {
try { try {
AuthlibInjectorServer server = new AuthlibInjectorServer(url); AuthlibInjectorServer server = new AuthlibInjectorServer(url);
server.refreshMetadata(new String(conn.getInputStream().readAllBytes(), UTF_8)); server.refreshMetadata(NetworkUtils.readFullyAsString(conn));
return server; return server;
} finally { } finally {
conn.disconnect(); conn.disconnect();
} }
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException | URISyntaxException e) {
throw new IOException(e); throw new IOException(e);
} }
} }
private static String addHttpsIfMissing(String url) { private static String addHttpsIfMissing(String url) throws IOException {
String lowercased = url.toLowerCase(Locale.ROOT); URI uri = URI.create(url);
if (!lowercased.startsWith("http://") && !lowercased.startsWith("https://")) {
url = "https://" + url; if (uri.getScheme() == null) {
return "https://" + url;
} else if (!NetworkUtils.isHttpUri(uri)) {
throw new IOException("Yggdrasil server should be an HTTP or HTTPS URI, but got: " + url);
} else {
return url;
} }
return url;
} }
private static boolean urlEqualsIgnoreSlash(String a, String b) { private static boolean urlEqualsIgnoreSlash(String a, String b) {
@@ -103,7 +104,7 @@ public class AuthlibInjectorServer implements Observable {
return a.equals(b); return a.equals(b);
} }
private String url; private final String url;
@Nullable @Nullable
private String metadataResponse; private String metadataResponse;
private long metadataTimestamp; private long metadataTimestamp;

View File

@@ -34,7 +34,8 @@ import org.jackhuang.hmcl.util.javafx.ObservableOptionalCache;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URL; import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.*; import java.util.*;
@@ -164,7 +165,7 @@ public class MicrosoftService {
.accept("application/json").createConnection(); .accept("application/json").createConnection();
if (request.getResponseCode() != 200) { if (request.getResponseCode() != 200) {
throw new ResponseCodeException(new URL("https://api.minecraftservices.com/entitlements/mcstore"), request.getResponseCode()); throw new ResponseCodeException(URI.create("https://api.minecraftservices.com/entitlements/mcstore"), request.getResponseCode());
} }
// Get Minecraft Account UUID // Get Minecraft Account UUID
@@ -247,22 +248,22 @@ public class MicrosoftService {
if (responseCode == HTTP_NOT_FOUND) { if (responseCode == HTTP_NOT_FOUND) {
throw new NoMinecraftJavaEditionProfileException(); throw new NoMinecraftJavaEditionProfileException();
} else if (responseCode != 200) { } else if (responseCode != 200) {
throw new ResponseCodeException(new URL("https://api.minecraftservices.com/minecraft/profile"), responseCode); throw new ResponseCodeException(URI.create("https://api.minecraftservices.com/minecraft/profile"), responseCode);
} }
String result = NetworkUtils.readData(conn); String result = NetworkUtils.readFullyAsString(conn);
return JsonUtils.fromNonNullJson(result, MinecraftProfileResponse.class); return JsonUtils.fromNonNullJson(result, MinecraftProfileResponse.class);
} }
public Optional<CompleteGameProfile> getCompleteGameProfile(UUID uuid) throws AuthenticationException { public Optional<CompleteGameProfile> getCompleteGameProfile(UUID uuid) throws AuthenticationException {
Objects.requireNonNull(uuid); Objects.requireNonNull(uuid);
return Optional.ofNullable(GSON.fromJson(request(NetworkUtils.toURL("https://sessionserver.mojang.com/session/minecraft/profile/" + UUIDTypeAdapter.fromUUID(uuid)), null), CompleteGameProfile.class)); return Optional.ofNullable(GSON.fromJson(request(URI.create("https://sessionserver.mojang.com/session/minecraft/profile/" + UUIDTypeAdapter.fromUUID(uuid)), null), CompleteGameProfile.class));
} }
public void uploadSkin(String accessToken, boolean isSlim, Path file) throws AuthenticationException, UnsupportedOperationException { public void uploadSkin(String accessToken, boolean isSlim, Path file) throws AuthenticationException, UnsupportedOperationException {
try { try {
HttpURLConnection con = NetworkUtils.createHttpConnection(NetworkUtils.toURL("https://api.minecraftservices.com/minecraft/profile/skins")); HttpURLConnection con = NetworkUtils.createHttpConnection(URI.create("https://api.minecraftservices.com/minecraft/profile/skins"));
con.setRequestMethod("POST"); con.setRequestMethod("POST");
con.setRequestProperty("Authorization", "Bearer " + accessToken); con.setRequestProperty("Authorization", "Bearer " + accessToken);
con.setDoOutput(true); con.setDoOutput(true);
@@ -273,21 +274,21 @@ public class MicrosoftService {
} }
} }
String response = NetworkUtils.readData(con); String response = NetworkUtils.readFullyAsString(con);
if (StringUtils.isBlank(response)) { if (StringUtils.isBlank(response)) {
if (con.getResponseCode() / 100 != 2) if (con.getResponseCode() / 100 != 2)
throw new ResponseCodeException(con.getURL(), con.getResponseCode()); throw new ResponseCodeException(con.getURL().toURI(), con.getResponseCode());
} else { } else {
MinecraftErrorResponse profileResponse = GSON.fromJson(response, MinecraftErrorResponse.class); MinecraftErrorResponse profileResponse = GSON.fromJson(response, MinecraftErrorResponse.class);
if (StringUtils.isNotBlank(profileResponse.errorMessage) || con.getResponseCode() / 100 != 2) if (StringUtils.isNotBlank(profileResponse.errorMessage) || con.getResponseCode() / 100 != 2)
throw new AuthenticationException("Failed to upload skin, response code: " + con.getResponseCode() + ", response: " + response); throw new AuthenticationException("Failed to upload skin, response code: " + con.getResponseCode() + ", response: " + response);
} }
} catch (IOException | JsonParseException e) { } catch (IOException | JsonParseException | URISyntaxException e) {
throw new AuthenticationException(e); throw new AuthenticationException(e);
} }
} }
private static String request(URL url, Object payload) throws AuthenticationException { private static String request(URI url, Object payload) throws AuthenticationException {
try { try {
if (payload == null) if (payload == null)
return NetworkUtils.doGet(url); return NetworkUtils.doGet(url);

View File

@@ -33,14 +33,11 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URL; import java.net.URI;
import java.net.URLConnection; import java.net.URLConnection;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Collections; import java.util.*;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import static org.jackhuang.hmcl.util.Lang.mapOf; import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Lang.tryCast; import static org.jackhuang.hmcl.util.Lang.tryCast;
@@ -169,7 +166,7 @@ public class Skin {
String realCslApi = type == Type.LITTLE_SKIN String realCslApi = type == Type.LITTLE_SKIN
? "https://littleskin.cn/csl" ? "https://littleskin.cn/csl"
: StringUtils.removeSuffix(Lang.requireNonNullElse(cslApi, ""), "/"); : StringUtils.removeSuffix(Lang.requireNonNullElse(cslApi, ""), "/");
return Task.composeAsync(() -> new GetTask(new URL(String.format("%s/%s.json", realCslApi, username)))) return Task.composeAsync(() -> new GetTask(URI.create(String.format("%s/%s.json", realCslApi, username))))
.thenComposeAsync(json -> { .thenComposeAsync(json -> {
SkinJson result = JsonUtils.GSON.fromJson(json, SkinJson.class); SkinJson result = JsonUtils.GSON.fromJson(json, SkinJson.class);
@@ -179,8 +176,8 @@ public class Skin {
return Task.allOf( return Task.allOf(
Task.supplyAsync(result::getModel), Task.supplyAsync(result::getModel),
result.getHash() == null ? Task.supplyAsync(() -> null) : new FetchBytesTask(new URL(String.format("%s/textures/%s", realCslApi, result.getHash())), 3), result.getHash() == null ? Task.supplyAsync(() -> null) : new FetchBytesTask(URI.create(String.format("%s/textures/%s", realCslApi, result.getHash())), 3),
result.getCapeHash() == null ? Task.supplyAsync(() -> null) : new FetchBytesTask(new URL(String.format("%s/textures/%s", realCslApi, result.getCapeHash())), 3) result.getCapeHash() == null ? Task.supplyAsync(() -> null) : new FetchBytesTask(URI.create(String.format("%s/textures/%s", realCslApi, result.getCapeHash())), 3)
); );
}).thenApplyAsync(result -> { }).thenApplyAsync(result -> {
if (result == null) { if (result == null) {
@@ -232,8 +229,8 @@ public class Skin {
private static class FetchBytesTask extends FetchTask<InputStream> { private static class FetchBytesTask extends FetchTask<InputStream> {
public FetchBytesTask(URL url, int retry) { public FetchBytesTask(URI uri, int retry) {
super(Collections.singletonList(url), retry); super(List.of(uri), retry);
} }
@Override @Override
@@ -247,7 +244,7 @@ public class Skin {
} }
@Override @Override
protected Context getContext(URLConnection conn, boolean checkETag) throws IOException { protected Context getContext(URLConnection connection, boolean checkETag) throws IOException {
return new Context() { return new Context() {
final ByteArrayOutputStream baos = new ByteArrayOutputStream(); final ByteArrayOutputStream baos = new ByteArrayOutputStream();
@@ -263,7 +260,7 @@ public class Skin {
setResult(new ByteArrayInputStream(baos.toByteArray())); setResult(new ByteArrayInputStream(baos.toByteArray()));
if (checkETag) { if (checkETag) {
repository.cacheBytes(baos.toByteArray(), conn); repository.cacheBytes(connection, baos.toByteArray());
} }
} }
}; };

View File

@@ -19,7 +19,7 @@ package org.jackhuang.hmcl.auth.yggdrasil;
import org.jackhuang.hmcl.auth.AuthenticationException; import org.jackhuang.hmcl.auth.AuthenticationException;
import java.net.URL; import java.net.URI;
import java.util.UUID; import java.util.UUID;
/** /**
@@ -27,13 +27,13 @@ import java.util.UUID;
*/ */
public interface YggdrasilProvider { public interface YggdrasilProvider {
URL getAuthenticationURL() throws AuthenticationException; URI getAuthenticationURL() throws AuthenticationException;
URL getRefreshmentURL() throws AuthenticationException; URI getRefreshmentURL() throws AuthenticationException;
URL getValidationURL() throws AuthenticationException; URI getValidationURL() throws AuthenticationException;
URL getInvalidationURL() throws AuthenticationException; URI getInvalidationURL() throws AuthenticationException;
/** /**
* URL to upload skin. * URL to upload skin.
@@ -51,8 +51,8 @@ public interface YggdrasilProvider {
* @throws AuthenticationException if url cannot be generated. e.g. some parameter or query is malformed. * @throws AuthenticationException if url cannot be generated. e.g. some parameter or query is malformed.
* @throws UnsupportedOperationException if the Yggdrasil provider does not support third-party skin uploading. * @throws UnsupportedOperationException if the Yggdrasil provider does not support third-party skin uploading.
*/ */
URL getSkinUploadURL(UUID uuid) throws AuthenticationException, UnsupportedOperationException; URI getSkinUploadURL(UUID uuid) throws AuthenticationException, UnsupportedOperationException;
URL getProfilePropertiesURL(UUID uuid) throws AuthenticationException; URI getProfilePropertiesURL(UUID uuid) throws AuthenticationException;
} }

View File

@@ -34,7 +34,7 @@ import org.jackhuang.hmcl.util.javafx.ObservableOptionalCache;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URL; import java.net.URI;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.*; import java.util.*;
@@ -160,7 +160,7 @@ public class YggdrasilService {
request.file("file", FileUtils.getName(file), "image/" + FileUtils.getExtension(file), fis); request.file("file", FileUtils.getName(file), "image/" + FileUtils.getExtension(file), fis);
} }
} }
requireEmpty(NetworkUtils.readData(con)); requireEmpty(NetworkUtils.readFullyAsString(con));
} catch (IOException e) { } catch (IOException e) {
throw new AuthenticationException(e); throw new AuthenticationException(e);
} }
@@ -227,12 +227,12 @@ public class YggdrasilService {
} }
} }
private static String request(URL url, Object payload) throws AuthenticationException { private static String request(URI uri, Object payload) throws AuthenticationException {
try { try {
if (payload == null) if (payload == null)
return NetworkUtils.doGet(url); return NetworkUtils.doGet(uri);
else else
return NetworkUtils.doPost(url, payload instanceof String ? (String) payload : GSON.toJson(payload), "application/json"); return NetworkUtils.doPost(uri, payload instanceof String ? (String) payload : GSON.toJson(payload), "application/json");
} catch (IOException e) { } catch (IOException e) {
throw new ServerDisconnectException(e); throw new ServerDisconnectException(e);
} }

View File

@@ -17,7 +17,7 @@
*/ */
package org.jackhuang.hmcl.download; package org.jackhuang.hmcl.download;
import java.net.URL; import java.net.URI;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -59,21 +59,21 @@ public class AdaptedDownloadProvider implements DownloadProvider {
} }
@Override @Override
public List<URL> getAssetObjectCandidates(String assetObjectLocation) { public List<URI> getAssetObjectCandidates(String assetObjectLocation) {
return downloadProviderCandidates.stream() return downloadProviderCandidates.stream()
.flatMap(d -> d.getAssetObjectCandidates(assetObjectLocation).stream()) .flatMap(d -> d.getAssetObjectCandidates(assetObjectLocation).stream())
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@Override @Override
public List<URL> injectURLWithCandidates(String baseURL) { public List<URI> injectURLWithCandidates(String baseURL) {
return downloadProviderCandidates.stream() return downloadProviderCandidates.stream()
.flatMap(d -> d.injectURLWithCandidates(baseURL).stream()) .flatMap(d -> d.injectURLWithCandidates(baseURL).stream())
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@Override @Override
public List<URL> injectURLsWithCandidates(List<String> urls) { public List<URI> injectURLsWithCandidates(List<String> urls) {
return downloadProviderCandidates.stream() return downloadProviderCandidates.stream()
.flatMap(d -> d.injectURLsWithCandidates(urls).stream()) .flatMap(d -> d.injectURLsWithCandidates(urls).stream())
.collect(Collectors.toList()); .collect(Collectors.toList());

View File

@@ -17,7 +17,7 @@
*/ */
package org.jackhuang.hmcl.download; package org.jackhuang.hmcl.download;
import java.net.URL; import java.net.URI;
import java.util.List; import java.util.List;
/** /**
@@ -51,17 +51,17 @@ public class AutoDownloadProvider implements DownloadProvider {
} }
@Override @Override
public List<URL> getAssetObjectCandidates(String assetObjectLocation) { public List<URI> getAssetObjectCandidates(String assetObjectLocation) {
return fileProvider.getAssetObjectCandidates(assetObjectLocation); return fileProvider.getAssetObjectCandidates(assetObjectLocation);
} }
@Override @Override
public List<URL> injectURLWithCandidates(String baseURL) { public List<URI> injectURLWithCandidates(String baseURL) {
return fileProvider.injectURLWithCandidates(baseURL); return fileProvider.injectURLWithCandidates(baseURL);
} }
@Override @Override
public List<URL> injectURLsWithCandidates(List<String> urls) { public List<URI> injectURLsWithCandidates(List<String> urls) {
return fileProvider.injectURLsWithCandidates(urls); return fileProvider.injectURLsWithCandidates(urls);
} }

View File

@@ -17,10 +17,7 @@
*/ */
package org.jackhuang.hmcl.download; package org.jackhuang.hmcl.download;
import org.jackhuang.hmcl.util.io.NetworkUtils; import java.net.URI;
import java.net.URL;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -35,8 +32,8 @@ public interface DownloadProvider {
String getAssetBaseURL(); String getAssetBaseURL();
default List<URL> getAssetObjectCandidates(String assetObjectLocation) { default List<URI> getAssetObjectCandidates(String assetObjectLocation) {
return Collections.singletonList(NetworkUtils.toURL(getAssetBaseURL() + assetObjectLocation)); return List.of(URI.create(getAssetBaseURL() + assetObjectLocation));
} }
/** /**
@@ -59,11 +56,11 @@ public interface DownloadProvider {
* @param baseURL original URL provided by Mojang and Forge. * @param baseURL original URL provided by Mojang and Forge.
* @return the URL that is equivalent to [baseURL], but belongs to your own service provider. * @return the URL that is equivalent to [baseURL], but belongs to your own service provider.
*/ */
default List<URL> injectURLWithCandidates(String baseURL) { default List<URI> injectURLWithCandidates(String baseURL) {
return Collections.singletonList(NetworkUtils.toURL(injectURL(baseURL))); return List.of(URI.create(injectURL(baseURL)));
} }
default List<URL> injectURLsWithCandidates(List<String> urls) { default List<URI> injectURLsWithCandidates(List<String> urls) {
return urls.stream().flatMap(url -> injectURLWithCandidates(url).stream()).collect(Collectors.toList()); return urls.stream().flatMap(url -> injectURLWithCandidates(url).stream()).collect(Collectors.toList());
} }

View File

@@ -23,7 +23,7 @@ import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URI;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
@@ -59,8 +59,8 @@ public final class FabricAPIInstallTask extends Task<Version> {
@Override @Override
public void execute() throws IOException { public void execute() throws IOException {
dependencies.add(new FileDownloadTask( dependencies.add(new FileDownloadTask(
new URL(remote.getVersion().getFile().getUrl()), URI.create(remote.getVersion().getFile().getUrl()),
dependencyManager.getGameRepository().getRunDirectory(version.getId()).toPath().resolve("mods").resolve("fabric-api-" + remote.getVersion().getVersion() + ".jar").toFile(), dependencyManager.getGameRepository().getRunDirectory(version.getId()).toPath().resolve("mods").resolve("fabric-api-" + remote.getVersion().getVersion() + ".jar"),
remote.getVersion().getFile().getIntegrityCheck()) remote.getVersion().getFile().getIntegrityCheck())
); );
} }

View File

@@ -69,7 +69,7 @@ public final class ForgeInstallTask extends Task<Version> {
dependent = new FileDownloadTask( dependent = new FileDownloadTask(
dependencyManager.getDownloadProvider().injectURLsWithCandidates(remote.getUrls()), dependencyManager.getDownloadProvider().injectURLsWithCandidates(remote.getUrls()),
installer.toFile(), null); installer, null);
dependent.setCacheRepository(dependencyManager.getCacheRepository()); dependent.setCacheRepository(dependencyManager.getCacheRepository());
dependent.setCaching(true); dependent.setCaching(true);
dependent.addIntegrityCheckHandler(FileDownloadTask.ZIP_INTEGRITY_CHECK_HANDLER); dependent.addIntegrityCheckHandler(FileDownloadTask.ZIP_INTEGRITY_CHECK_HANDLER);

View File

@@ -31,7 +31,6 @@ import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.util.DigestUtils; import org.jackhuang.hmcl.util.DigestUtils;
import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.function.ExceptionalFunction; import org.jackhuang.hmcl.util.function.ExceptionalFunction;
@@ -48,7 +47,7 @@ import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URL; import java.net.URI;
import java.nio.file.FileSystem; import java.nio.file.FileSystem;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
@@ -346,12 +345,12 @@ public class ForgeNewInstallTask extends Task<Version> {
throw new Exception("client_mappings download info not found"); throw new Exception("client_mappings download info not found");
} }
List<URL> mappingsUrl = dependencyManager.getDownloadProvider() List<URI> mappingsUrl = dependencyManager.getDownloadProvider()
.injectURLWithCandidates(mappings.getUrl()); .injectURLWithCandidates(mappings.getUrl());
FileDownloadTask mappingsTask = new FileDownloadTask( var mappingsTask = new FileDownloadTask(
mappingsUrl, mappingsUrl,
new File(output), Path.of(output),
IntegrityCheck.of("SHA-1", mappings.getSha1())); FileDownloadTask.IntegrityCheck.of("SHA-1", mappings.getSha1()));
mappingsTask.setCaching(true); mappingsTask.setCaching(true);
mappingsTask.setCacheRepository(dependencyManager.getCacheRepository()); mappingsTask.setCacheRepository(dependencyManager.getCacheRepository());
return mappingsTask; return mappingsTask;

View File

@@ -29,7 +29,7 @@ import org.jackhuang.hmcl.util.CacheRepository;
import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.gson.JsonUtils;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URI;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
@@ -102,9 +102,9 @@ public final class GameAssetDownloadTask extends Task<Void> {
LOG.warning("Unable to calc hash value of file " + file, e); LOG.warning("Unable to calc hash value of file " + file, e);
} }
if (download) { if (download) {
List<URL> urls = dependencyManager.getDownloadProvider().getAssetObjectCandidates(assetObject.getLocation()); List<URI> uris = dependencyManager.getDownloadProvider().getAssetObjectCandidates(assetObject.getLocation());
FileDownloadTask task = new FileDownloadTask(urls, file.toFile(), new FileDownloadTask.IntegrityCheck("SHA-1", assetObject.getHash())); var task = new FileDownloadTask(uris, file, new FileDownloadTask.IntegrityCheck("SHA-1", assetObject.getHash()));
task.setName(assetObject.getHash()); task.setName(assetObject.getHash());
task.setCandidate(dependencyManager.getCacheRepository().getCommonDirectory() task.setCandidate(dependencyManager.getCacheRepository().getCommonDirectory()
.resolve("assets").resolve("objects").resolve(assetObject.getLocation())); .resolve("assets").resolve("objects").resolve(assetObject.getLocation()));

View File

@@ -94,9 +94,9 @@ public final class GameAssetIndexDownloadTask extends Task<Void> {
// We should not check the hash code of asset index file since this file is not consistent // We should not check the hash code of asset index file since this file is not consistent
// And Mojang will modify this file anytime. So assetIndex.hash might be outdated. // And Mojang will modify this file anytime. So assetIndex.hash might be outdated.
FileDownloadTask task = new FileDownloadTask( var task = new FileDownloadTask(
dependencyManager.getDownloadProvider().injectURLWithCandidates(assetIndexInfo.getUrl()), dependencyManager.getDownloadProvider().injectURLWithCandidates(assetIndexInfo.getUrl()),
assetIndexFile.toFile(), assetIndexFile,
verifyHashCode ? new FileDownloadTask.IntegrityCheck("SHA-1", assetIndexInfo.getSha1()) : null verifyHashCode ? new FileDownloadTask.IntegrityCheck("SHA-1", assetIndexInfo.getSha1()) : null
); );
task.setCacheRepository(dependencyManager.getCacheRepository()); task.setCacheRepository(dependencyManager.getCacheRepository());

View File

@@ -20,11 +20,10 @@ package org.jackhuang.hmcl.download.game;
import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.CacheRepository; import org.jackhuang.hmcl.util.CacheRepository;
import java.io.File; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
@@ -54,12 +53,12 @@ public final class GameDownloadTask extends Task<Void> {
@Override @Override
public void execute() { public void execute() {
File jar = dependencyManager.getGameRepository().getVersionJar(version); Path jar = dependencyManager.getGameRepository().getVersionJar(version).toPath();
FileDownloadTask task = new FileDownloadTask( var task = new FileDownloadTask(
dependencyManager.getDownloadProvider().injectURLWithCandidates(version.getDownloadInfo().getUrl()), dependencyManager.getDownloadProvider().injectURLWithCandidates(version.getDownloadInfo().getUrl()),
jar, jar,
IntegrityCheck.of(CacheRepository.SHA1, version.getDownloadInfo().getSha1())); FileDownloadTask.IntegrityCheck.of(CacheRepository.SHA1, version.getDownloadInfo().getSha1()));
task.setCaching(true); task.setCaching(true);
task.setCacheRepository(dependencyManager.getCacheRepository()); task.setCacheRepository(dependencyManager.getCacheRepository());

View File

@@ -30,7 +30,7 @@ import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URI;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
@@ -116,8 +116,8 @@ public class LibraryDownloadTask extends Task<Void> {
} }
List<URL> urls = dependencyManager.getDownloadProvider().injectURLWithCandidates(url); List<URI> uris = dependencyManager.getDownloadProvider().injectURLWithCandidates(url);
task = new FileDownloadTask(urls, jar, task = new FileDownloadTask(uris, jar.toPath(),
library.getDownload().getSha1() != null ? new IntegrityCheck("SHA-1", library.getDownload().getSha1()) : null); library.getDownload().getSha1() != null ? new IntegrityCheck("SHA-1", library.getDownload().getSha1()) : null);
task.setCacheRepository(cacheRepository); task.setCacheRepository(cacheRepository);
task.setCaching(true); task.setCaching(true);

View File

@@ -103,7 +103,7 @@ public final class MojangJavaDownloadTask extends Task<MojangJavaDownloadTask.Re
if (file.getDownloads().containsKey("lzma")) { if (file.getDownloads().containsKey("lzma")) {
DownloadInfo download = file.getDownloads().get("lzma"); DownloadInfo download = file.getDownloads().get("lzma");
File tempFile = target.resolve(entry.getKey() + ".lzma").toFile(); File tempFile = target.resolve(entry.getKey() + ".lzma").toFile();
FileDownloadTask task = new FileDownloadTask(downloadProvider.injectURLWithCandidates(download.getUrl()), tempFile, new FileDownloadTask.IntegrityCheck("SHA-1", download.getSha1())); var task = new FileDownloadTask(downloadProvider.injectURLWithCandidates(download.getUrl()), tempFile.toPath(), new FileDownloadTask.IntegrityCheck("SHA-1", download.getSha1()));
task.setName(entry.getKey()); task.setName(entry.getKey());
dependencies.add(task.thenRunAsync(() -> { dependencies.add(task.thenRunAsync(() -> {
Path decompressed = target.resolve(entry.getKey() + ".tmp"); Path decompressed = target.resolve(entry.getKey() + ".tmp");
@@ -121,7 +121,7 @@ public final class MojangJavaDownloadTask extends Task<MojangJavaDownloadTask.Re
})); }));
} else if (file.getDownloads().containsKey("raw")) { } else if (file.getDownloads().containsKey("raw")) {
DownloadInfo download = file.getDownloads().get("raw"); DownloadInfo download = file.getDownloads().get("raw");
FileDownloadTask task = new FileDownloadTask(downloadProvider.injectURLWithCandidates(download.getUrl()), dest.toFile(), new FileDownloadTask.IntegrityCheck("SHA-1", download.getSha1())); var task = new FileDownloadTask(downloadProvider.injectURLWithCandidates(download.getUrl()), dest, new FileDownloadTask.IntegrityCheck("SHA-1", download.getSha1()));
task.setName(entry.getKey()); task.setName(entry.getKey());
if (file.isExecutable()) { if (file.isExecutable()) {
dependencies.add(task.thenRunAsync(() -> dest.toFile().setExecutable(true))); dependencies.add(task.thenRunAsync(() -> dest.toFile().setExecutable(true)));

View File

@@ -49,7 +49,7 @@ public final class NeoForgeInstallTask extends Task<Version> {
dependent = new FileDownloadTask( dependent = new FileDownloadTask(
dependencyManager.getDownloadProvider().injectURLsWithCandidates(remoteVersion.getUrls()), dependencyManager.getDownloadProvider().injectURLsWithCandidates(remoteVersion.getUrls()),
installer.toFile(), null installer, null
); );
dependent.setCacheRepository(dependencyManager.getCacheRepository()); dependent.setCacheRepository(dependencyManager.getCacheRepository());
dependent.setCaching(true); dependent.setCaching(true);

View File

@@ -26,7 +26,6 @@ import org.jackhuang.hmcl.download.game.GameLibrariesTask;
import org.jackhuang.hmcl.download.game.VersionJsonDownloadTask; import org.jackhuang.hmcl.download.game.VersionJsonDownloadTask;
import org.jackhuang.hmcl.game.*; import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.DigestUtils; import org.jackhuang.hmcl.util.DigestUtils;
import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.StringUtils;
@@ -44,7 +43,7 @@ import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URL; import java.net.URI;
import java.nio.file.FileSystem; import java.nio.file.FileSystem;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
@@ -342,12 +341,12 @@ public class NeoForgeOldInstallTask extends Task<Version> {
throw new Exception("client_mappings download info not found"); throw new Exception("client_mappings download info not found");
} }
List<URL> mappingsUrl = dependencyManager.getDownloadProvider() List<URI> mappingsUri = dependencyManager.getDownloadProvider()
.injectURLWithCandidates(mappings.getUrl()); .injectURLWithCandidates(mappings.getUrl());
FileDownloadTask mappingsTask = new FileDownloadTask( var mappingsTask = new FileDownloadTask(
mappingsUrl, mappingsUri,
new File(output), Path.of(output),
IntegrityCheck.of("SHA-1", mappings.getSha1())); FileDownloadTask.IntegrityCheck.of("SHA-1", mappings.getSha1()));
mappingsTask.setCaching(true); mappingsTask.setCaching(true);
mappingsTask.setCacheRepository(dependencyManager.getCacheRepository()); mappingsTask.setCacheRepository(dependencyManager.getCacheRepository());
return mappingsTask; return mappingsTask;

View File

@@ -96,9 +96,9 @@ public final class OptiFineInstallTask extends Task<Version> {
dest = Files.createTempFile("optifine-installer", ".jar"); dest = Files.createTempFile("optifine-installer", ".jar");
if (installer == null) { if (installer == null) {
FileDownloadTask task = new FileDownloadTask( var task = new FileDownloadTask(
dependencyManager.getDownloadProvider().injectURLsWithCandidates(remote.getUrls()), dependencyManager.getDownloadProvider().injectURLsWithCandidates(remote.getUrls()),
dest.toFile(), null); dest, null);
task.setCacheRepository(dependencyManager.getCacheRepository()); task.setCacheRepository(dependencyManager.getCacheRepository());
task.setCaching(true); task.setCaching(true);
dependents.add(task); dependents.add(task);

View File

@@ -23,7 +23,7 @@ import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URI;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
@@ -59,8 +59,8 @@ public final class QuiltAPIInstallTask extends Task<Version> {
@Override @Override
public void execute() throws IOException { public void execute() throws IOException {
dependencies.add(new FileDownloadTask( dependencies.add(new FileDownloadTask(
new URL(remote.getVersion().getFile().getUrl()), URI.create(remote.getVersion().getFile().getUrl()),
dependencyManager.getGameRepository().getRunDirectory(version.getId()).toPath().resolve("mods").resolve("quilt-api-" + remote.getVersion().getVersion() + ".jar").toFile(), dependencyManager.getGameRepository().getRunDirectory(version.getId()).toPath().resolve("mods").resolve("quilt-api-" + remote.getVersion().getVersion() + ".jar"),
remote.getVersion().getFile().getIntegrityCheck()) remote.getVersion().getFile().getIntegrityCheck())
); );
} }

View File

@@ -152,7 +152,7 @@ public final class CurseCompletionTask extends Task<Void> {
return Stream.empty(); return Stream.empty();
} }
FileDownloadTask task = new FileDownloadTask(f.getUrl(), path); var task = new FileDownloadTask(f.getUrl(), path.toPath());
task.setCacheRepository(dependency.getCacheRepository()); task.setCacheRepository(dependency.getCacheRepository());
task.setCaching(true); task.setCaching(true);
return Stream.of(task.withCounter("hmcl.modpack.download")); return Stream.of(task.withCounter("hmcl.modpack.download"));

View File

@@ -24,7 +24,7 @@ import org.jackhuang.hmcl.util.gson.Validation;
import org.jackhuang.hmcl.util.io.NetworkUtils; import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.net.URL; import java.net.URI;
import java.util.Objects; import java.util.Objects;
/** /**
@@ -84,15 +84,15 @@ public final class CurseManifestFile implements Validation {
} }
@Nullable @Nullable
public URL getUrl() { public URI getUrl() {
if (url == null) { if (url == null) {
if (fileName != null) { if (fileName != null) {
return NetworkUtils.toURL(NetworkUtils.encodeLocation(String.format("https://edge.forgecdn.net/files/%d/%d/%s", fileID / 1000, fileID % 1000, fileName))); return URI.create(NetworkUtils.encodeLocation(String.format("https://edge.forgecdn.net/files/%d/%d/%s", fileID / 1000, fileID % 1000, fileName)));
} else { } else {
return null; return null;
} }
} else { } else {
return NetworkUtils.toURL(NetworkUtils.encodeLocation(url)); return URI.create(NetworkUtils.encodeLocation(url));
} }
} }

View File

@@ -36,7 +36,7 @@ import org.jetbrains.annotations.Nullable;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URI;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.*; import java.util.*;
@@ -102,7 +102,7 @@ public class McbbsModpackCompletionTask extends CompletableFutureTask<Void> {
throw new CustomException(); throw new CustomException();
} }
})).thenComposeAsync(wrap(unused1 -> { })).thenComposeAsync(wrap(unused1 -> {
return executor.one(new GetTask(new URL(manifest.getFileApi() + "/manifest.json"))); return executor.one(new GetTask(URI.create(manifest.getFileApi() + "/manifest.json")));
})).thenComposeAsync(wrap(remoteManifestJson -> { })).thenComposeAsync(wrap(remoteManifestJson -> {
McbbsModpackManifest remoteManifest; McbbsModpackManifest remoteManifest;
// We needs to update modpack from online server. // We needs to update modpack from online server.
@@ -204,7 +204,7 @@ public class McbbsModpackCompletionTask extends CompletableFutureTask<Void> {
return file.withFileName(NetworkUtils.detectFileName(file.getUrl())); return file.withFileName(NetworkUtils.detectFileName(file.getUrl()));
} catch (IOException e) { } catch (IOException e) {
try { try {
String result = NetworkUtils.doGet(NetworkUtils.toURL(String.format("https://cursemeta.dries007.net/%d/%d.json", file.getProjectID(), file.getFileID()))); String result = NetworkUtils.doGet(URI.create(String.format("https://cursemeta.dries007.net/%d/%d.json", file.getProjectID(), file.getFileID())));
CurseMetaMod mod = JsonUtils.fromNonNullJson(result, CurseMetaMod.class); CurseMetaMod mod = JsonUtils.fromNonNullJson(result, CurseMetaMod.class);
return file.withFileName(mod.getFileNameOnDisk()).withURL(mod.getDownloadURL()); return file.withFileName(mod.getFileNameOnDisk()).withURL(mod.getDownloadURL());
} catch (FileNotFoundException fof) { } catch (FileNotFoundException fof) {
@@ -213,7 +213,7 @@ public class McbbsModpackCompletionTask extends CompletableFutureTask<Void> {
return file; return file;
} catch (IOException | JsonParseException e2) { } catch (IOException | JsonParseException e2) {
try { try {
String result = NetworkUtils.doGet(NetworkUtils.toURL(String.format("https://addons-ecs.forgesvc.net/api/v2/addon/%d/file/%d", file.getProjectID(), file.getFileID()))); String result = NetworkUtils.doGet(URI.create(String.format("https://addons-ecs.forgesvc.net/api/v2/addon/%d/file/%d", file.getProjectID(), file.getFileID())));
CurseMetaMod mod = JsonUtils.fromNonNullJson(result, CurseMetaMod.class); CurseMetaMod mod = JsonUtils.fromNonNullJson(result, CurseMetaMod.class);
return file.withFileName(mod.getFileName()).withURL(mod.getDownloadURL()); return file.withFileName(mod.getFileName()).withURL(mod.getDownloadURL());
} catch (FileNotFoundException fof) { } catch (FileNotFoundException fof) {
@@ -247,7 +247,7 @@ public class McbbsModpackCompletionTask extends CompletableFutureTask<Void> {
McbbsModpackManifest.CurseFile curseFile = (McbbsModpackManifest.CurseFile) file; McbbsModpackManifest.CurseFile curseFile = (McbbsModpackManifest.CurseFile) file;
if (StringUtils.isNotBlank(curseFile.getFileName())) { if (StringUtils.isNotBlank(curseFile.getFileName())) {
if (!modManager.hasSimpleMod(curseFile.getFileName())) { if (!modManager.hasSimpleMod(curseFile.getFileName())) {
FileDownloadTask task = new FileDownloadTask(curseFile.getUrl(), modManager.getSimpleModPath(curseFile.getFileName()).toFile()); var task = new FileDownloadTask(curseFile.getUrl(), modManager.getSimpleModPath(curseFile.getFileName()));
task.setCacheRepository(dependency.getCacheRepository()); task.setCacheRepository(dependency.getCacheRepository());
task.setCaching(true); task.setCaching(true);
dependencies.add(task.withCounter("hmcl.modpack.download")); dependencies.add(task.withCounter("hmcl.modpack.download"));
@@ -297,8 +297,8 @@ public class McbbsModpackCompletionTask extends CompletableFutureTask<Void> {
if (file instanceof McbbsModpackManifest.AddonFile) { if (file instanceof McbbsModpackManifest.AddonFile) {
McbbsModpackManifest.AddonFile addonFile = (McbbsModpackManifest.AddonFile) file; McbbsModpackManifest.AddonFile addonFile = (McbbsModpackManifest.AddonFile) file;
return new FileDownloadTask( return new FileDownloadTask(
new URL(remoteManifest.getFileApi() + "/overrides/" + NetworkUtils.encodeLocation(addonFile.getPath())), URI.create(remoteManifest.getFileApi() + "/overrides/" + NetworkUtils.encodeLocation(addonFile.getPath())),
modManager.getSimpleModPath(addonFile.getPath()).toFile(), modManager.getSimpleModPath(addonFile.getPath()),
addonFile.getHash() != null ? new FileDownloadTask.IntegrityCheck("SHA-1", addonFile.getHash()) : null); addonFile.getHash() != null ? new FileDownloadTask.IntegrityCheck("SHA-1", addonFile.getHash()) : null);
} else if (file instanceof McbbsModpackManifest.CurseFile) { } else if (file instanceof McbbsModpackManifest.CurseFile) {
// we download it later. // we download it later.

View File

@@ -31,7 +31,7 @@ import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URI;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@@ -329,9 +329,9 @@ public class McbbsModpackManifest implements ModpackManifest, Validation {
return fileName; return fileName;
} }
public URL getUrl() { public URI getUrl() {
return url == null ? NetworkUtils.toURL("https://www.curseforge.com/minecraft/mc-mods/" + projectID + "/download/" + fileID + "/file") return url == null ? URI.create("https://www.curseforge.com/minecraft/mc-mods/" + projectID + "/download/" + fileID + "/file")
: NetworkUtils.toURL(NetworkUtils.encodeLocation(url)); : URI.create(NetworkUtils.encodeLocation(url));
} }
public CurseFile withFileName(String fileName) { public CurseFile withFileName(String fileName) {

View File

@@ -121,7 +121,7 @@ public class ModrinthCompletionTask extends Task<Void> {
if (modsDirectory.equals(filePath.getParent()) && this.modManager.hasSimpleMod(FileUtils.getName(filePath))) if (modsDirectory.equals(filePath.getParent()) && this.modManager.hasSimpleMod(FileUtils.getName(filePath)))
continue; continue;
FileDownloadTask task = new FileDownloadTask(file.getDownloads(), filePath.toFile()); var task = new FileDownloadTask(file.getDownloads(), filePath);
task.setCacheRepository(dependency.getCacheRepository()); task.setCacheRepository(dependency.getCacheRepository());
task.setCaching(true); task.setCaching(true);
dependencies.add(task.withCounter("hmcl.modpack.download")); dependencies.add(task.withCounter("hmcl.modpack.download"));

View File

@@ -24,7 +24,7 @@ import org.jackhuang.hmcl.util.gson.TolerableValidationException;
import org.jackhuang.hmcl.util.gson.Validation; import org.jackhuang.hmcl.util.gson.Validation;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.net.URL; import java.net.URI;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
@@ -98,10 +98,10 @@ public class ModrinthManifest implements ModpackManifest, Validation {
private final String path; private final String path;
private final Map<String, String> hashes; private final Map<String, String> hashes;
private final Map<String, String> env; private final Map<String, String> env;
private final List<URL> downloads; private final List<URI> downloads;
private final int fileSize; private final int fileSize;
public File(String path, Map<String, String> hashes, Map<String, String> env, List<URL> downloads, int fileSize) { public File(String path, Map<String, String> hashes, Map<String, String> env, List<URI> downloads, int fileSize) {
this.path = path; this.path = path;
this.hashes = hashes; this.hashes = hashes;
this.env = env; this.env = env;
@@ -121,7 +121,7 @@ public class ModrinthManifest implements ModpackManifest, Validation {
return env; return env;
} }
public List<URL> getDownloads() { public List<URI> getDownloads() {
return downloads; return downloads;
} }

View File

@@ -1,9 +1,8 @@
package org.jackhuang.hmcl.mod.multimc; package org.jackhuang.hmcl.mod.multimc;
import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.net.URL; import java.net.URI;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -46,7 +45,7 @@ public final class MultiMCComponents {
return PAIRS; return PAIRS;
} }
public static URL getMetaURL(String componentID, String version) { public static URI getMetaURL(String componentID, String version) {
return NetworkUtils.toURL(String.format("https://meta.multimc.org/v1/%s/%s.json", componentID, version)); return URI.create(String.format("https://meta.multimc.org/v1/%s/%s.json", componentID, version));
} }
} }

View File

@@ -33,7 +33,7 @@ import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URI;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.*; import java.util.*;
@@ -85,7 +85,7 @@ public class ServerModpackCompletionTask extends Task<Void> {
@Override @Override
public void preExecute() throws Exception { public void preExecute() throws Exception {
if (manifest == null || StringUtils.isBlank(manifest.getManifest().getFileApi())) return; if (manifest == null || StringUtils.isBlank(manifest.getManifest().getFileApi())) return;
dependent = new GetTask(new URL(manifest.getManifest().getFileApi() + "/server-manifest.json")); dependent = new GetTask(URI.create(manifest.getManifest().getFileApi() + "/server-manifest.json"));
} }
@Override @Override
@@ -153,8 +153,8 @@ public class ServerModpackCompletionTask extends Task<Void> {
if (download) { if (download) {
total++; total++;
dependencies.add(new FileDownloadTask( dependencies.add(new FileDownloadTask(
new URL(remoteManifest.getFileApi() + "/overrides/" + NetworkUtils.encodeLocation(file.getPath())), URI.create(remoteManifest.getFileApi() + "/overrides/" + NetworkUtils.encodeLocation(file.getPath())),
actualPath.toFile(), actualPath,
new FileDownloadTask.IntegrityCheck("SHA-1", file.getHash())) new FileDownloadTask.IntegrityCheck("SHA-1", file.getHash()))
.withCounter("hmcl.modpack.download")); .withCounter("hmcl.modpack.download"));
} }

View File

@@ -20,21 +20,20 @@ package org.jackhuang.hmcl.task;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URI;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
public class DownloadException extends IOException { public class DownloadException extends IOException {
private final URL url; private final URI uri;
public DownloadException(URL url, @NotNull Throwable cause) { public DownloadException(URI uri, @NotNull Throwable cause) {
super("Unable to download " + url + ", " + cause.getMessage(), requireNonNull(cause)); super("Unable to download " + uri + ", " + cause.getMessage(), requireNonNull(cause));
this.uri = uri;
this.url = url;
} }
public URL getUrl() { public URI getUri() {
return url; return uri;
} }
} }

View File

@@ -24,36 +24,38 @@ import org.jackhuang.hmcl.util.ToStringBuilder;
import org.jackhuang.hmcl.util.io.IOUtils; import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils; import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.io.ResponseCodeException; import org.jackhuang.hmcl.util.io.ResponseCodeException;
import org.jetbrains.annotations.NotNull;
import java.io.Closeable; import java.io.Closeable;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URL; import java.net.URI;
import java.net.URLConnection; import java.net.URLConnection;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.*; import java.util.*;
import java.util.concurrent.*; import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.Lang.threadPool; import static org.jackhuang.hmcl.util.Lang.threadPool;
import static org.jackhuang.hmcl.util.logging.Logger.LOG; import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public abstract class FetchTask<T> extends Task<T> { public abstract class FetchTask<T> extends Task<T> {
protected final List<URL> urls; protected final List<URI> uris;
protected final int retry; protected final int retry;
protected boolean caching; protected boolean caching;
protected CacheRepository repository = CacheRepository.getInstance(); protected CacheRepository repository = CacheRepository.getInstance();
public FetchTask(List<URL> urls, int retry) { public FetchTask(@NotNull List<@NotNull URI> uris, int retry) {
Objects.requireNonNull(urls); Objects.requireNonNull(uris);
this.urls = urls.stream().filter(Objects::nonNull).collect(Collectors.toList()); this.uris = List.copyOf(uris);
this.retry = retry; this.retry = retry;
if (this.urls.isEmpty()) if (this.uris.isEmpty())
throw new IllegalArgumentException("At least one URL is required"); throw new IllegalArgumentException("At least one URL is required");
setExecutor(download()); setExecutor(download());
@@ -67,18 +69,19 @@ public abstract class FetchTask<T> extends Task<T> {
this.repository = repository; this.repository = repository;
} }
protected void beforeDownload(URL url) throws IOException {} protected void beforeDownload(URI uri) throws IOException {
}
protected abstract void useCachedResult(Path cachedFile) throws IOException; protected abstract void useCachedResult(Path cachedFile) throws IOException;
protected abstract EnumCheckETag shouldCheckETag(); protected abstract EnumCheckETag shouldCheckETag();
protected abstract Context getContext(URLConnection conn, boolean checkETag) throws IOException; protected abstract Context getContext(URLConnection connection, boolean checkETag) throws IOException;
@Override @Override
public void execute() throws Exception { public void execute() throws Exception {
Exception exception = null; Exception exception = null;
URL failedURL = null; URI failedURI = null;
boolean checkETag; boolean checkETag;
switch (shouldCheckETag()) { switch (shouldCheckETag()) {
case CHECK_E_TAG: checkETag = true; break; case CHECK_E_TAG: checkETag = true; break;
@@ -87,7 +90,7 @@ public abstract class FetchTask<T> extends Task<T> {
} }
int repeat = 0; int repeat = 0;
download: for (URL url : urls) { download: for (URI uri : uris) {
for (int retryTime = 0; retryTime < retry; retryTime++) { for (int retryTime = 0; retryTime < retry; retryTime++) {
if (isCancelled()) { if (isCancelled()) {
break download; break download;
@@ -95,11 +98,11 @@ public abstract class FetchTask<T> extends Task<T> {
List<String> redirects = null; List<String> redirects = null;
try { try {
beforeDownload(url); beforeDownload(uri);
updateProgress(0); updateProgress(0);
URLConnection conn = NetworkUtils.createConnection(url); URLConnection conn = NetworkUtils.createConnection(uri);
if (checkETag) repository.injectConnection(conn); if (checkETag) repository.injectConnection(conn);
if (conn instanceof HttpURLConnection) { if (conn instanceof HttpURLConnection) {
@@ -111,21 +114,21 @@ public abstract class FetchTask<T> extends Task<T> {
if (responseCode == HttpURLConnection.HTTP_NOT_MODIFIED) { if (responseCode == HttpURLConnection.HTTP_NOT_MODIFIED) {
// Handle cache // Handle cache
try { try {
Path cache = repository.getCachedRemoteFile(conn); Path cache = repository.getCachedRemoteFile(conn.getURL().toURI());
useCachedResult(cache); useCachedResult(cache);
return; return;
} catch (IOException e) { } catch (IOException e) {
LOG.warning("Unable to use cached file, redownload " + url, e); LOG.warning("Unable to use cached file, redownload " + uri, e);
repository.removeRemoteEntry(conn); repository.removeRemoteEntry(conn.getURL().toURI());
// Now we must reconnect the server since 304 may result in empty content, // Now we must reconnect the server since 304 may result in empty content,
// if we want to redownload the file, we must reconnect the server without etag settings. // if we want to redownload the file, we must reconnect the server without etag settings.
retryTime--; retryTime--;
continue; continue;
} }
} else if (responseCode / 100 == 4) { } else if (responseCode / 100 == 4) {
throw new FileNotFoundException(url.toString()); throw new FileNotFoundException(uri.toString());
} else if (responseCode / 100 != 2) { } else if (responseCode / 100 != 2) {
throw new ResponseCodeException(url, responseCode); throw new ResponseCodeException(uri, responseCode);
} }
} }
@@ -164,21 +167,21 @@ public abstract class FetchTask<T> extends Task<T> {
return; return;
} catch (FileNotFoundException ex) { } catch (FileNotFoundException ex) {
failedURL = url; failedURI = uri;
exception = ex; exception = ex;
LOG.warning("Failed to download " + url + ", not found" + ((redirects == null || redirects.isEmpty()) ? "" : ", redirects: " + redirects), ex); LOG.warning("Failed to download " + uri + ", not found" + ((redirects == null || redirects.isEmpty()) ? "" : ", redirects: " + redirects), ex);
break; // we will not try this URL again break; // we will not try this URL again
} catch (IOException ex) { } catch (IOException ex) {
failedURL = url; failedURI = uri;
exception = ex; exception = ex;
LOG.warning("Failed to download " + url + ", repeat times: " + (++repeat) + ((redirects == null || redirects.isEmpty()) ? "" : ", redirects: " + redirects), ex); LOG.warning("Failed to download " + uri + ", repeat times: " + (++repeat) + ((redirects == null || redirects.isEmpty()) ? "" : ", redirects: " + redirects), ex);
} }
} }
} }
if (exception != null) if (exception != null)
throw new DownloadException(failedURL, exception); throw new DownloadException(failedURI, exception);
} }
private static final Timer timer = new Timer("DownloadSpeedRecorder", true); private static final Timer timer = new Timer("DownloadSpeedRecorder", true);
@@ -209,6 +212,7 @@ public abstract class FetchTask<T> extends Task<T> {
/** /**
* Download speed in byte/sec. * Download speed in byte/sec.
*
* @return download speed * @return download speed
*/ */
public int getSpeed() { public int getSpeed() {

View File

@@ -22,16 +22,20 @@ import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.RandomAccessFile; import java.io.OutputStream;
import java.net.URL; import java.net.URI;
import java.net.URLConnection; import java.net.URLConnection;
import java.nio.file.FileSystem; import java.nio.file.FileSystem;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.util.*; import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
import static org.jackhuang.hmcl.util.DigestUtils.getDigest; import static org.jackhuang.hmcl.util.DigestUtils.getDigest;
@@ -78,73 +82,76 @@ public class FileDownloadTask extends FetchTask<Void> {
} }
} }
private final File file; private final Path file;
private final IntegrityCheck integrityCheck; private final IntegrityCheck integrityCheck;
private Path candidate; private Path candidate;
private final ArrayList<IntegrityCheckHandler> integrityCheckHandlers = new ArrayList<>(); private final ArrayList<IntegrityCheckHandler> integrityCheckHandlers = new ArrayList<>();
/** /**
* @param url the URL of remote file. * @param uri the URI of remote file.
* @param file the location that download to. * @param path the location that download to.
*/ */
public FileDownloadTask(URL url, File file) { public FileDownloadTask(URI uri, Path path) {
this(url, file, null); this(uri, path, null);
} }
/** /**
* @param url the URL of remote file. * @param uri the URI of remote file.
* @param file the location that download to. * @param path the location that download to.
* @param integrityCheck the integrity check to perform, null if no integrity check is to be performed * @param integrityCheck the integrity check to perform, null if no integrity check is to be performed
*/ */
public FileDownloadTask(URL url, File file, IntegrityCheck integrityCheck) { public FileDownloadTask(URI uri, Path path, IntegrityCheck integrityCheck) {
this(Collections.singletonList(url), file, integrityCheck); this(List.of(uri), path, integrityCheck);
} }
/** /**
* @param url the URL of remote file. * @param uri the URI of remote file.
* @param file the location that download to. * @param path the location that download to.
* @param integrityCheck the integrity check to perform, null if no integrity check is to be performed * @param integrityCheck the integrity check to perform, null if no integrity check is to be performed
* @param retry the times for retrying if downloading fails. * @param retry the times for retrying if downloading fails.
*/ */
public FileDownloadTask(URL url, File file, IntegrityCheck integrityCheck, int retry) { public FileDownloadTask(URI uri, Path path, IntegrityCheck integrityCheck, int retry) {
this(Collections.singletonList(url), file, integrityCheck, retry); this(List.of(uri), path, integrityCheck, retry);
} }
/** /**
* Constructor. * Constructor.
* @param urls urls of remote file, will be attempted in order. *
* @param uris uris of remote file, will be attempted in order.
* @param file the location that download to. * @param file the location that download to.
*/ */
public FileDownloadTask(List<URL> urls, File file) { public FileDownloadTask(List<URI> uris, Path file) {
this(urls, file, null); this(uris, file, null);
} }
/** /**
* Constructor. * Constructor.
* @param urls urls of remote file, will be attempted in order. *
* @param file the location that download to. * @param uris uris of remote file, will be attempted in order.
* @param path the location that download to.
* @param integrityCheck the integrity check to perform, null if no integrity check is to be performed * @param integrityCheck the integrity check to perform, null if no integrity check is to be performed
*/ */
public FileDownloadTask(List<URL> urls, File file, IntegrityCheck integrityCheck) { public FileDownloadTask(List<URI> uris, Path path, IntegrityCheck integrityCheck) {
this(urls, file, integrityCheck, 3); this(uris, path, integrityCheck, 3);
} }
/** /**
* Constructor. * Constructor.
* @param urls urls of remote file, will be attempted in order. *
* @param file the location that download to. * @param uris uris of remote file, will be attempted in order.
* @param path the location that download to.
* @param integrityCheck the integrity check to perform, null if no integrity check is to be performed * @param integrityCheck the integrity check to perform, null if no integrity check is to be performed
* @param retry the times for retrying if downloading fails. * @param retry the times for retrying if downloading fails.
*/ */
public FileDownloadTask(List<URL> urls, File file, IntegrityCheck integrityCheck, int retry) { public FileDownloadTask(List<URI> uris, Path path, IntegrityCheck integrityCheck, int retry) {
super(urls, retry); super(uris, retry);
this.file = file; this.file = path;
this.integrityCheck = integrityCheck; this.integrityCheck = integrityCheck;
setName(file.getName()); setName(path.getFileName().toString());
} }
public File getFile() { public Path getPath() {
return file; return file;
} }
@@ -164,8 +171,8 @@ public class FileDownloadTask extends FetchTask<Void> {
Optional<Path> cache = repository.checkExistentFile(candidate, integrityCheck.getAlgorithm(), integrityCheck.getChecksum()); Optional<Path> cache = repository.checkExistentFile(candidate, integrityCheck.getAlgorithm(), integrityCheck.getChecksum());
if (cache.isPresent()) { if (cache.isPresent()) {
try { try {
FileUtils.copyFile(cache.get().toFile(), file); FileUtils.copyFile(cache.get(), file);
LOG.trace("Successfully verified file " + file + " from " + urls.get(0)); LOG.trace("Successfully verified file " + file + " from " + uris.get(0));
return EnumCheckETag.CACHED; return EnumCheckETag.CACHED;
} catch (IOException e) { } catch (IOException e) {
LOG.warning("Failed to copy cache files", e); LOG.warning("Failed to copy cache files", e);
@@ -178,20 +185,20 @@ public class FileDownloadTask extends FetchTask<Void> {
} }
@Override @Override
protected void beforeDownload(URL url) { protected void beforeDownload(URI uri) {
LOG.trace("Downloading " + url + " to " + file); LOG.trace("Downloading " + uri + " to " + file);
} }
@Override @Override
protected void useCachedResult(Path cache) throws IOException { protected void useCachedResult(Path cache) throws IOException {
FileUtils.copyFile(cache.toFile(), file); FileUtils.copyFile(cache, file);
} }
@Override @Override
protected Context getContext(URLConnection conn, boolean checkETag) throws IOException { protected Context getContext(URLConnection connection, boolean checkETag) throws IOException {
Path temp = Files.createTempFile(null, null); Path temp = Files.createTempFile(null, null);
RandomAccessFile rFile = new RandomAccessFile(temp.toFile(), "rw");
MessageDigest digest = integrityCheck == null ? null : integrityCheck.createDigest(); MessageDigest digest = integrityCheck == null ? null : integrityCheck.createDigest();
OutputStream fileOutput = Files.newOutputStream(temp);
return new Context() { return new Context() {
@Override @Override
@@ -200,36 +207,34 @@ public class FileDownloadTask extends FetchTask<Void> {
digest.update(buffer, offset, len); digest.update(buffer, offset, len);
} }
rFile.write(buffer, offset, len); fileOutput.write(buffer, offset, len);
} }
@Override @Override
public void close() throws IOException { public void close() throws IOException {
try { try {
rFile.close(); fileOutput.close();
} catch (IOException e) { } catch (IOException e) {
LOG.warning("Failed to close file: " + rFile, e); LOG.warning("Failed to close file: " + temp, e);
} }
if (!isSuccess()) { if (!isSuccess()) {
try { try {
Files.delete(temp); Files.deleteIfExists(temp);
} catch (IOException e) { } catch (IOException e) {
LOG.warning("Failed to delete file: " + rFile, e); LOG.warning("Failed to delete file: " + temp, e);
} }
return; return;
} }
for (IntegrityCheckHandler handler : integrityCheckHandlers) { for (IntegrityCheckHandler handler : integrityCheckHandlers) {
handler.checkIntegrity(temp, file.toPath()); handler.checkIntegrity(temp, file);
} }
Files.deleteIfExists(file.toPath()); Files.createDirectories(file.toAbsolutePath().getParent());
if (!FileUtils.makeDirectory(file.getAbsoluteFile().getParentFile()))
throw new IOException("Unable to make parent directory " + file);
try { try {
FileUtils.moveFile(temp.toFile(), file); Files.move(temp, file, StandardCopyOption.REPLACE_EXISTING);
} catch (Exception e) { } catch (Exception e) {
throw new IOException("Unable to move temp file from " + temp + " to " + file, e); throw new IOException("Unable to move temp file from " + temp + " to " + file, e);
} }
@@ -241,14 +246,14 @@ public class FileDownloadTask extends FetchTask<Void> {
if (caching && integrityCheck != null) { if (caching && integrityCheck != null) {
try { try {
repository.cacheFile(file.toPath(), integrityCheck.getAlgorithm(), integrityCheck.getChecksum()); repository.cacheFile(file, integrityCheck.getAlgorithm(), integrityCheck.getChecksum());
} catch (IOException e) { } catch (IOException e) {
LOG.warning("Failed to cache file", e); LOG.warning("Failed to cache file", e);
} }
} }
if (checkETag) { if (checkETag) {
repository.cacheRemoteFile(file.toPath(), conn); repository.cacheRemoteFile(connection, file);
} }
} }
}; };
@@ -257,7 +262,8 @@ public class FileDownloadTask extends FetchTask<Void> {
public interface IntegrityCheckHandler { public interface IntegrityCheckHandler {
/** /**
* Check whether the file is corrupted or not. * Check whether the file is corrupted or not.
* @param filePath the file locates in (maybe in temp directory) *
* @param filePath the file locates in (maybe in temp directory)
* @param destinationPath for real file name * @param destinationPath for real file name
* @throws IOException if the file is corrupted * @throws IOException if the file is corrupted
*/ */

View File

@@ -19,12 +19,12 @@ package org.jackhuang.hmcl.task;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URI;
import java.net.URLConnection; import java.net.URLConnection;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.*; import java.util.List;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
@@ -34,25 +34,27 @@ import static java.nio.charset.StandardCharsets.UTF_8;
*/ */
public final class GetTask extends FetchTask<String> { public final class GetTask extends FetchTask<String> {
private static final int DEFAULT_RETRY = 3;
private final Charset charset; private final Charset charset;
public GetTask(URL url) { public GetTask(URI url) {
this(url, UTF_8); this(url, UTF_8);
} }
public GetTask(URL url, Charset charset) { public GetTask(URI url, Charset charset) {
this(url, charset, 3); this(url, charset, DEFAULT_RETRY);
} }
public GetTask(URL url, Charset charset, int retry) { public GetTask(URI url, Charset charset, int retry) {
this(Collections.singletonList(url), charset, retry); this(List.of(url), charset, retry);
} }
public GetTask(List<URL> url) { public GetTask(List<URI> url) {
this(url, UTF_8, 3); this(url, UTF_8, DEFAULT_RETRY);
} }
public GetTask(List<URL> urls, Charset charset, int retry) { public GetTask(List<URI> urls, Charset charset, int retry) {
super(urls, retry); super(urls, retry);
this.charset = charset; this.charset = charset;
@@ -70,7 +72,7 @@ public final class GetTask extends FetchTask<String> {
} }
@Override @Override
protected Context getContext(URLConnection conn, boolean checkETag) { protected Context getContext(URLConnection connection, boolean checkETag) {
return new Context() { return new Context() {
final ByteArrayOutputStream baos = new ByteArrayOutputStream(); final ByteArrayOutputStream baos = new ByteArrayOutputStream();
@@ -87,7 +89,7 @@ public final class GetTask extends FetchTask<String> {
setResult(result); setResult(result);
if (checkETag) { if (checkETag) {
repository.cacheText(result, conn); repository.cacheText(connection, result);
} }
} }
}; };

View File

@@ -24,6 +24,7 @@ import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.net.URI;
import java.net.URLConnection; import java.net.URLConnection;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.channels.Channels; import java.nio.channels.Channels;
@@ -157,12 +158,11 @@ public class CacheRepository {
return cache; return cache;
} }
public Path getCachedRemoteFile(URLConnection conn) throws IOException { public Path getCachedRemoteFile(URI uri) throws IOException {
String url = conn.getURL().toString();
lock.readLock().lock(); lock.readLock().lock();
ETagItem eTagItem; ETagItem eTagItem;
try { try {
eTagItem = index.get(url); eTagItem = index.get(uri.toString());
} finally { } finally {
lock.readLock().unlock(); lock.readLock().unlock();
} }
@@ -177,11 +177,10 @@ public class CacheRepository {
return file; return file;
} }
public void removeRemoteEntry(URLConnection conn) { public void removeRemoteEntry(URI uri) {
String url = conn.getURL().toString();
lock.readLock().lock(); lock.readLock().lock();
try { try {
index.remove(url); index.remove(uri.toString());
} finally { } finally {
lock.readLock().unlock(); lock.readLock().unlock();
} }
@@ -203,34 +202,34 @@ public class CacheRepository {
// conn.setRequestProperty("If-Modified-Since", eTagItem.getRemoteLastModified()); // conn.setRequestProperty("If-Modified-Since", eTagItem.getRemoteLastModified());
} }
public void cacheRemoteFile(Path downloaded, URLConnection conn) throws IOException { public void cacheRemoteFile(URLConnection connection, Path downloaded) throws IOException {
cacheData(() -> { cacheData(connection, () -> {
String hash = DigestUtils.digestToString(SHA1, downloaded); String hash = DigestUtils.digestToString(SHA1, downloaded);
Path cached = cacheFile(downloaded, SHA1, hash); Path cached = cacheFile(downloaded, SHA1, hash);
return new CacheResult(hash, cached); return new CacheResult(hash, cached);
}, conn); });
} }
public void cacheText(String text, URLConnection conn) throws IOException { public void cacheText(URLConnection connection, String text) throws IOException {
cacheBytes(text.getBytes(UTF_8), conn); cacheBytes(connection, text.getBytes(UTF_8));
} }
public void cacheBytes(byte[] bytes, URLConnection conn) throws IOException { public void cacheBytes(URLConnection connection, byte[] bytes) throws IOException {
cacheData(() -> { cacheData(connection, () -> {
String hash = DigestUtils.digestToString(SHA1, bytes); String hash = DigestUtils.digestToString(SHA1, bytes);
Path cached = getFile(SHA1, hash); Path cached = getFile(SHA1, hash);
FileUtils.writeBytes(cached, bytes); FileUtils.writeBytes(cached, bytes);
return new CacheResult(hash, cached); return new CacheResult(hash, cached);
}, conn); });
} }
public synchronized void cacheData(ExceptionalSupplier<CacheResult, IOException> cacheSupplier, URLConnection conn) throws IOException { private void cacheData(URLConnection connection, ExceptionalSupplier<CacheResult, IOException> cacheSupplier) throws IOException {
String eTag = conn.getHeaderField("ETag"); String eTag = connection.getHeaderField("ETag");
if (eTag == null) return; if (eTag == null || eTag.isEmpty()) return;
String url = conn.getURL().toString(); String uri = connection.getURL().toString();
String lastModified = conn.getHeaderField("Last-Modified"); String lastModified = connection.getHeaderField("Last-Modified");
CacheResult cacheResult = cacheSupplier.get(); CacheResult cacheResult = cacheSupplier.get();
ETagItem eTagItem = new ETagItem(url, eTag, cacheResult.hash, Files.getLastModifiedTime(cacheResult.cachedFile).toMillis(), lastModified); ETagItem eTagItem = new ETagItem(uri, eTag, cacheResult.hash, Files.getLastModifiedTime(cacheResult.cachedFile).toMillis(), lastModified);
Lock writeLock = lock.writeLock(); Lock writeLock = lock.writeLock();
writeLock.lock(); writeLock.lock();
try { try {

View File

@@ -0,0 +1,56 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util.io;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLConnection;
import java.util.zip.GZIPInputStream;
/**
* @author Glavo
*/
public enum ContentEncoding {
NONE {
@Override
public InputStream wrap(InputStream inputStream) {
return inputStream;
}
},
GZIP {
@Override
public InputStream wrap(InputStream inputStream) throws IOException {
return new GZIPInputStream(inputStream);
}
};
public static @NotNull ContentEncoding fromConnection(URLConnection connection) throws IOException {
String encoding = connection.getContentEncoding();
if (encoding == null || encoding.isEmpty()) {
return NONE;
} else if ("gzip".equalsIgnoreCase(encoding)) {
return GZIP;
} else {
throw new IOException("Unsupported content encoding: " + encoding);
}
}
public abstract InputStream wrap(InputStream inputStream) throws IOException;
}

View File

@@ -29,6 +29,7 @@ import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL; import java.net.URL;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
@@ -113,18 +114,13 @@ public abstract class HttpRequest {
return getStringAsync().thenApplyAsync(jsonString -> JsonUtils.fromNonNullJson(jsonString, type)); return getStringAsync().thenApplyAsync(jsonString -> JsonUtils.fromNonNullJson(jsonString, type));
} }
public HttpRequest filter(ExceptionalBiConsumer<URL, Integer, IOException> responseCodeTester) {
this.responseCodeTester = responseCodeTester;
return this;
}
public HttpRequest ignoreHttpErrorCode(int code) { public HttpRequest ignoreHttpErrorCode(int code) {
toleratedHttpCodes.add(code); toleratedHttpCodes.add(code);
return this; return this;
} }
public HttpURLConnection createConnection() throws IOException { public HttpURLConnection createConnection() throws IOException {
HttpURLConnection con = createHttpConnection(new URL(url)); HttpURLConnection con = createHttpConnection(URI.create(url));
con.setRequestMethod(method); con.setRequestMethod(method);
for (Map.Entry<String, String> entry : headers.entrySet()) { for (Map.Entry<String, String> entry : headers.entrySet()) {
con.setRequestProperty(entry.getKey(), entry.getValue()); con.setRequestProperty(entry.getKey(), entry.getValue());
@@ -133,7 +129,7 @@ public abstract class HttpRequest {
} }
public static class HttpGetRequest extends HttpRequest { public static class HttpGetRequest extends HttpRequest {
public HttpGetRequest(String url) { protected HttpGetRequest(String url) {
super(url, "GET"); super(url, "GET");
} }
@@ -149,7 +145,7 @@ public abstract class HttpRequest {
public static final class HttpPostRequest extends HttpRequest { public static final class HttpPostRequest extends HttpRequest {
private byte[] bytes; private byte[] bytes;
public HttpPostRequest(String url) { private HttpPostRequest(String url) {
super(url, "POST"); super(url, "POST");
} }
@@ -189,21 +185,17 @@ public abstract class HttpRequest {
URL url = new URL(this.url); URL url = new URL(this.url);
if (responseCodeTester != null) { if (con.getResponseCode() / 100 != 2) {
responseCodeTester.accept(url, con.getResponseCode()); if (!ignoreHttpCode && !toleratedHttpCodes.contains(con.getResponseCode())) {
} else { try {
if (con.getResponseCode() / 100 != 2) { throw new ResponseCodeException(NetworkUtils.toURI(url), con.getResponseCode(), NetworkUtils.readFullyAsString(con));
if (!ignoreHttpCode && !toleratedHttpCodes.contains(con.getResponseCode())) { } catch (IOException e) {
try { throw new ResponseCodeException(NetworkUtils.toURI(url), con.getResponseCode(), e);
throw new ResponseCodeException(url, con.getResponseCode(), NetworkUtils.readData(con));
} catch (IOException e) {
throw new ResponseCodeException(url, con.getResponseCode(), e);
}
} }
} }
} }
return NetworkUtils.readData(con); return NetworkUtils.readFullyAsString(con);
}, retryTimes); }, retryTimes);
} }
} }

View File

@@ -21,12 +21,16 @@ import org.jackhuang.hmcl.util.Pair;
import java.io.*; import java.io.*;
import java.net.*; import java.net.*;
import java.nio.charset.Charset;
import java.util.*; import java.util.*;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
import static org.jackhuang.hmcl.util.Pair.pair; import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.StringUtils.*; import static org.jackhuang.hmcl.util.StringUtils.*;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/** /**
* @author huangyuhui * @author huangyuhui
@@ -39,6 +43,10 @@ public final class NetworkUtils {
private NetworkUtils() { private NetworkUtils() {
} }
public static boolean isHttpUri(URI uri) {
return "http".equals(uri.getScheme()) || "https".equals(uri.getScheme());
}
public static String withQuery(String baseUrl, Map<String, String> params) { public static String withQuery(String baseUrl, Map<String, String> params) {
StringBuilder sb = new StringBuilder(baseUrl); StringBuilder sb = new StringBuilder(baseUrl);
boolean first = true; boolean first = true;
@@ -73,7 +81,7 @@ public final class NetworkUtils {
scanner.useDelimiter("&"); scanner.useDelimiter("&");
while (scanner.hasNext()) { while (scanner.hasNext()) {
String[] nameValue = scanner.next().split(NAME_VALUE_SEPARATOR); String[] nameValue = scanner.next().split(NAME_VALUE_SEPARATOR);
if (nameValue.length <= 0 || nameValue.length > 2) { if (nameValue.length == 0 || nameValue.length > 2) {
throw new IllegalArgumentException("bad query string"); throw new IllegalArgumentException("bad query string");
} }
@@ -85,8 +93,8 @@ public final class NetworkUtils {
return result; return result;
} }
public static URLConnection createConnection(URL url) throws IOException { public static URLConnection createConnection(URI uri) throws IOException {
URLConnection connection = url.openConnection(); URLConnection connection = uri.toURL().openConnection();
connection.setUseCaches(false); connection.setUseCaches(false);
connection.setConnectTimeout(TIME_OUT); connection.setConnectTimeout(TIME_OUT);
connection.setReadTimeout(TIME_OUT); connection.setReadTimeout(TIME_OUT);
@@ -94,15 +102,15 @@ public final class NetworkUtils {
return connection; return connection;
} }
public static HttpURLConnection createHttpConnection(URL url) throws IOException { public static HttpURLConnection createHttpConnection(URI url) throws IOException {
return (HttpURLConnection) createConnection(url); return (HttpURLConnection) createConnection(url);
} }
/** /**
* @see <a href=
* "https://github.com/curl/curl/blob/3f7b1bb89f92c13e69ee51b710ac54f775aab320/lib/transfer.c#L1427-L1461">Curl</a>
* @param location the url to be URL encoded * @param location the url to be URL encoded
* @return encoded URL * @return encoded URL
* @see <a href=
* "https://github.com/curl/curl/blob/3f7b1bb89f92c13e69ee51b710ac54f775aab320/lib/transfer.c#L1427-L1461">Curl</a>
*/ */
public static String encodeLocation(String location) { public static String encodeLocation(String location) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
@@ -138,10 +146,10 @@ public final class NetworkUtils {
* This method is a work-around that aims to solve problem when "Location" in * This method is a work-around that aims to solve problem when "Location" in
* stupid server's response is not encoded. * stupid server's response is not encoded.
* *
* @see <a href="https://github.com/curl/curl/issues/473">Issue with libcurl</a>
* @param conn the stupid http connection. * @param conn the stupid http connection.
* @return manually redirected http connection. * @return manually redirected http connection.
* @throws IOException if an I/O error occurs. * @throws IOException if an I/O error occurs.
* @see <a href="https://github.com/curl/curl/issues/473">Issue with libcurl</a>
*/ */
public static HttpURLConnection resolveConnection(HttpURLConnection conn, List<String> redirects) throws IOException { public static HttpURLConnection resolveConnection(HttpURLConnection conn, List<String> redirects) throws IOException {
int redirect = 0; int redirect = 0;
@@ -153,7 +161,7 @@ public final class NetworkUtils {
Map<String, List<String>> properties = conn.getRequestProperties(); Map<String, List<String>> properties = conn.getRequestProperties();
String method = conn.getRequestMethod(); String method = conn.getRequestMethod();
int code = conn.getResponseCode(); int code = conn.getResponseCode();
if (code >= 300 && code <= 307 && code != 306 && code != 304) { if (code >= 300 && code <= 308 && code != 306 && code != 304) {
String newURL = conn.getHeaderField("Location"); String newURL = conn.getHeaderField("Location");
conn.disconnect(); conn.disconnect();
@@ -178,19 +186,15 @@ public final class NetworkUtils {
return conn; return conn;
} }
public static String doGet(URL url) throws IOException { public static String doGet(URI uri) throws IOException {
HttpURLConnection con = createHttpConnection(url); return readFullyAsString(resolveConnection(createHttpConnection(uri)));
con = resolveConnection(con);
return IOUtils.readFullyAsString(con.getInputStream());
} }
public static String doGet(List<URL> urls) throws IOException { public static String doGet(List<URI> uris) throws IOException {
List<IOException> exceptions = null; List<IOException> exceptions = null;
for (URL url : urls) { for (URI uri : uris) {
try { try {
HttpURLConnection con = createHttpConnection(url); return doGet(uri);
con = resolveConnection(con);
return IOUtils.readFullyAsString(con.getInputStream());
} catch (IOException e) { } catch (IOException e) {
if (exceptions == null) { if (exceptions == null) {
exceptions = new ArrayList<>(1); exceptions = new ArrayList<>(1);
@@ -212,7 +216,11 @@ public final class NetworkUtils {
} }
} }
public static String doPost(URL u, Map<String, String> params) throws IOException { public static String doPost(URI uri, String post) throws IOException {
return doPost(uri, post, "application/x-www-form-urlencoded");
}
public static String doPost(URI u, Map<String, String> params) throws IOException {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
if (params != null) { if (params != null) {
for (Map.Entry<String, String> e : params.entrySet()) for (Map.Entry<String, String> e : params.entrySet())
@@ -222,50 +230,72 @@ public final class NetworkUtils {
return doPost(u, sb.toString()); return doPost(u, sb.toString());
} }
public static String doPost(URL u, String post) throws IOException { public static String doPost(URI uri, String post, String contentType) throws IOException {
return doPost(u, post, "application/x-www-form-urlencoded");
}
public static String doPost(URL url, String post, String contentType) throws IOException {
byte[] bytes = post.getBytes(UTF_8); byte[] bytes = post.getBytes(UTF_8);
HttpURLConnection con = createHttpConnection(url); HttpURLConnection con = createHttpConnection(uri);
con.setRequestMethod("POST"); con.setRequestMethod("POST");
con.setDoOutput(true); con.setDoOutput(true);
con.setRequestProperty("Content-Type", contentType + "; charset=utf-8"); con.setRequestProperty("Content-Type", contentType + "; charset=utf-8");
con.setRequestProperty("Content-Length", "" + bytes.length); con.setRequestProperty("Content-Length", String.valueOf(bytes.length));
try (OutputStream os = con.getOutputStream()) { try (OutputStream os = con.getOutputStream()) {
os.write(bytes); os.write(bytes);
} }
return readData(con); return readFullyAsString(con);
} }
public static String readData(HttpURLConnection con) throws IOException { static final Pattern CHARSET_REGEX = Pattern.compile("\\s*(charset)\\s*=\\s*['|\"]?(?<charset>[^\"^';,]+)['|\"]?");
try {
try (InputStream stdout = con.getInputStream()) { static Charset getCharsetFromContentType(String contentType) {
return IOUtils.readFullyAsString("gzip".equals(con.getContentEncoding()) ? IOUtils.wrapFromGZip(stdout) : stdout); if (contentType == null || contentType.isBlank())
return UTF_8;
Matcher matcher = CHARSET_REGEX.matcher(contentType);
if (matcher.find()) {
String charsetName = matcher.group("charset");
try {
return Charset.forName(charsetName);
} catch (Throwable e) {
// Ignore invalid charset
LOG.warning("Bad charset name: " + charsetName + ", using UTF-8 instead", e);
} }
} catch (IOException e) { }
try (InputStream stderr = con.getErrorStream()) { return UTF_8;
if (stderr == null) }
public static String readFullyAsString(URLConnection con) throws IOException {
try {
var contentEncoding = ContentEncoding.fromConnection(con);
Charset charset = getCharsetFromContentType(con.getHeaderField("Content-Type"));
try (InputStream stdout = con.getInputStream()) {
return IOUtils.readFullyAsString(contentEncoding.wrap(stdout), charset);
} catch (IOException e) {
if (con instanceof HttpURLConnection) {
try (InputStream stderr = ((HttpURLConnection) con).getErrorStream()) {
if (stderr == null)
throw e;
return IOUtils.readFullyAsString(contentEncoding.wrap(stderr), charset);
}
} else {
throw e; throw e;
return IOUtils.readFullyAsString("gzip".equals(con.getContentEncoding()) ? IOUtils.wrapFromGZip(stderr) : stderr); }
}
} finally {
if (con instanceof HttpURLConnection) {
((HttpURLConnection) con).disconnect();
} }
} }
} }
public static String detectFileName(URL url) throws IOException { public static String detectFileName(URI uri) throws IOException {
HttpURLConnection conn = resolveConnection(createHttpConnection(url)); HttpURLConnection conn = resolveConnection(createHttpConnection(uri));
int code = conn.getResponseCode(); int code = conn.getResponseCode();
if (code / 100 == 4) if (code / 100 == 4)
throw new FileNotFoundException(); throw new FileNotFoundException();
if (code / 100 != 2) if (code / 100 != 2)
throw new IOException(url + ": response code " + conn.getResponseCode()); throw new ResponseCodeException(uri, conn.getResponseCode());
return detectFileName(conn);
}
public static String detectFileName(HttpURLConnection conn) {
String disposition = conn.getHeaderField("Content-Disposition"); String disposition = conn.getHeaderField("Content-Disposition");
if (disposition == null || !disposition.contains("filename=")) { if (disposition == null || !disposition.contains("filename=")) {
String u = conn.getURL().toString(); String u = conn.getURL().toString();
@@ -275,46 +305,22 @@ public final class NetworkUtils {
} }
} }
public static URL toURL(String str) { public static URI toURI(URL url) {
try { try {
return new URL(str); return url.toURI();
} catch (MalformedURLException e) { } catch (URISyntaxException e) {
throw new IllegalArgumentException(e); throw new IllegalArgumentException(e);
} }
} }
public static boolean isURL(String str) {
try {
new URL(str);
return true;
} catch (MalformedURLException e) {
return false;
}
}
public static boolean urlExists(URL url) throws IOException {
HttpURLConnection con = createHttpConnection(url);
con = resolveConnection(con);
int responseCode = con.getResponseCode();
con.disconnect();
return responseCode / 100 == 2;
}
// ==== Shortcut methods for encoding/decoding URLs in UTF-8 ==== // ==== Shortcut methods for encoding/decoding URLs in UTF-8 ====
public static String encodeURL(String toEncode) { public static String encodeURL(String toEncode) {
try { return URLEncoder.encode(toEncode, UTF_8);
return URLEncoder.encode(toEncode, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new Error();
}
} }
public static String decodeURL(String toDecode) { public static String decodeURL(String toDecode) {
try { return URLDecoder.decode(toDecode, UTF_8);
return URLDecoder.decode(toDecode, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new Error();
}
} }
// ==== // ====
} }

View File

@@ -18,37 +18,37 @@
package org.jackhuang.hmcl.util.io; package org.jackhuang.hmcl.util.io;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URI;
public final class ResponseCodeException extends IOException { public final class ResponseCodeException extends IOException {
private final URL url; private final URI uri;
private final int responseCode; private final int responseCode;
private final String data; private final String data;
public ResponseCodeException(URL url, int responseCode) { public ResponseCodeException(URI uri, int responseCode) {
super("Unable to request url " + url + ", response code: " + responseCode); super("Unable to request url " + uri + ", response code: " + responseCode);
this.url = url; this.uri = uri;
this.responseCode = responseCode; this.responseCode = responseCode;
this.data = null; this.data = null;
} }
public ResponseCodeException(URL url, int responseCode, Throwable cause) { public ResponseCodeException(URI uri, int responseCode, Throwable cause) {
super("Unable to request url " + url + ", response code: " + responseCode, cause); super("Unable to request url " + uri + ", response code: " + responseCode, cause);
this.url = url; this.uri = uri;
this.responseCode = responseCode; this.responseCode = responseCode;
this.data = null; this.data = null;
} }
public ResponseCodeException(URL url, int responseCode, String data) { public ResponseCodeException(URI uri, int responseCode, String data) {
super("Unable to request url " + url + ", response code: " + responseCode + ", data: " + data); super("Unable to request url " + uri + ", response code: " + responseCode + ", data: " + data);
this.url = url; this.uri = uri;
this.responseCode = responseCode; this.responseCode = responseCode;
this.data = data; this.data = data;
} }
public URL getUrl() { public URI getUri() {
return url; return uri;
} }
public int getResponseCode() { public int getResponseCode() {

View File

@@ -0,0 +1,38 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util.io;
import org.junit.jupiter.api.Test;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* @author Glavo
*/
public class NetworkUtilsTest {
@Test
public void testGetEncodingFromUrl() {
assertEquals(UTF_8, NetworkUtils.getCharsetFromContentType(null));
assertEquals(UTF_8, NetworkUtils.getCharsetFromContentType(""));
assertEquals(UTF_8, NetworkUtils.getCharsetFromContentType("text/html"));
assertEquals(UTF_8, NetworkUtils.getCharsetFromContentType("text/html; charset=utf-8"));
assertEquals(US_ASCII, NetworkUtils.getCharsetFromContentType("text/html; charset=ascii"));
}
}