修复下载缓存不生效的问题 (#4167)

This commit is contained in:
Glavo 2025-08-01 20:53:32 +08:00 committed by GitHub
parent 7d6f21dc3a
commit bcd73e6d15
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 292 additions and 320 deletions

View File

@ -49,6 +49,8 @@ import org.jackhuang.hmcl.download.quilt.QuiltAPIRemoteVersion;
import org.jackhuang.hmcl.download.quilt.QuiltRemoteVersion;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.setting.VersionIconType;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
@ -67,7 +69,6 @@ import org.jackhuang.hmcl.util.i18n.I18n;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@ -96,7 +97,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
private final StackPane center;
private final VersionList<?> versionList;
private CompletableFuture<?> executor;
private Task<?> executor;
private final HBox searchBar;
private final StringProperty queryString = new SimpleStringProperty();
@ -308,7 +309,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
public void refresh() {
VersionList<?> currentVersionList = versionList;
root.setContent(spinner, ContainerAnimations.FADE);
executor = currentVersionList.refreshAsync(gameVersion).whenComplete((result, exception) -> {
executor = currentVersionList.refreshAsync(gameVersion).whenComplete(Schedulers.defaultScheduler(), (result, exception) -> {
if (exception == null) {
List<RemoteVersion> items = loadVersions();
@ -338,6 +339,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
// https://github.com/HMCL-dev/HMCL/issues/938
System.gc();
});
executor.start();
}
@Override
@ -348,8 +350,9 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
@Override
public void cleanup(Map<String, Object> settings) {
settings.remove(libraryId);
if (executor != null)
executor.cancel(true);
// fixme
// if (executor != null)
// executor.cancel(true);
}
private void onRefresh() {

View File

@ -29,13 +29,14 @@ import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.NotNull;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@ -43,7 +44,6 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class DefaultCacheRepository extends CacheRepository {
private Path librariesDir;
private Path indexFile;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private Index index = null;
public DefaultCacheRepository() {
@ -64,10 +64,13 @@ public class DefaultCacheRepository extends CacheRepository {
lock.writeLock().lock();
try {
if (Files.isRegularFile(indexFile)) {
index = JsonUtils.fromNonNullJson(Files.readString(indexFile), Index.class);
}
else
index = JsonUtils.fromJsonFile(indexFile, Index.class);
if (index == null) {
throw new JsonParseException("Index file is empty or invalid");
}
} else {
index = new Index();
}
} catch (IOException | JsonParseException e) {
LOG.warning("Unable to read index file", e);
index = new Index();
@ -82,7 +85,7 @@ public class DefaultCacheRepository extends CacheRepository {
* If cannot be verified, the library will not be cached.
*
* @param library the library being cached
* @param jar the file of library
* @param jar the file of library
*/
public void tryCacheLibrary(Library library, Path jar) {
lock.readLock().lock();
@ -171,8 +174,8 @@ public class DefaultCacheRepository extends CacheRepository {
* Caches the library file to repository.
*
* @param library the library to cache
* @param path the file being cached, must be verified
* @param forge true if this library is provided by Forge
* @param path the file being cached, must be verified
* @param forge true if this library is provided by Forge
* @return cached file location
* @throws IOException if failed to calculate hash code of {@code path} or copy the file to cache
*/
@ -201,25 +204,29 @@ public class DefaultCacheRepository extends CacheRepository {
if (indexFile == null || index == null) return;
try {
Files.createDirectories(indexFile.getParent());
JsonUtils.writeToJsonFile(indexFile, index);
FileUtils.saveSafely(indexFile, outputStream -> {
try (var writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8))) {
JsonUtils.GSON.toJson(index, writer);
}
});
} catch (IOException e) {
LOG.error("Unable to save index.json", e);
}
}
/**
* {
* "libraries": {
* // allow a library has multiple hash code.
* [
* "name": "net.minecraftforge:forge:1.11.2-13.20.0.2345",
* "hash": "blablabla",
* "type": "forge"
* ]
* }
* // assets and versions will not be included in index.
* }
*/
/// ```json
/// {
/// "libraries": {
/// // allow a library has multiple hash code.
/// [
/// "name": "net.minecraftforge:forge:1.11.2-13.20.0.2345",
/// "hash": "blablabla",
/// "type": "forge"
/// ]
/// }
/// }
///```
/// assets and versions will not be included in index.
private static final class Index implements Validation {
private final Set<LibraryIndex> libraries;

View File

@ -148,7 +148,7 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
if (baseVersion.isResolved()) throw new IllegalArgumentException("Version should not be resolved");
VersionList<?> versionList = getVersionList(libraryId);
return Task.fromCompletableFuture(versionList.loadAsync(gameVersion))
return versionList.loadAsync(gameVersion)
.thenComposeAsync(() -> installLibraryAsync(baseVersion, versionList.getVersion(gameVersion, libraryVersion)
.orElseThrow(() -> new IOException("Remote library " + libraryId + " has no version " + libraryVersion))))
.withStage(String.format("hmcl.install.%s:%s", libraryId, libraryVersion));

View File

@ -17,8 +17,9 @@
*/
package org.jackhuang.hmcl.download;
import org.jackhuang.hmcl.task.Task;
import java.util.Arrays;
import java.util.concurrent.CompletableFuture;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@ -40,44 +41,40 @@ public class MultipleSourceVersionList extends VersionList<RemoteVersion> {
}
@Override
public CompletableFuture<?> loadAsync() {
public Task<?> loadAsync() {
throw new UnsupportedOperationException("MultipleSourceVersionList does not support loading the entire remote version list.");
}
@Override
public CompletableFuture<?> refreshAsync() {
public Task<?> refreshAsync() {
throw new UnsupportedOperationException("MultipleSourceVersionList does not support loading the entire remote version list.");
}
private CompletableFuture<?> refreshAsync(String gameVersion, int sourceIndex) {
private Task<?> refreshAsync(String gameVersion, int sourceIndex) {
VersionList<?> versionList = backends[sourceIndex];
CompletableFuture<Void> future = versionList.refreshAsync(gameVersion)
.thenRunAsync(() -> {
return versionList.refreshAsync(gameVersion)
.thenComposeAsync(() -> {
lock.writeLock().lock();
try {
versions.putAll(gameVersion, versionList.getVersions(gameVersion));
} catch (Exception e) {
if (sourceIndex == backends.length - 1) {
LOG.warning("Failed to fetch versions list from all sources", e);
throw e;
} else {
LOG.warning("Failed to fetch versions list and try to fetch from other source", e);
return refreshAsync(gameVersion, sourceIndex + 1);
}
} finally {
lock.writeLock().unlock();
}
return null;
});
if (sourceIndex == backends.length - 1) {
return future;
} else {
return future.<CompletableFuture<?>>handle((ignore, e) -> {
if (e == null) {
return future;
}
LOG.warning("Failed to fetch versions list and try to fetch from other source", e);
return refreshAsync(gameVersion, sourceIndex + 1);
}).thenCompose(it -> it);
}
}
@Override
public CompletableFuture<?> refreshAsync(String gameVersion) {
public Task<?> refreshAsync(String gameVersion) {
versions.clear(gameVersion);
return refreshAsync(gameVersion, 0);
}

View File

@ -17,17 +17,16 @@
*/
package org.jackhuang.hmcl.download;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.SimpleMultimap;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* The remote version list.
*
* @param <T> The subclass of {@code RemoteVersion}, the type of RemoteVersion.
*
* @author huangyuhui
*/
public abstract class VersionList<T extends RemoteVersion> {
@ -48,6 +47,7 @@ public abstract class VersionList<T extends RemoteVersion> {
/**
* True if the version list that contains the remote versions which depends on the specific game version has been loaded.
*
* @param gameVersion the remote version depends on
*/
public boolean isLoaded(String gameVersion) {
@ -61,44 +61,36 @@ public abstract class VersionList<T extends RemoteVersion> {
/**
* @return the task to reload the remote version list.
*/
public abstract CompletableFuture<?> refreshAsync();
public abstract Task<?> refreshAsync();
/**
* @param gameVersion the remote version depends on
* @return the task to reload the remote version list.
*/
public CompletableFuture<?> refreshAsync(String gameVersion) {
public Task<?> refreshAsync(String gameVersion) {
return refreshAsync();
}
public CompletableFuture<?> loadAsync() {
return CompletableFuture.completedFuture(null)
.thenComposeAsync(unused -> {
lock.readLock().lock();
boolean loaded;
try {
loaded = isLoaded();
} finally {
lock.readLock().unlock();
}
return loaded ? CompletableFuture.completedFuture(null) : refreshAsync();
});
public Task<?> loadAsync() {
return Task.composeAsync(() -> {
lock.readLock().lock();
try {
return isLoaded() ? null : refreshAsync();
} finally {
lock.readLock().unlock();
}
});
}
public CompletableFuture<?> loadAsync(String gameVersion) {
return CompletableFuture.completedFuture(null)
.thenComposeAsync(unused -> {
lock.readLock().lock();
boolean loaded;
try {
loaded = isLoaded(gameVersion);
} finally {
lock.readLock().unlock();
}
return loaded ? CompletableFuture.completedFuture(null) : refreshAsync(gameVersion);
});
public Task<?> loadAsync(String gameVersion) {
return Task.composeAsync(() -> {
lock.readLock().lock();
try {
return isLoaded(gameVersion) ? null : refreshAsync(gameVersion);
} finally {
lock.readLock().unlock();
}
});
}
protected Collection<T> getVersionsImpl(String gameVersion) {
@ -123,7 +115,7 @@ public abstract class VersionList<T extends RemoteVersion> {
/**
* Get the specific remote version.
*
* @param gameVersion the Minecraft version that remote versions belong to
* @param gameVersion the Minecraft version that remote versions belong to
* @param remoteVersion the version of the remote version.
* @return the specific remote version, null if it is not found.
*/

View File

@ -21,12 +21,10 @@ import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Lang;
import java.util.Collections;
import java.util.concurrent.CompletableFuture;
import static org.jackhuang.hmcl.util.Lang.wrap;
public class FabricAPIVersionList extends VersionList<FabricAPIRemoteVersion> {
@ -42,14 +40,14 @@ public class FabricAPIVersionList extends VersionList<FabricAPIRemoteVersion> {
}
@Override
public CompletableFuture<?> refreshAsync() {
return CompletableFuture.runAsync(wrap(() -> {
public Task<?> refreshAsync() {
return Task.runAsync(() -> {
for (RemoteMod.Version modVersion : Lang.toIterable(ModrinthRemoteModRepository.MODS.getRemoteVersionsById("P7dR8mSH"))) {
for (String gameVersion : modVersion.getGameVersions()) {
versions.put(gameVersion, new FabricAPIRemoteVersion(gameVersion, modVersion.getVersion(), modVersion.getName(), modVersion.getDatePublished(), modVersion,
Collections.singletonList(modVersion.getFile().getUrl())));
}
}
}));
});
}
}

View File

@ -19,6 +19,7 @@ package org.jackhuang.hmcl.download.fabric;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.Nullable;
@ -26,10 +27,8 @@ import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.Lang.wrap;
import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf;
public final class FabricVersionList extends VersionList<FabricRemoteVersion> {
@ -45,8 +44,8 @@ public final class FabricVersionList extends VersionList<FabricRemoteVersion> {
}
@Override
public CompletableFuture<?> refreshAsync() {
return CompletableFuture.runAsync(wrap(() -> {
public Task<?> refreshAsync() {
return Task.runAsync(() -> {
List<String> gameVersions = getGameVersions(GAME_META_URL);
List<String> loaderVersions = getGameVersions(LOADER_META_URL);
@ -60,7 +59,7 @@ public final class FabricVersionList extends VersionList<FabricRemoteVersion> {
} finally {
lock.writeLock().unlock();
}
}));
});
}
private static final String LOADER_META_URL = "https://meta.fabricmc.net/v2/versions/loader";

View File

@ -19,25 +19,25 @@ package org.jackhuang.hmcl.download.forge;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.Validation;
import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.net.URI;
import java.time.Instant;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Lang.wrap;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@ -58,12 +58,12 @@ public final class ForgeBMCLVersionList extends VersionList<ForgeRemoteVersion>
}
@Override
public CompletableFuture<?> loadAsync() {
public Task<?> loadAsync() {
throw new UnsupportedOperationException("ForgeBMCLVersionList does not support loading the entire Forge remote version list.");
}
@Override
public CompletableFuture<?> refreshAsync() {
public Task<?> refreshAsync() {
throw new UnsupportedOperationException("ForgeBMCLVersionList does not support loading the entire Forge remote version list.");
}
@ -83,11 +83,10 @@ public final class ForgeBMCLVersionList extends VersionList<ForgeRemoteVersion>
}
@Override
public CompletableFuture<?> refreshAsync(String gameVersion) {
public Task<?> refreshAsync(String gameVersion) {
String lookupVersion = toLookupVersion(gameVersion);
return CompletableFuture.completedFuture(null)
.thenApplyAsync(wrap(unused -> HttpRequest.GET(apiRoot + "/forge/minecraft/" + lookupVersion).getJson(listTypeOf(ForgeVersion.class))))
return new GetTask(URI.create(apiRoot + "/forge/minecraft/" + lookupVersion)).thenGetJsonAsync(listTypeOf(ForgeVersion.class))
.thenAcceptAsync(forgeVersions -> {
lock.writeLock().lock();
try {

View File

@ -19,14 +19,15 @@ package org.jackhuang.hmcl.download.forge;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.net.URI;
import java.time.Instant;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
/**
*
@ -53,8 +54,8 @@ public final class ForgeVersionList extends VersionList<ForgeRemoteVersion> {
}
@Override
public CompletableFuture<?> refreshAsync() {
return HttpRequest.GET(FORGE_LIST).getJsonAsync(ForgeVersionRoot.class)
public Task<?> refreshAsync() {
return new GetTask(FORGE_LIST).thenGetJsonAsync(ForgeVersionRoot.class)
.thenAcceptAsync(root -> {
lock.writeLock().lock();
@ -95,5 +96,5 @@ public final class ForgeVersionList extends VersionList<ForgeRemoteVersion> {
});
}
public static final String FORGE_LIST = "https://hmcl-dev.github.io/metadata/forge/";
public static final URI FORGE_LIST = URI.create("https://hmcl-dev.github.io/metadata/forge/");
}

View File

@ -19,14 +19,15 @@ package org.jackhuang.hmcl.download.game;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.HttpRequest;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URI;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.CompletableFuture;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@ -52,8 +53,8 @@ public final class GameVersionList extends VersionList<GameRemoteVersion> {
}
@Override
public CompletableFuture<?> refreshAsync() {
return HttpRequest.GET(downloadProvider.getVersionListURL()).getJsonAsync(GameRemoteVersions.class)
public Task<?> refreshAsync() {
return new GetTask(URI.create(downloadProvider.getVersionListURL())).thenGetJsonAsync(GameRemoteVersions.class)
.thenAcceptAsync(root -> {
GameRemoteVersions unlistedVersions = null;

View File

@ -44,7 +44,7 @@ public final class VersionJsonDownloadTask extends Task<String> {
this.dependencyManager = dependencyManager;
this.gameVersionList = dependencyManager.getVersionList("game");
dependents.add(Task.fromCompletableFuture(gameVersionList.loadAsync(gameVersion)));
dependents.add(gameVersionList.loadAsync(gameVersion));
setSignificance(TaskSignificance.MODERATE);
}

View File

@ -20,12 +20,12 @@ package org.jackhuang.hmcl.download.liteloader;
import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.util.Collections;
import java.util.concurrent.CompletableFuture;
import java.util.Map;
/**
* @author huangyuhui
@ -54,17 +54,15 @@ public final class LiteLoaderBMCLVersionList extends VersionList<LiteLoaderRemot
}
@Override
public CompletableFuture<?> refreshAsync() {
public Task<?> refreshAsync() {
throw new UnsupportedOperationException();
}
@Override
public CompletableFuture<?> refreshAsync(String gameVersion) {
return HttpRequest.GET(
downloadProvider.injectURL("https://bmclapi2.bangbang93.com/liteloader/list"), Pair.pair("mcversion", gameVersion)
)
.getJsonAsync(LiteLoaderBMCLVersion.class)
.thenAccept(v -> {
public Task<?> refreshAsync(String gameVersion) {
return new GetTask(NetworkUtils.withQuery(downloadProvider.injectURLWithCandidates("https://bmclapi2.bangbang93.com/liteloader/list"), Map.of("mcversion", gameVersion)))
.thenGetJsonAsync(LiteLoaderBMCLVersion.class)
.thenAcceptAsync(v -> {
lock.writeLock().lock();
try {
versions.clear();

View File

@ -20,16 +20,18 @@ package org.jackhuang.hmcl.download.liteloader;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
/**
* @author huangyuhui
@ -50,8 +52,9 @@ public final class LiteLoaderVersionList extends VersionList<LiteLoaderRemoteVer
public static final String LITELOADER_LIST = "https://dl.liteloader.com/versions/versions.json";
@Override
public CompletableFuture<?> refreshAsync(String gameVersion) {
return HttpRequest.GET(downloadProvider.injectURL(LITELOADER_LIST)).getJsonAsync(LiteLoaderVersionsRoot.class)
public Task<?> refreshAsync(String gameVersion) {
return new GetTask(URI.create(downloadProvider.injectURL(LITELOADER_LIST)))
.thenGetJsonAsync(LiteLoaderVersionsRoot.class)
.thenAcceptAsync(root -> {
LiteLoaderGameVersions versions = root.getVersions().get(gameVersion);
if (versions == null) {
@ -85,7 +88,7 @@ public final class LiteLoaderVersionList extends VersionList<LiteLoaderRemoteVer
}
@Override
public CompletableFuture<?> refreshAsync() {
public Task<?> refreshAsync() {
throw new UnsupportedOperationException();
}

View File

@ -20,15 +20,15 @@ package org.jackhuang.hmcl.download.neoforge;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.gson.Validation;
import org.jackhuang.hmcl.util.io.HttpRequest;
import java.net.URI;
import java.util.Collections;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import static org.jackhuang.hmcl.util.Lang.wrap;
import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf;
public final class NeoForgeBMCLVersionList extends VersionList<NeoForgeRemoteVersion> {
@ -47,12 +47,12 @@ public final class NeoForgeBMCLVersionList extends VersionList<NeoForgeRemoteVer
}
@Override
public CompletableFuture<?> loadAsync() {
public Task<?> loadAsync() {
throw new UnsupportedOperationException("NeoForgeBMCLVersionList does not support loading the entire NeoForge remote version list.");
}
@Override
public CompletableFuture<?> refreshAsync() {
public Task<?> refreshAsync() {
throw new UnsupportedOperationException("NeoForgeBMCLVersionList does not support loading the entire NeoForge remote version list.");
}
@ -65,9 +65,8 @@ public final class NeoForgeBMCLVersionList extends VersionList<NeoForgeRemoteVer
}
@Override
public CompletableFuture<?> refreshAsync(String gameVersion) {
return CompletableFuture.completedFuture((Void) null)
.thenApplyAsync(wrap(unused -> HttpRequest.GET(apiRoot + "/neoforge/list/" + gameVersion).getJson(listTypeOf(NeoForgeVersion.class))))
public Task<?> refreshAsync(String gameVersion) {
return new GetTask(URI.create(apiRoot + "/neoforge/list/" + gameVersion)).thenGetJsonAsync(listTypeOf(NeoForgeVersion.class))
.thenAcceptAsync(neoForgeVersions -> {
lock.writeLock().lock();

View File

@ -2,14 +2,14 @@ package org.jackhuang.hmcl.download.neoforge;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import static org.jackhuang.hmcl.util.Lang.wrap;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class NeoForgeOfficialVersionList extends VersionList<NeoForgeRemoteVersion> {
@ -37,17 +37,17 @@ public final class NeoForgeOfficialVersionList extends VersionList<NeoForgeRemot
}
@Override
public CompletableFuture<?> refreshAsync() {
return CompletableFuture.supplyAsync(wrap(() -> new OfficialAPIResult[]{
HttpRequest.GET(downloadProvider.injectURL(OLD_URL)).getJson(OfficialAPIResult.class),
HttpRequest.GET(downloadProvider.injectURL(META_URL)).getJson(OfficialAPIResult.class)
})).thenAccept(results -> {
public Task<?> refreshAsync() {
return Task.allOf(
new GetTask(URI.create(downloadProvider.injectURL(OLD_URL))).thenGetJsonAsync(OfficialAPIResult.class),
new GetTask(URI.create(downloadProvider.injectURL(META_URL))).thenGetJsonAsync(OfficialAPIResult.class)
).thenAcceptAsync(results -> {
lock.writeLock().lock();
try {
versions.clear();
for (String version : results[0].versions) {
for (String version : results.get(0).versions) {
versions.put("1.20.1", new NeoForgeRemoteVersion(
"1.20.1", NeoForgeRemoteVersion.normalize(version),
Collections.singletonList(
@ -56,7 +56,7 @@ public final class NeoForgeOfficialVersionList extends VersionList<NeoForgeRemot
));
}
for (String version : results[1].versions) {
for (String version : results.get(1).versions) {
String mcVersion;
try {

View File

@ -19,14 +19,15 @@ package org.jackhuang.hmcl.download.optifine;
import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.net.URI;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf;
@ -71,8 +72,8 @@ public final class OptiFineBMCLVersionList extends VersionList<OptiFineRemoteVer
}
@Override
public CompletableFuture<?> refreshAsync() {
return HttpRequest.GET(apiRoot + "/optifine/versionlist").getJsonAsync(listTypeOf(OptiFineVersion.class)).thenAcceptAsync(root -> {
public Task<?> refreshAsync() {
return new GetTask(URI.create(apiRoot + "/optifine/versionlist")).thenGetJsonAsync(listTypeOf(OptiFineVersion.class)).thenAcceptAsync(root -> {
lock.writeLock().lock();
try {

View File

@ -21,12 +21,10 @@ import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Lang;
import java.util.Collections;
import java.util.concurrent.CompletableFuture;
import static org.jackhuang.hmcl.util.Lang.wrap;
public class QuiltAPIVersionList extends VersionList<QuiltAPIRemoteVersion> {
@ -42,14 +40,14 @@ public class QuiltAPIVersionList extends VersionList<QuiltAPIRemoteVersion> {
}
@Override
public CompletableFuture<?> refreshAsync() {
return CompletableFuture.runAsync(wrap(() -> {
public Task<?> refreshAsync() {
return Task.runAsync(() -> {
for (RemoteMod.Version modVersion : Lang.toIterable(ModrinthRemoteModRepository.MODS.getRemoteVersionsById("qsl"))) {
for (String gameVersion : modVersion.getGameVersions()) {
versions.put(gameVersion, new QuiltAPIRemoteVersion(gameVersion, modVersion.getVersion(), modVersion.getName(), modVersion.getDatePublished(), modVersion,
Collections.singletonList(modVersion.getFile().getUrl())));
}
}
}));
});
}
}

View File

@ -19,6 +19,7 @@ package org.jackhuang.hmcl.download.quilt;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.Nullable;
@ -26,10 +27,8 @@ import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.Lang.wrap;
import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf;
public final class QuiltVersionList extends VersionList<QuiltRemoteVersion> {
@ -45,8 +44,8 @@ public final class QuiltVersionList extends VersionList<QuiltRemoteVersion> {
}
@Override
public CompletableFuture<?> refreshAsync() {
return CompletableFuture.runAsync(wrap(() -> {
public Task<?> refreshAsync() {
return Task.runAsync(() -> {
List<String> gameVersions = getGameVersions(GAME_META_URL);
List<String> loaderVersions = getGameVersions(LOADER_META_URL);
@ -60,7 +59,7 @@ public final class QuiltVersionList extends VersionList<QuiltRemoteVersion> {
} finally {
lock.writeLock().unlock();
}
}));
});
}
private static final String LOADER_META_URL = "https://meta.quiltmc.org/v3/versions/loader";

View File

@ -84,13 +84,19 @@ public abstract class FetchTask<T> extends Task<T> {
URI failedURI = null;
boolean checkETag;
switch (shouldCheckETag()) {
case CHECK_E_TAG: checkETag = true; break;
case NOT_CHECK_E_TAG: checkETag = false; break;
default: return;
case CHECK_E_TAG:
checkETag = true;
break;
case NOT_CHECK_E_TAG:
checkETag = false;
break;
default:
return;
}
int repeat = 0;
download: for (URI uri : uris) {
download:
for (URI uri : uris) {
for (int retryTime = 0; retryTime < retry; retryTime++) {
if (isCancelled()) {
break download;
@ -116,9 +122,10 @@ public abstract class FetchTask<T> extends Task<T> {
try {
Path cache = repository.getCachedRemoteFile(conn.getURL().toURI());
useCachedResult(cache);
LOG.info("Using cached file for " + NetworkUtils.dropQuery(uri));
return;
} catch (IOException e) {
LOG.warning("Unable to use cached file, redownload " + uri, e);
LOG.warning("Unable to use cached file, redownload " + NetworkUtils.dropQuery(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.

View File

@ -17,6 +17,9 @@
*/
package org.jackhuang.hmcl.task;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URI;
@ -29,7 +32,6 @@ import java.util.List;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
*
* @author huangyuhui
*/
public final class GetTask extends FetchTask<String> {
@ -95,4 +97,11 @@ public final class GetTask extends FetchTask<String> {
};
}
public <T> Task<T> thenGetJsonAsync(Class<T> type) {
return thenGetJsonAsync(TypeToken.get(type));
}
public <T> Task<T> thenGetJsonAsync(TypeToken<T> type) {
return thenApplyAsync(jsonString -> JsonUtils.fromNonNullJson(jsonString, type));
}
}

View File

@ -917,7 +917,8 @@ public abstract class Task<T> {
* @param tasks the Tasks
* @return a new Task that is completed when all of the given Tasks complete
*/
public static Task<List<Object>> allOf(Task<?>... tasks) {
@SafeVarargs
public static <T> Task<List<T>> allOf(Task<? extends T>... tasks) {
return allOf(Arrays.asList(tasks));
}
@ -932,8 +933,8 @@ public abstract class Task<T> {
* @param tasks the Tasks
* @return a new Task that is completed when all of the given Tasks complete
*/
public static Task<List<Object>> allOf(Collection<Task<?>> tasks) {
return new Task<List<Object>>() {
public static <T> Task<List<T>> allOf(Collection<? extends Task<? extends T>> tasks) {
return new Task<>() {
{
setSignificance(TaskSignificance.MINOR);
}
@ -944,7 +945,7 @@ public abstract class Task<T> {
}
@Override
public Collection<Task<?>> getDependents() {
public Collection<? extends Task<?>> getDependents() {
return tasks;
}
};

View File

@ -18,45 +18,42 @@
package org.jackhuang.hmcl.util;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSyntaxException;
import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.util.function.ExceptionalSupplier;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLConnection;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.FileTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.BiFunction;
import java.util.stream.Stream;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.jackhuang.hmcl.util.gson.JsonUtils.fromMaybeMalformedJson;
import static org.jackhuang.hmcl.util.gson.JsonUtils.fromNonNullJson;
import static org.jackhuang.hmcl.util.gson.JsonUtils.mapTypeOf;
import static org.jackhuang.hmcl.util.gson.JsonUtils.*;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class CacheRepository {
private Path commonDirectory;
private Path cacheDirectory;
private Path indexFile;
private Map<String, ETagItem> index;
private final Map<String, Storage> storages = new HashMap<>();
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private FileTime indexFileLastModified;
private LinkedHashMap<URI, ETagItem> index;
protected final ReadWriteLock lock = new ReentrantReadWriteLock();
public void changeDirectory(Path commonDir) {
commonDirectory = commonDir;
@ -65,25 +62,25 @@ public class CacheRepository {
lock.writeLock().lock();
try {
for (Storage storage : storages.values()) {
storage.changeDirectory(cacheDirectory);
}
if (Files.isRegularFile(indexFile)) {
ETagIndex raw = JsonUtils.fromJsonFile(indexFile, ETagIndex.class);
if (raw == null)
index = new HashMap<>();
else
index = joinETagIndexes(raw.eTag);
} else
index = new HashMap<>();
try (FileChannel channel = FileChannel.open(indexFile, StandardOpenOption.READ);
@SuppressWarnings("unused") FileLock lock = channel.tryLock(0, Long.MAX_VALUE, true)) {
FileTime lastModified = Lang.ignoringException(() -> Files.getLastModifiedTime(indexFile));
ETagIndex raw = JsonUtils.GSON.fromJson(new BufferedReader(Channels.newReader(channel, UTF_8)), ETagIndex.class);
index = raw != null ? joinETagIndexes(raw.eTag) : new LinkedHashMap<>();
indexFileLastModified = lastModified;
}
} else {
index = new LinkedHashMap<>();
indexFileLastModified = null;
}
} catch (IOException | JsonParseException e) {
LOG.warning("Unable to read index file", e);
index = new HashMap<>();
index = new LinkedHashMap<>();
indexFileLastModified = null;
} finally {
lock.writeLock().unlock();
}
}
public Path getCommonDirectory() {
@ -94,15 +91,6 @@ public class CacheRepository {
return cacheDirectory;
}
public Storage getStorage(String key) {
lock.readLock().lock();
try {
return storages.computeIfAbsent(key, Storage::new);
} finally {
lock.readLock().unlock();
}
}
protected Path getFile(String algorithm, String hash) {
return getCacheDirectory().resolve(algorithm).resolve(hash.substring(0, 2)).resolve(hash);
}
@ -165,7 +153,7 @@ public class CacheRepository {
lock.readLock().lock();
ETagItem eTagItem;
try {
eTagItem = index.get(uri.toString());
eTagItem = index.get(NetworkUtils.dropQuery(uri));
} finally {
lock.readLock().unlock();
}
@ -181,20 +169,27 @@ public class CacheRepository {
}
public void removeRemoteEntry(URI uri) {
lock.readLock().lock();
lock.writeLock().lock();
try {
index.remove(uri.toString());
index.remove(NetworkUtils.dropQuery(uri));
} finally {
lock.readLock().unlock();
lock.writeLock().unlock();
}
}
public void injectConnection(URLConnection conn) {
String url = conn.getURL().toString();
lock.readLock().lock();
ETagItem eTagItem;
conn.setUseCaches(true);
URI uri;
try {
eTagItem = index.get(url);
uri = NetworkUtils.dropQuery(conn.getURL().toURI());
} catch (URISyntaxException e) {
return;
}
ETagItem eTagItem;
lock.readLock().lock();
try {
eTagItem = index.get(uri);
} finally {
lock.readLock().unlock();
}
@ -228,22 +223,26 @@ public class CacheRepository {
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();
if (StringUtils.isBlank(eTag)) return;
URI uri;
try {
uri = NetworkUtils.dropQuery(connection.getURL().toURI());
} catch (URISyntaxException e) {
throw new IOException(e);
}
String lastModified = connection.getHeaderField("Last-Modified");
CacheResult cacheResult = cacheSupplier.get();
ETagItem eTagItem = new ETagItem(uri, eTag, cacheResult.hash, Files.getLastModifiedTime(cacheResult.cachedFile).toMillis(), lastModified);
Lock writeLock = lock.writeLock();
writeLock.lock();
lock.writeLock().lock();
try {
index.compute(eTagItem.url, updateEntity(eTagItem));
index.compute(eTagItem.url, updateEntity(eTagItem, true));
saveETagIndex();
} finally {
writeLock.unlock();
lock.writeLock().unlock();
}
}
private static class CacheResult {
private static final class CacheResult {
public String hash;
public Path cachedFile;
@ -253,11 +252,11 @@ public class CacheRepository {
}
}
private BiFunction<String, ETagItem, ETagItem> updateEntity(ETagItem newItem) {
private BiFunction<URI, ETagItem, ETagItem> updateEntity(ETagItem newItem, boolean force) {
return (key, oldItem) -> {
if (oldItem == null) {
return newItem;
} else if (oldItem.compareTo(newItem) < 0) {
} else if (force || oldItem.compareTo(newItem) < 0) {
Path cached = getFile(SHA1, oldItem.hash);
try {
Files.deleteIfExists(cached);
@ -272,36 +271,44 @@ public class CacheRepository {
}
@SafeVarargs
private final Map<String, ETagItem> joinETagIndexes(Collection<ETagItem>... indexes) {
Map<String, ETagItem> eTags = new ConcurrentHashMap<>();
Stream<ETagItem> stream = Arrays.stream(indexes).filter(Objects::nonNull).map(Collection::stream)
.reduce(Stream.empty(), Stream::concat);
stream.forEach(eTag -> {
eTags.compute(eTag.url, updateEntity(eTag));
});
private LinkedHashMap<URI, ETagItem> joinETagIndexes(Collection<ETagItem>... indexes) {
var eTags = new LinkedHashMap<URI, ETagItem>();
for (Collection<ETagItem> eTagItems : indexes) {
if (eTagItems != null) {
for (ETagItem eTag : eTagItems) {
eTags.compute(eTag.url, updateEntity(eTag, false));
}
}
}
return eTags;
}
public void saveETagIndex() throws IOException {
try (FileChannel channel = FileChannel.open(indexFile, StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE)) {
FileLock lock = channel.lock();
try {
ETagIndex indexOnDisk = fromMaybeMalformedJson(new String(Channels.newInputStream(channel).readAllBytes(), UTF_8), ETagIndex.class);
Map<String, ETagItem> newIndex = joinETagIndexes(indexOnDisk == null ? null : indexOnDisk.eTag, index.values());
channel.truncate(0);
ByteBuffer writeTo = ByteBuffer.wrap(JsonUtils.GSON.toJson(new ETagIndex(newIndex.values())).getBytes(UTF_8));
while (writeTo.hasRemaining()) {
if (channel.write(writeTo) == 0) {
throw new IOException("No value is written");
try (FileChannel channel = FileChannel.open(indexFile, StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE);
@SuppressWarnings("unused") FileLock lock = channel.lock()) {
FileTime lastModified = Lang.ignoringException(() -> Files.getLastModifiedTime(indexFile));
if (indexFileLastModified == null || lastModified == null || indexFileLastModified.compareTo(lastModified) < 0) {
try {
ETagIndex indexOnDisk = GSON.fromJson(
// Should not be closed
new BufferedReader(Channels.newReader(channel, UTF_8)),
ETagIndex.class
);
if (indexOnDisk != null) {
index = joinETagIndexes(index.values(), indexOnDisk.eTag);
indexFileLastModified = lastModified;
}
} catch (JsonSyntaxException ignored) {
}
this.index = newIndex;
} finally {
lock.release();
}
channel.truncate(0);
BufferedWriter writer = new BufferedWriter(Channels.newWriter(channel, UTF_8));
JsonUtils.GSON.toJson(new ETagIndex(index.values()), writer);
writer.flush();
channel.force(true);
this.indexFileLastModified = Lang.ignoringException(() -> Files.getLastModifiedTime(indexFile));
}
}
@ -318,7 +325,7 @@ public class CacheRepository {
}
private static final class ETagItem {
private final String url;
private final URI url;
private final String eTag;
private final String hash;
@SerializedName("local")
@ -333,7 +340,7 @@ public class CacheRepository {
this(null, null, null, 0, null);
}
public ETagItem(String url, String eTag, String hash, long localLastModified, String remoteLastModified) {
public ETagItem(URI url, String eTag, String hash, long localLastModified, String remoteLastModified) {
this.url = url;
this.eTag = eTag;
this.hash = hash;
@ -348,8 +355,8 @@ public class CacheRepository {
ZonedDateTime thisTime = Lang.ignoringException(() -> ZonedDateTime.parse(remoteLastModified, DateTimeFormatter.RFC_1123_DATE_TIME), null);
ZonedDateTime otherTime = Lang.ignoringException(() -> ZonedDateTime.parse(other.remoteLastModified, DateTimeFormatter.RFC_1123_DATE_TIME), null);
if (thisTime == null && otherTime == null) return 0;
else if (thisTime == null) return -1;
else if (otherTime == null) return 1;
else if (thisTime == null) return 1;
else if (otherTime == null) return -1;
else return thisTime.compareTo(otherTime);
}
@ -369,80 +376,16 @@ public class CacheRepository {
public int hashCode() {
return Objects.hash(url, eTag, hash, localLastModified, remoteLastModified);
}
}
/**
* Universal cache
*/
public static final class Storage {
private final String name;
private Map<String, Object> storage;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private Path indexFile;
public Storage(String name) {
this.name = name;
}
public Object getEntry(String key) {
lock.readLock().lock();
try {
return storage.get(key);
} finally {
lock.readLock().unlock();
}
}
public void putEntry(String key, Object value) {
lock.writeLock().lock();
try {
storage.put(key, value);
saveToFile();
} finally {
lock.writeLock().unlock();
}
}
private void joinEntries(Map<String, Object> storage) {
this.storage.putAll(storage);
}
private void changeDirectory(Path cacheDirectory) {
lock.writeLock().lock();
try {
indexFile = cacheDirectory.resolve(name + ".json");
if (Files.isRegularFile(indexFile)) {
joinEntries(fromNonNullJson(Files.readString(indexFile), mapTypeOf(String.class, Object.class)));
}
} catch (IOException | JsonParseException e) {
LOG.warning("Unable to read storage {" + name + "} file");
} finally {
lock.writeLock().unlock();
}
}
public void saveToFile() {
try (FileChannel channel = FileChannel.open(indexFile, StandardOpenOption.READ, StandardOpenOption.WRITE)) {
FileLock lock = channel.lock();
try {
Map<String, Object> indexOnDisk = fromMaybeMalformedJson(new String(Channels.newInputStream(channel).readAllBytes(), UTF_8), mapTypeOf(String.class, Object.class));
if (indexOnDisk == null) indexOnDisk = new HashMap<>();
indexOnDisk.putAll(storage);
channel.truncate(0);
ByteBuffer writeTo = ByteBuffer.wrap(JsonUtils.GSON.toJson(storage).getBytes(UTF_8));
while (writeTo.hasRemaining()) {
if (channel.write(writeTo) == 0) {
throw new IOException("No value is written");
}
}
this.storage = indexOnDisk;
} finally {
lock.release();
}
} catch (IOException e) {
LOG.warning("Unable to write storage {" + name + "} file");
}
@Override
public String toString() {
return "ETagItem[" +
"url='" + url + '\'' +
", eTag='" + eTag + '\'' +
", hash='" + hash + '\'' +
", localLastModified=" + localLastModified +
", remoteLastModified='" + remoteLastModified + '\'' +
']';
}
}

View File

@ -26,6 +26,7 @@ import java.util.*;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.jackhuang.hmcl.util.Pair.pair;
@ -68,6 +69,10 @@ public final class NetworkUtils {
return sb.toString();
}
public static List<URI> withQuery(List<URI> list, Map<String, String> params) {
return list.stream().map(uri -> URI.create(withQuery(uri.toString(), params))).collect(Collectors.toList());
}
public static List<Pair<String, String>> parseQuery(URI uri) {
return parseQuery(uri.getRawQuery());
}
@ -93,9 +98,20 @@ public final class NetworkUtils {
return result;
}
public static URI dropQuery(URI u) {
if (u.getRawQuery() == null && u.getRawFragment() == null) {
return u;
}
try {
return new URI(u.getScheme(), u.getUserInfo(), u.getHost(), u.getPort(), u.getPath(), null, null);
} catch (URISyntaxException e) {
throw new AssertionError("Unreachable", e);
}
}
public static URLConnection createConnection(URI uri) throws IOException {
URLConnection connection = uri.toURL().openConnection();
connection.setUseCaches(false);
connection.setConnectTimeout(TIME_OUT);
connection.setReadTimeout(TIME_OUT);
connection.setRequestProperty("Accept-Language", Locale.getDefault().toLanguageTag());
@ -152,9 +168,10 @@ public final class NetworkUtils {
* @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 {
final boolean useCache = conn.getUseCaches();
int redirect = 0;
while (true) {
conn.setUseCaches(false);
conn.setUseCaches(useCache);
conn.setConnectTimeout(TIME_OUT);
conn.setReadTimeout(TIME_OUT);
conn.setInstanceFollowRedirects(false);