将代码中的 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

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

View File

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

View File

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

View File

@@ -34,7 +34,8 @@ import org.jackhuang.hmcl.util.javafx.ObservableOptionalCache;
import java.io.IOException;
import java.io.InputStream;
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.Path;
import java.util.*;
@@ -164,7 +165,7 @@ public class MicrosoftService {
.accept("application/json").createConnection();
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
@@ -247,22 +248,22 @@ public class MicrosoftService {
if (responseCode == HTTP_NOT_FOUND) {
throw new NoMinecraftJavaEditionProfileException();
} 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);
}
public Optional<CompleteGameProfile> getCompleteGameProfile(UUID uuid) throws AuthenticationException {
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 {
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.setRequestProperty("Authorization", "Bearer " + accessToken);
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 (con.getResponseCode() / 100 != 2)
throw new ResponseCodeException(con.getURL(), con.getResponseCode());
throw new ResponseCodeException(con.getURL().toURI(), con.getResponseCode());
} else {
MinecraftErrorResponse profileResponse = GSON.fromJson(response, MinecraftErrorResponse.class);
if (StringUtils.isNotBlank(profileResponse.errorMessage) || con.getResponseCode() / 100 != 2)
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);
}
}
private static String request(URL url, Object payload) throws AuthenticationException {
private static String request(URI url, Object payload) throws AuthenticationException {
try {
if (payload == null)
return NetworkUtils.doGet(url);

View File

@@ -33,14 +33,11 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URI;
import java.net.URLConnection;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.*;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Lang.tryCast;
@@ -169,7 +166,7 @@ public class Skin {
String realCslApi = type == Type.LITTLE_SKIN
? "https://littleskin.cn/csl"
: 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 -> {
SkinJson result = JsonUtils.GSON.fromJson(json, SkinJson.class);
@@ -179,8 +176,8 @@ public class Skin {
return Task.allOf(
Task.supplyAsync(result::getModel),
result.getHash() == null ? Task.supplyAsync(() -> null) : new FetchBytesTask(new URL(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.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(URI.create(String.format("%s/textures/%s", realCslApi, result.getCapeHash())), 3)
);
}).thenApplyAsync(result -> {
if (result == null) {
@@ -232,8 +229,8 @@ public class Skin {
private static class FetchBytesTask extends FetchTask<InputStream> {
public FetchBytesTask(URL url, int retry) {
super(Collections.singletonList(url), retry);
public FetchBytesTask(URI uri, int retry) {
super(List.of(uri), retry);
}
@Override
@@ -247,7 +244,7 @@ public class Skin {
}
@Override
protected Context getContext(URLConnection conn, boolean checkETag) throws IOException {
protected Context getContext(URLConnection connection, boolean checkETag) throws IOException {
return new Context() {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
@@ -263,7 +260,7 @@ public class Skin {
setResult(new ByteArrayInputStream(baos.toByteArray()));
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 java.net.URL;
import java.net.URI;
import java.util.UUID;
/**
@@ -27,13 +27,13 @@ import java.util.UUID;
*/
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.
@@ -51,8 +51,8 @@ public interface YggdrasilProvider {
* @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.
*/
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.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
@@ -160,7 +160,7 @@ public class YggdrasilService {
request.file("file", FileUtils.getName(file), "image/" + FileUtils.getExtension(file), fis);
}
}
requireEmpty(NetworkUtils.readData(con));
requireEmpty(NetworkUtils.readFullyAsString(con));
} catch (IOException 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 {
if (payload == null)
return NetworkUtils.doGet(url);
return NetworkUtils.doGet(uri);
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) {
throw new ServerDisconnectException(e);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -29,7 +29,7 @@ import org.jackhuang.hmcl.util.CacheRepository;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import java.io.IOException;
import java.net.URL;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
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);
}
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.setCandidate(dependencyManager.getCacheRepository().getCommonDirectory()
.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
// 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()),
assetIndexFile.toFile(),
assetIndexFile,
verifyHashCode ? new FileDownloadTask.IntegrityCheck("SHA-1", assetIndexInfo.getSha1()) : null
);
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.game.Version;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.CacheRepository;
import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@@ -54,12 +53,12 @@ public final class GameDownloadTask extends Task<Void> {
@Override
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()),
jar,
IntegrityCheck.of(CacheRepository.SHA1, version.getDownloadInfo().getSha1()));
FileDownloadTask.IntegrityCheck.of(CacheRepository.SHA1, version.getDownloadInfo().getSha1()));
task.setCaching(true);
task.setCacheRepository(dependencyManager.getCacheRepository());

View File

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

View File

@@ -103,7 +103,7 @@ public final class MojangJavaDownloadTask extends Task<MojangJavaDownloadTask.Re
if (file.getDownloads().containsKey("lzma")) {
DownloadInfo download = file.getDownloads().get("lzma");
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());
dependencies.add(task.thenRunAsync(() -> {
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")) {
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());
if (file.isExecutable()) {
dependencies.add(task.thenRunAsync(() -> dest.toFile().setExecutable(true)));

View File

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

View File

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

View File

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

View File

@@ -152,7 +152,7 @@ public final class CurseCompletionTask extends Task<Void> {
return Stream.empty();
}
FileDownloadTask task = new FileDownloadTask(f.getUrl(), path);
var task = new FileDownloadTask(f.getUrl(), path.toPath());
task.setCacheRepository(dependency.getCacheRepository());
task.setCaching(true);
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.jetbrains.annotations.Nullable;
import java.net.URL;
import java.net.URI;
import java.util.Objects;
/**
@@ -84,15 +84,15 @@ public final class CurseManifestFile implements Validation {
}
@Nullable
public URL getUrl() {
public URI getUrl() {
if (url == 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 {
return null;
}
} 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.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
@@ -102,7 +102,7 @@ public class McbbsModpackCompletionTask extends CompletableFutureTask<Void> {
throw new CustomException();
}
})).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 -> {
McbbsModpackManifest remoteManifest;
// 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()));
} catch (IOException e) {
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);
return file.withFileName(mod.getFileNameOnDisk()).withURL(mod.getDownloadURL());
} catch (FileNotFoundException fof) {
@@ -213,7 +213,7 @@ public class McbbsModpackCompletionTask extends CompletableFutureTask<Void> {
return file;
} catch (IOException | JsonParseException e2) {
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);
return file.withFileName(mod.getFileName()).withURL(mod.getDownloadURL());
} catch (FileNotFoundException fof) {
@@ -247,7 +247,7 @@ public class McbbsModpackCompletionTask extends CompletableFutureTask<Void> {
McbbsModpackManifest.CurseFile curseFile = (McbbsModpackManifest.CurseFile) file;
if (StringUtils.isNotBlank(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.setCaching(true);
dependencies.add(task.withCounter("hmcl.modpack.download"));
@@ -297,8 +297,8 @@ public class McbbsModpackCompletionTask extends CompletableFutureTask<Void> {
if (file instanceof McbbsModpackManifest.AddonFile) {
McbbsModpackManifest.AddonFile addonFile = (McbbsModpackManifest.AddonFile) file;
return new FileDownloadTask(
new URL(remoteManifest.getFileApi() + "/overrides/" + NetworkUtils.encodeLocation(addonFile.getPath())),
modManager.getSimpleModPath(addonFile.getPath()).toFile(),
URI.create(remoteManifest.getFileApi() + "/overrides/" + NetworkUtils.encodeLocation(addonFile.getPath())),
modManager.getSimpleModPath(addonFile.getPath()),
addonFile.getHash() != null ? new FileDownloadTask.IntegrityCheck("SHA-1", addonFile.getHash()) : null);
} else if (file instanceof McbbsModpackManifest.CurseFile) {
// we download it later.

View File

@@ -31,7 +31,7 @@ import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.net.URL;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.List;
@@ -329,9 +329,9 @@ public class McbbsModpackManifest implements ModpackManifest, Validation {
return fileName;
}
public URL getUrl() {
return url == null ? NetworkUtils.toURL("https://www.curseforge.com/minecraft/mc-mods/" + projectID + "/download/" + fileID + "/file")
: NetworkUtils.toURL(NetworkUtils.encodeLocation(url));
public URI getUrl() {
return url == null ? URI.create("https://www.curseforge.com/minecraft/mc-mods/" + projectID + "/download/" + fileID + "/file")
: URI.create(NetworkUtils.encodeLocation(url));
}
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)))
continue;
FileDownloadTask task = new FileDownloadTask(file.getDownloads(), filePath.toFile());
var task = new FileDownloadTask(file.getDownloads(), filePath);
task.setCacheRepository(dependency.getCacheRepository());
task.setCaching(true);
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.jetbrains.annotations.Nullable;
import java.net.URL;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -98,10 +98,10 @@ public class ModrinthManifest implements ModpackManifest, Validation {
private final String path;
private final Map<String, String> hashes;
private final Map<String, String> env;
private final List<URL> downloads;
private final List<URI> downloads;
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.hashes = hashes;
this.env = env;
@@ -121,7 +121,7 @@ public class ModrinthManifest implements ModpackManifest, Validation {
return env;
}
public List<URL> getDownloads() {
public List<URI> getDownloads() {
return downloads;
}

View File

@@ -1,9 +1,8 @@
package org.jackhuang.hmcl.mod.multimc;
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.stream.Collectors;
@@ -46,7 +45,7 @@ public final class MultiMCComponents {
return PAIRS;
}
public static URL getMetaURL(String componentID, String version) {
return NetworkUtils.toURL(String.format("https://meta.multimc.org/v1/%s/%s.json", componentID, version));
public static URI getMetaURL(String componentID, String 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.IOException;
import java.net.URL;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
@@ -85,7 +85,7 @@ public class ServerModpackCompletionTask extends Task<Void> {
@Override
public void preExecute() throws Exception {
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
@@ -153,8 +153,8 @@ public class ServerModpackCompletionTask extends Task<Void> {
if (download) {
total++;
dependencies.add(new FileDownloadTask(
new URL(remoteManifest.getFileApi() + "/overrides/" + NetworkUtils.encodeLocation(file.getPath())),
actualPath.toFile(),
URI.create(remoteManifest.getFileApi() + "/overrides/" + NetworkUtils.encodeLocation(file.getPath())),
actualPath,
new FileDownloadTask.IntegrityCheck("SHA-1", file.getHash()))
.withCounter("hmcl.modpack.download"));
}

View File

@@ -20,21 +20,20 @@ package org.jackhuang.hmcl.task;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.net.URL;
import java.net.URI;
import static java.util.Objects.requireNonNull;
public class DownloadException extends IOException {
private final URL url;
private final URI uri;
public DownloadException(URL url, @NotNull Throwable cause) {
super("Unable to download " + url + ", " + cause.getMessage(), requireNonNull(cause));
this.url = url;
public DownloadException(URI uri, @NotNull Throwable cause) {
super("Unable to download " + uri + ", " + cause.getMessage(), requireNonNull(cause));
this.uri = uri;
}
public URL getUrl() {
return url;
public URI getUri() {
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.NetworkUtils;
import org.jackhuang.hmcl.util.io.ResponseCodeException;
import org.jetbrains.annotations.NotNull;
import java.io.Closeable;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URI;
import java.net.URLConnection;
import java.nio.file.Path;
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.stream.Collectors;
import static org.jackhuang.hmcl.util.Lang.threadPool;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public abstract class FetchTask<T> extends Task<T> {
protected final List<URL> urls;
protected final List<URI> uris;
protected final int retry;
protected boolean caching;
protected CacheRepository repository = CacheRepository.getInstance();
public FetchTask(List<URL> urls, int retry) {
Objects.requireNonNull(urls);
public FetchTask(@NotNull List<@NotNull URI> uris, int retry) {
Objects.requireNonNull(uris);
this.urls = urls.stream().filter(Objects::nonNull).collect(Collectors.toList());
this.uris = List.copyOf(uris);
this.retry = retry;
if (this.urls.isEmpty())
if (this.uris.isEmpty())
throw new IllegalArgumentException("At least one URL is required");
setExecutor(download());
@@ -67,18 +69,19 @@ public abstract class FetchTask<T> extends Task<T> {
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 EnumCheckETag shouldCheckETag();
protected abstract Context getContext(URLConnection conn, boolean checkETag) throws IOException;
protected abstract Context getContext(URLConnection connection, boolean checkETag) throws IOException;
@Override
public void execute() throws Exception {
Exception exception = null;
URL failedURL = null;
URI failedURI = null;
boolean checkETag;
switch (shouldCheckETag()) {
case CHECK_E_TAG: checkETag = true; break;
@@ -87,7 +90,7 @@ public abstract class FetchTask<T> extends Task<T> {
}
int repeat = 0;
download: for (URL url : urls) {
download: for (URI uri : uris) {
for (int retryTime = 0; retryTime < retry; retryTime++) {
if (isCancelled()) {
break download;
@@ -95,11 +98,11 @@ public abstract class FetchTask<T> extends Task<T> {
List<String> redirects = null;
try {
beforeDownload(url);
beforeDownload(uri);
updateProgress(0);
URLConnection conn = NetworkUtils.createConnection(url);
URLConnection conn = NetworkUtils.createConnection(uri);
if (checkETag) repository.injectConnection(conn);
if (conn instanceof HttpURLConnection) {
@@ -111,21 +114,21 @@ public abstract class FetchTask<T> extends Task<T> {
if (responseCode == HttpURLConnection.HTTP_NOT_MODIFIED) {
// Handle cache
try {
Path cache = repository.getCachedRemoteFile(conn);
Path cache = repository.getCachedRemoteFile(conn.getURL().toURI());
useCachedResult(cache);
return;
} catch (IOException e) {
LOG.warning("Unable to use cached file, redownload " + url, e);
repository.removeRemoteEntry(conn);
LOG.warning("Unable to use cached file, redownload " + uri, e);
repository.removeRemoteEntry(conn.getURL().toURI());
// 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.
retryTime--;
continue;
}
} else if (responseCode / 100 == 4) {
throw new FileNotFoundException(url.toString());
throw new FileNotFoundException(uri.toString());
} 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;
} catch (FileNotFoundException ex) {
failedURL = url;
failedURI = uri;
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
} catch (IOException ex) {
failedURL = url;
failedURI = uri;
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)
throw new DownloadException(failedURL, exception);
throw new DownloadException(failedURI, exception);
}
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.
*
* @return download speed
*/
public int getSpeed() {
@@ -240,7 +244,7 @@ public abstract class FetchTask<T> extends Task<T> {
NOT_CHECK_E_TAG,
CACHED
}
protected static final class DownloadState {
private final int startPosition;
private final int endPosition;

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.FileUtils;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.URL;
import java.io.OutputStream;
import java.net.URI;
import java.net.URLConnection;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
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 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 Path candidate;
private final ArrayList<IntegrityCheckHandler> integrityCheckHandlers = new ArrayList<>();
/**
* @param url the URL of remote file.
* @param file the location that download to.
* @param uri the URI of remote file.
* @param path the location that download to.
*/
public FileDownloadTask(URL url, File file) {
this(url, file, null);
public FileDownloadTask(URI uri, Path path) {
this(uri, path, null);
}
/**
* @param url the URL of remote file.
* @param file the location that download to.
* @param uri the URI of remote file.
* @param path the location that download to.
* @param integrityCheck the integrity check to perform, null if no integrity check is to be performed
*/
public FileDownloadTask(URL url, File file, IntegrityCheck integrityCheck) {
this(Collections.singletonList(url), file, integrityCheck);
public FileDownloadTask(URI uri, Path path, IntegrityCheck integrityCheck) {
this(List.of(uri), path, integrityCheck);
}
/**
* @param url the URL of remote file.
* @param file the location that download to.
* @param uri the URI of remote file.
* @param path the location that download to.
* @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) {
this(Collections.singletonList(url), file, integrityCheck, retry);
public FileDownloadTask(URI uri, Path path, IntegrityCheck integrityCheck, int retry) {
this(List.of(uri), path, integrityCheck, retry);
}
/**
* 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.
*/
public FileDownloadTask(List<URL> urls, File file) {
this(urls, file, null);
public FileDownloadTask(List<URI> uris, Path file) {
this(uris, file, null);
}
/**
* 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
*/
public FileDownloadTask(List<URL> urls, File file, IntegrityCheck integrityCheck) {
this(urls, file, integrityCheck, 3);
public FileDownloadTask(List<URI> uris, Path path, IntegrityCheck integrityCheck) {
this(uris, path, integrityCheck, 3);
}
/**
* 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 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) {
super(urls, retry);
this.file = file;
public FileDownloadTask(List<URI> uris, Path path, IntegrityCheck integrityCheck, int retry) {
super(uris, retry);
this.file = path;
this.integrityCheck = integrityCheck;
setName(file.getName());
setName(path.getFileName().toString());
}
public File getFile() {
public Path getPath() {
return file;
}
@@ -164,8 +171,8 @@ public class FileDownloadTask extends FetchTask<Void> {
Optional<Path> cache = repository.checkExistentFile(candidate, integrityCheck.getAlgorithm(), integrityCheck.getChecksum());
if (cache.isPresent()) {
try {
FileUtils.copyFile(cache.get().toFile(), file);
LOG.trace("Successfully verified file " + file + " from " + urls.get(0));
FileUtils.copyFile(cache.get(), file);
LOG.trace("Successfully verified file " + file + " from " + uris.get(0));
return EnumCheckETag.CACHED;
} catch (IOException e) {
LOG.warning("Failed to copy cache files", e);
@@ -178,20 +185,20 @@ public class FileDownloadTask extends FetchTask<Void> {
}
@Override
protected void beforeDownload(URL url) {
LOG.trace("Downloading " + url + " to " + file);
protected void beforeDownload(URI uri) {
LOG.trace("Downloading " + uri + " to " + file);
}
@Override
protected void useCachedResult(Path cache) throws IOException {
FileUtils.copyFile(cache.toFile(), file);
FileUtils.copyFile(cache, file);
}
@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);
RandomAccessFile rFile = new RandomAccessFile(temp.toFile(), "rw");
MessageDigest digest = integrityCheck == null ? null : integrityCheck.createDigest();
OutputStream fileOutput = Files.newOutputStream(temp);
return new Context() {
@Override
@@ -200,36 +207,34 @@ public class FileDownloadTask extends FetchTask<Void> {
digest.update(buffer, offset, len);
}
rFile.write(buffer, offset, len);
fileOutput.write(buffer, offset, len);
}
@Override
public void close() throws IOException {
try {
rFile.close();
fileOutput.close();
} catch (IOException e) {
LOG.warning("Failed to close file: " + rFile, e);
LOG.warning("Failed to close file: " + temp, e);
}
if (!isSuccess()) {
try {
Files.delete(temp);
Files.deleteIfExists(temp);
} catch (IOException e) {
LOG.warning("Failed to delete file: " + rFile, e);
LOG.warning("Failed to delete file: " + temp, e);
}
return;
}
for (IntegrityCheckHandler handler : integrityCheckHandlers) {
handler.checkIntegrity(temp, file.toPath());
handler.checkIntegrity(temp, file);
}
Files.deleteIfExists(file.toPath());
if (!FileUtils.makeDirectory(file.getAbsoluteFile().getParentFile()))
throw new IOException("Unable to make parent directory " + file);
Files.createDirectories(file.toAbsolutePath().getParent());
try {
FileUtils.moveFile(temp.toFile(), file);
Files.move(temp, file, StandardCopyOption.REPLACE_EXISTING);
} catch (Exception 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) {
try {
repository.cacheFile(file.toPath(), integrityCheck.getAlgorithm(), integrityCheck.getChecksum());
repository.cacheFile(file, integrityCheck.getAlgorithm(), integrityCheck.getChecksum());
} catch (IOException e) {
LOG.warning("Failed to cache file", e);
}
}
if (checkETag) {
repository.cacheRemoteFile(file.toPath(), conn);
repository.cacheRemoteFile(connection, file);
}
}
};
@@ -257,7 +262,8 @@ public class FileDownloadTask extends FetchTask<Void> {
public interface IntegrityCheckHandler {
/**
* 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
* @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.IOException;
import java.net.URL;
import java.net.URI;
import java.net.URLConnection;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.List;
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> {
private static final int DEFAULT_RETRY = 3;
private final Charset charset;
public GetTask(URL url) {
public GetTask(URI url) {
this(url, UTF_8);
}
public GetTask(URL url, Charset charset) {
this(url, charset, 3);
public GetTask(URI url, Charset charset) {
this(url, charset, DEFAULT_RETRY);
}
public GetTask(URL url, Charset charset, int retry) {
this(Collections.singletonList(url), charset, retry);
public GetTask(URI url, Charset charset, int retry) {
this(List.of(url), charset, retry);
}
public GetTask(List<URL> url) {
this(url, UTF_8, 3);
public GetTask(List<URI> url) {
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);
this.charset = charset;
@@ -70,7 +72,7 @@ public final class GetTask extends FetchTask<String> {
}
@Override
protected Context getContext(URLConnection conn, boolean checkETag) {
protected Context getContext(URLConnection connection, boolean checkETag) {
return new Context() {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
@@ -87,7 +89,7 @@ public final class GetTask extends FetchTask<String> {
setResult(result);
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.IOException;
import java.net.URI;
import java.net.URLConnection;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
@@ -157,12 +158,11 @@ public class CacheRepository {
return cache;
}
public Path getCachedRemoteFile(URLConnection conn) throws IOException {
String url = conn.getURL().toString();
public Path getCachedRemoteFile(URI uri) throws IOException {
lock.readLock().lock();
ETagItem eTagItem;
try {
eTagItem = index.get(url);
eTagItem = index.get(uri.toString());
} finally {
lock.readLock().unlock();
}
@@ -177,11 +177,10 @@ public class CacheRepository {
return file;
}
public void removeRemoteEntry(URLConnection conn) {
String url = conn.getURL().toString();
public void removeRemoteEntry(URI uri) {
lock.readLock().lock();
try {
index.remove(url);
index.remove(uri.toString());
} finally {
lock.readLock().unlock();
}
@@ -203,34 +202,34 @@ public class CacheRepository {
// conn.setRequestProperty("If-Modified-Since", eTagItem.getRemoteLastModified());
}
public void cacheRemoteFile(Path downloaded, URLConnection conn) throws IOException {
cacheData(() -> {
public void cacheRemoteFile(URLConnection connection, Path downloaded) throws IOException {
cacheData(connection, () -> {
String hash = DigestUtils.digestToString(SHA1, downloaded);
Path cached = cacheFile(downloaded, SHA1, hash);
return new CacheResult(hash, cached);
}, conn);
});
}
public void cacheText(String text, URLConnection conn) throws IOException {
cacheBytes(text.getBytes(UTF_8), conn);
public void cacheText(URLConnection connection, String text) throws IOException {
cacheBytes(connection, text.getBytes(UTF_8));
}
public void cacheBytes(byte[] bytes, URLConnection conn) throws IOException {
cacheData(() -> {
public void cacheBytes(URLConnection connection, byte[] bytes) throws IOException {
cacheData(connection, () -> {
String hash = DigestUtils.digestToString(SHA1, bytes);
Path cached = getFile(SHA1, hash);
FileUtils.writeBytes(cached, bytes);
return new CacheResult(hash, cached);
}, conn);
});
}
public synchronized void cacheData(ExceptionalSupplier<CacheResult, IOException> cacheSupplier, URLConnection conn) throws IOException {
String eTag = conn.getHeaderField("ETag");
if (eTag == null) return;
String url = conn.getURL().toString();
String lastModified = conn.getHeaderField("Last-Modified");
private void cacheData(URLConnection connection, ExceptionalSupplier<CacheResult, IOException> cacheSupplier) throws IOException {
String eTag = connection.getHeaderField("ETag");
if (eTag == null || eTag.isEmpty()) return;
String uri = connection.getURL().toString();
String lastModified = connection.getHeaderField("Last-Modified");
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();
writeLock.lock();
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.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.HashMap;
import java.util.HashSet;
@@ -113,18 +114,13 @@ public abstract class HttpRequest {
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) {
toleratedHttpCodes.add(code);
return this;
}
public HttpURLConnection createConnection() throws IOException {
HttpURLConnection con = createHttpConnection(new URL(url));
HttpURLConnection con = createHttpConnection(URI.create(url));
con.setRequestMethod(method);
for (Map.Entry<String, String> entry : headers.entrySet()) {
con.setRequestProperty(entry.getKey(), entry.getValue());
@@ -133,7 +129,7 @@ public abstract class HttpRequest {
}
public static class HttpGetRequest extends HttpRequest {
public HttpGetRequest(String url) {
protected HttpGetRequest(String url) {
super(url, "GET");
}
@@ -149,7 +145,7 @@ public abstract class HttpRequest {
public static final class HttpPostRequest extends HttpRequest {
private byte[] bytes;
public HttpPostRequest(String url) {
private HttpPostRequest(String url) {
super(url, "POST");
}
@@ -189,21 +185,17 @@ public abstract class HttpRequest {
URL url = new URL(this.url);
if (responseCodeTester != null) {
responseCodeTester.accept(url, con.getResponseCode());
} else {
if (con.getResponseCode() / 100 != 2) {
if (!ignoreHttpCode && !toleratedHttpCodes.contains(con.getResponseCode())) {
try {
throw new ResponseCodeException(url, con.getResponseCode(), NetworkUtils.readData(con));
} catch (IOException e) {
throw new ResponseCodeException(url, con.getResponseCode(), e);
}
if (con.getResponseCode() / 100 != 2) {
if (!ignoreHttpCode && !toleratedHttpCodes.contains(con.getResponseCode())) {
try {
throw new ResponseCodeException(NetworkUtils.toURI(url), con.getResponseCode(), NetworkUtils.readFullyAsString(con));
} catch (IOException e) {
throw new ResponseCodeException(NetworkUtils.toURI(url), con.getResponseCode(), e);
}
}
}
return NetworkUtils.readData(con);
return NetworkUtils.readFullyAsString(con);
}, retryTimes);
}
}

View File

@@ -21,12 +21,16 @@ import org.jackhuang.hmcl.util.Pair;
import java.io.*;
import java.net.*;
import java.nio.charset.Charset;
import java.util.*;
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 org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.StringUtils.*;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author huangyuhui
@@ -39,6 +43,10 @@ public final class 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) {
StringBuilder sb = new StringBuilder(baseUrl);
boolean first = true;
@@ -73,7 +81,7 @@ public final class NetworkUtils {
scanner.useDelimiter("&");
while (scanner.hasNext()) {
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");
}
@@ -85,8 +93,8 @@ public final class NetworkUtils {
return result;
}
public static URLConnection createConnection(URL url) throws IOException {
URLConnection connection = url.openConnection();
public static URLConnection createConnection(URI uri) throws IOException {
URLConnection connection = uri.toURL().openConnection();
connection.setUseCaches(false);
connection.setConnectTimeout(TIME_OUT);
connection.setReadTimeout(TIME_OUT);
@@ -94,15 +102,15 @@ public final class NetworkUtils {
return connection;
}
public static HttpURLConnection createHttpConnection(URL url) throws IOException {
public static HttpURLConnection createHttpConnection(URI url) throws IOException {
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
* @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) {
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
* 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.
* @return manually redirected http connection.
* @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 {
int redirect = 0;
@@ -153,7 +161,7 @@ public final class NetworkUtils {
Map<String, List<String>> properties = conn.getRequestProperties();
String method = conn.getRequestMethod();
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");
conn.disconnect();
@@ -178,19 +186,15 @@ public final class NetworkUtils {
return conn;
}
public static String doGet(URL url) throws IOException {
HttpURLConnection con = createHttpConnection(url);
con = resolveConnection(con);
return IOUtils.readFullyAsString(con.getInputStream());
public static String doGet(URI uri) throws IOException {
return readFullyAsString(resolveConnection(createHttpConnection(uri)));
}
public static String doGet(List<URL> urls) throws IOException {
public static String doGet(List<URI> uris) throws IOException {
List<IOException> exceptions = null;
for (URL url : urls) {
for (URI uri : uris) {
try {
HttpURLConnection con = createHttpConnection(url);
con = resolveConnection(con);
return IOUtils.readFullyAsString(con.getInputStream());
return doGet(uri);
} catch (IOException e) {
if (exceptions == null) {
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();
if (params != null) {
for (Map.Entry<String, String> e : params.entrySet())
@@ -222,50 +230,72 @@ public final class NetworkUtils {
return doPost(u, sb.toString());
}
public static String doPost(URL u, String post) throws IOException {
return doPost(u, post, "application/x-www-form-urlencoded");
}
public static String doPost(URL url, String post, String contentType) throws IOException {
public static String doPost(URI uri, String post, String contentType) throws IOException {
byte[] bytes = post.getBytes(UTF_8);
HttpURLConnection con = createHttpConnection(url);
HttpURLConnection con = createHttpConnection(uri);
con.setRequestMethod("POST");
con.setDoOutput(true);
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()) {
os.write(bytes);
}
return readData(con);
return readFullyAsString(con);
}
public static String readData(HttpURLConnection con) throws IOException {
try {
try (InputStream stdout = con.getInputStream()) {
return IOUtils.readFullyAsString("gzip".equals(con.getContentEncoding()) ? IOUtils.wrapFromGZip(stdout) : stdout);
static final Pattern CHARSET_REGEX = Pattern.compile("\\s*(charset)\\s*=\\s*['|\"]?(?<charset>[^\"^';,]+)['|\"]?");
static Charset getCharsetFromContentType(String contentType) {
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()) {
if (stderr == null)
}
return UTF_8;
}
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;
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 {
HttpURLConnection conn = resolveConnection(createHttpConnection(url));
public static String detectFileName(URI uri) throws IOException {
HttpURLConnection conn = resolveConnection(createHttpConnection(uri));
int code = conn.getResponseCode();
if (code / 100 == 4)
throw new FileNotFoundException();
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");
if (disposition == null || !disposition.contains("filename=")) {
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 {
return new URL(str);
} catch (MalformedURLException e) {
return url.toURI();
} catch (URISyntaxException 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 ====
public static String encodeURL(String toEncode) {
try {
return URLEncoder.encode(toEncode, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new Error();
}
return URLEncoder.encode(toEncode, UTF_8);
}
public static String decodeURL(String toDecode) {
try {
return URLDecoder.decode(toDecode, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new Error();
}
return URLDecoder.decode(toDecode, UTF_8);
}
// ====
}

View File

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