diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java index 5ec976168..ac80067fb 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java @@ -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 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 settings) { settings.remove(libraryId); - if (executor != null) - executor.cancel(true); + // fixme +// if (executor != null) +// executor.cancel(true); } private void onRefresh() { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultCacheRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultCacheRepository.java index de9ffaffb..32bb627b4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultCacheRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultCacheRepository.java @@ -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 libraries; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java index 8b665aec9..995189c1b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java @@ -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)); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MultipleSourceVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MultipleSourceVersionList.java index cde656bf3..b6ca45a46 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MultipleSourceVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MultipleSourceVersionList.java @@ -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 { } @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 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.>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); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/VersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/VersionList.java index b41e835cc..5519b8663 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/VersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/VersionList.java @@ -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 The subclass of {@code RemoteVersion}, the type of RemoteVersion. - * * @author huangyuhui */ public abstract class VersionList { @@ -48,6 +47,7 @@ public abstract class VersionList { /** * 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 { /** * @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 getVersionsImpl(String gameVersion) { @@ -123,7 +115,7 @@ public abstract class VersionList { /** * 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. */ diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricAPIVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricAPIVersionList.java index 74c862874..8c092822e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricAPIVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricAPIVersionList.java @@ -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 { @@ -42,14 +40,14 @@ public class FabricAPIVersionList extends VersionList { } @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()))); } } - })); + }); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricVersionList.java index 8440f70b7..b71a7fb30 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricVersionList.java @@ -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 { @@ -45,8 +44,8 @@ public final class FabricVersionList extends VersionList { } @Override - public CompletableFuture refreshAsync() { - return CompletableFuture.runAsync(wrap(() -> { + public Task refreshAsync() { + return Task.runAsync(() -> { List gameVersions = getGameVersions(GAME_META_URL); List loaderVersions = getGameVersions(LOADER_META_URL); @@ -60,7 +59,7 @@ public final class FabricVersionList extends VersionList { } finally { lock.writeLock().unlock(); } - })); + }); } private static final String LOADER_META_URL = "https://meta.fabricmc.net/v2/versions/loader"; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeBMCLVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeBMCLVersionList.java index c0d0fdc8b..447599099 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeBMCLVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeBMCLVersionList.java @@ -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 } @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 } @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 { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeVersionList.java index acb97fef3..cf58b60cf 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeVersionList.java @@ -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 { } @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 { }); } - 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/"); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameVersionList.java index bfcb0bfa3..30ff1bdb1 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameVersionList.java @@ -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 { } @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; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/VersionJsonDownloadTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/VersionJsonDownloadTask.java index f91b6e426..112295a15 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/VersionJsonDownloadTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/VersionJsonDownloadTask.java @@ -44,7 +44,7 @@ public final class VersionJsonDownloadTask extends Task { this.dependencyManager = dependencyManager; this.gameVersionList = dependencyManager.getVersionList("game"); - dependents.add(Task.fromCompletableFuture(gameVersionList.loadAsync(gameVersion))); + dependents.add(gameVersionList.loadAsync(gameVersion)); setSignificance(TaskSignificance.MODERATE); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderBMCLVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderBMCLVersionList.java index 8ede1acce..d8d5d04b7 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderBMCLVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderBMCLVersionList.java @@ -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 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(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderVersionList.java index 3f95c06d5..2310a238a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderVersionList.java @@ -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 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 refreshAsync() { + public Task refreshAsync() { throw new UnsupportedOperationException(); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeBMCLVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeBMCLVersionList.java index a00f2eaeb..6db11dcb1 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeBMCLVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeBMCLVersionList.java @@ -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 { @@ -47,12 +47,12 @@ public final class NeoForgeBMCLVersionList extends VersionList 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 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(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeOfficialVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeOfficialVersionList.java index d853b9d3e..3a20320a6 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeOfficialVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeOfficialVersionList.java @@ -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 { @@ -37,17 +37,17 @@ public final class NeoForgeOfficialVersionList extends VersionList 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 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 { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltAPIVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltAPIVersionList.java index 9435f5c86..04fc0327a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltAPIVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltAPIVersionList.java @@ -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 { @@ -42,14 +40,14 @@ public class QuiltAPIVersionList extends VersionList { } @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()))); } } - })); + }); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltVersionList.java index f7d6a2e6b..c33d0acf0 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltVersionList.java @@ -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 { @@ -45,8 +44,8 @@ public final class QuiltVersionList extends VersionList { } @Override - public CompletableFuture refreshAsync() { - return CompletableFuture.runAsync(wrap(() -> { + public Task refreshAsync() { + return Task.runAsync(() -> { List gameVersions = getGameVersions(GAME_META_URL); List loaderVersions = getGameVersions(LOADER_META_URL); @@ -60,7 +59,7 @@ public final class QuiltVersionList extends VersionList { } finally { lock.writeLock().unlock(); } - })); + }); } private static final String LOADER_META_URL = "https://meta.quiltmc.org/v3/versions/loader"; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/FetchTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/FetchTask.java index b3a0ff1e8..b7f27dc72 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/FetchTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/FetchTask.java @@ -84,13 +84,19 @@ public abstract class FetchTask extends Task { 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 extends Task { 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. diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/GetTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/GetTask.java index d5505420f..c0fab32db 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/GetTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/GetTask.java @@ -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 { @@ -95,4 +97,11 @@ public final class GetTask extends FetchTask { }; } + public Task thenGetJsonAsync(Class type) { + return thenGetJsonAsync(TypeToken.get(type)); + } + + public Task thenGetJsonAsync(TypeToken type) { + return thenApplyAsync(jsonString -> JsonUtils.fromNonNullJson(jsonString, type)); + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java index 4f4d793b3..422308d80 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java @@ -917,7 +917,8 @@ public abstract class Task { * @param tasks the Tasks * @return a new Task that is completed when all of the given Tasks complete */ - public static Task> allOf(Task... tasks) { + @SafeVarargs + public static Task> allOf(Task... tasks) { return allOf(Arrays.asList(tasks)); } @@ -932,8 +933,8 @@ public abstract class Task { * @param tasks the Tasks * @return a new Task that is completed when all of the given Tasks complete */ - public static Task> allOf(Collection> tasks) { - return new Task>() { + public static Task> allOf(Collection> tasks) { + return new Task<>() { { setSignificance(TaskSignificance.MINOR); } @@ -944,7 +945,7 @@ public abstract class Task { } @Override - public Collection> getDependents() { + public Collection> getDependents() { return tasks; } }; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/CacheRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/CacheRepository.java index 01da58ce7..8049f8d81 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/CacheRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/CacheRepository.java @@ -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 index; - private final Map storages = new HashMap<>(); - private final ReadWriteLock lock = new ReentrantReadWriteLock(); + private FileTime indexFileLastModified; + private LinkedHashMap 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 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 updateEntity(ETagItem newItem) { + private BiFunction 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 joinETagIndexes(Collection... indexes) { - Map eTags = new ConcurrentHashMap<>(); - - Stream 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 joinETagIndexes(Collection... indexes) { + var eTags = new LinkedHashMap(); + for (Collection 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 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 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 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 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 + '\'' + + ']'; } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/NetworkUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/NetworkUtils.java index d2c82bd19..034508ad4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/NetworkUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/NetworkUtils.java @@ -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 withQuery(List list, Map params) { + return list.stream().map(uri -> URI.create(withQuery(uri.toString(), params))).collect(Collectors.toList()); + } + public static List> 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 Issue with libcurl */ public static HttpURLConnection resolveConnection(HttpURLConnection conn, List 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);