library download optimization
This commit is contained in:
@@ -31,10 +31,7 @@ import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask;
|
|||||||
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
|
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
|
||||||
import org.jackhuang.hmcl.game.HMCLModpackExportTask;
|
import org.jackhuang.hmcl.game.HMCLModpackExportTask;
|
||||||
import org.jackhuang.hmcl.game.HMCLModpackInstallTask;
|
import org.jackhuang.hmcl.game.HMCLModpackInstallTask;
|
||||||
import org.jackhuang.hmcl.mod.CurseCompletionTask;
|
import org.jackhuang.hmcl.mod.*;
|
||||||
import org.jackhuang.hmcl.mod.CurseInstallTask;
|
|
||||||
import org.jackhuang.hmcl.mod.MinecraftInstanceTask;
|
|
||||||
import org.jackhuang.hmcl.mod.MultiMCModpackInstallTask;
|
|
||||||
import org.jackhuang.hmcl.task.Task;
|
import org.jackhuang.hmcl.task.Task;
|
||||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||||
import org.jackhuang.hmcl.task.TaskListener;
|
import org.jackhuang.hmcl.task.TaskListener;
|
||||||
@@ -60,7 +57,7 @@ public final class TaskListPane extends StackPane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onReady(Task task) {
|
public void onRunning(Task task) {
|
||||||
if (!task.getSignificance().shouldShow())
|
if (!task.getSignificance().shouldShow())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -76,6 +73,8 @@ public final class TaskListPane extends StackPane {
|
|||||||
task.setName(Main.i18n("install.installer.install", Main.i18n("install.installer.optifine")));
|
task.setName(Main.i18n("install.installer.install", Main.i18n("install.installer.optifine")));
|
||||||
} else if (task instanceof CurseCompletionTask) {
|
} else if (task instanceof CurseCompletionTask) {
|
||||||
task.setName(Main.i18n("modpack.type.curse.completion"));
|
task.setName(Main.i18n("modpack.type.curse.completion"));
|
||||||
|
} else if (task instanceof ModpackInstallTask) {
|
||||||
|
task.setName(Main.i18n("modpack.installing"));
|
||||||
} else if (task instanceof CurseInstallTask) {
|
} else if (task instanceof CurseInstallTask) {
|
||||||
task.setName(Main.i18n("modpack.install", Main.i18n("modpack.type.curse")));
|
task.setName(Main.i18n("modpack.install", Main.i18n("modpack.type.curse")));
|
||||||
} else if (task instanceof MultiMCModpackInstallTask) {
|
} else if (task instanceof MultiMCModpackInstallTask) {
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ public class DefaultGameBuilder extends GameBuilder {
|
|||||||
public Task buildAsync() {
|
public Task buildAsync() {
|
||||||
return new VersionJsonDownloadTask(gameVersion, dependencyManager).then(variables -> {
|
return new VersionJsonDownloadTask(gameVersion, dependencyManager).then(variables -> {
|
||||||
Version version = Constants.GSON.fromJson(variables.<String>get(VersionJsonDownloadTask.ID), Version.class);
|
Version version = Constants.GSON.fromJson(variables.<String>get(VersionJsonDownloadTask.ID), Version.class);
|
||||||
version = version.setId(name).setJar(null).resolve(new SimpleVersionProvider());
|
version = version.setId(name).setJar(null);
|
||||||
variables.set("version", version);
|
variables.set("version", version);
|
||||||
Task result = new ParallelTask(
|
Task result = new ParallelTask(
|
||||||
new GameAssetDownloadTask(dependencyManager, version),
|
new GameAssetDownloadTask(dependencyManager, version),
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ public final class GameAssetDownloadTask extends Task {
|
|||||||
*/
|
*/
|
||||||
public GameAssetDownloadTask(AbstractDependencyManager dependencyManager, Version version) {
|
public GameAssetDownloadTask(AbstractDependencyManager dependencyManager, Version version) {
|
||||||
this.dependencyManager = dependencyManager;
|
this.dependencyManager = dependencyManager;
|
||||||
this.version = version.requireResolved();
|
this.version = version;
|
||||||
this.refreshTask = new GameAssetRefreshTask(dependencyManager, version);
|
this.refreshTask = new GameAssetRefreshTask(dependencyManager, version);
|
||||||
this.dependents.add(refreshTask);
|
this.dependents.add(refreshTask);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ public final class GameAssetIndexDownloadTask extends Task {
|
|||||||
*/
|
*/
|
||||||
public GameAssetIndexDownloadTask(AbstractDependencyManager dependencyManager, Version version) {
|
public GameAssetIndexDownloadTask(AbstractDependencyManager dependencyManager, Version version) {
|
||||||
this.dependencyManager = dependencyManager;
|
this.dependencyManager = dependencyManager;
|
||||||
this.version = version.requireResolved();
|
this.version = version;
|
||||||
setSignificance(TaskSignificance.MODERATE);
|
setSignificance(TaskSignificance.MODERATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ public final class GameAssetRefreshTask extends TaskResult<Collection<Pair<File,
|
|||||||
*/
|
*/
|
||||||
public GameAssetRefreshTask(AbstractDependencyManager dependencyManager, Version version) {
|
public GameAssetRefreshTask(AbstractDependencyManager dependencyManager, Version version) {
|
||||||
this.dependencyManager = dependencyManager;
|
this.dependencyManager = dependencyManager;
|
||||||
this.version = version.requireResolved();
|
this.version = version;
|
||||||
this.assetIndexInfo = version.getAssetIndex();
|
this.assetIndexInfo = version.getAssetIndex();
|
||||||
this.assetIndexFile = dependencyManager.getGameRepository().getIndexFile(version.getId(), assetIndexInfo.getId());
|
this.assetIndexFile = dependencyManager.getGameRepository().getIndexFile(version.getId(), assetIndexInfo.getId());
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ public final class GameLibrariesTask extends Task {
|
|||||||
*/
|
*/
|
||||||
public GameLibrariesTask(AbstractDependencyManager dependencyManager, Version version) {
|
public GameLibrariesTask(AbstractDependencyManager dependencyManager, Version version) {
|
||||||
this.dependencyManager = dependencyManager;
|
this.dependencyManager = dependencyManager;
|
||||||
this.version = version.requireResolved();
|
this.version = version;
|
||||||
setSignificance(TaskSignificance.MODERATE);
|
setSignificance(TaskSignificance.MODERATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ public final class GameLoggingDownloadTask extends Task {
|
|||||||
*/
|
*/
|
||||||
public GameLoggingDownloadTask(DependencyManager dependencyManager, Version version) {
|
public GameLoggingDownloadTask(DependencyManager dependencyManager, Version version) {
|
||||||
this.dependencyManager = dependencyManager;
|
this.dependencyManager = dependencyManager;
|
||||||
this.version = version.requireResolved();
|
this.version = version;
|
||||||
setSignificance(TaskSignificance.MODERATE);
|
setSignificance(TaskSignificance.MODERATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.download.game;
|
|||||||
import org.jackhuang.hmcl.game.Library;
|
import org.jackhuang.hmcl.game.Library;
|
||||||
import org.jackhuang.hmcl.game.Version;
|
import org.jackhuang.hmcl.game.Version;
|
||||||
import org.jackhuang.hmcl.task.TaskResult;
|
import org.jackhuang.hmcl.task.TaskResult;
|
||||||
|
import org.jackhuang.hmcl.util.Constants;
|
||||||
import org.jackhuang.hmcl.util.SimpleMultimap;
|
import org.jackhuang.hmcl.util.SimpleMultimap;
|
||||||
import org.jackhuang.hmcl.util.VersionNumber;
|
import org.jackhuang.hmcl.util.VersionNumber;
|
||||||
|
|
||||||
@@ -49,6 +50,7 @@ public class LibrariesUniqueTask extends TaskResult<Version> {
|
|||||||
for (Library library : libraries) {
|
for (Library library : libraries) {
|
||||||
String id = library.getGroupId() + ":" + library.getArtifactId();
|
String id = library.getGroupId() + ":" + library.getArtifactId();
|
||||||
VersionNumber number = VersionNumber.asVersion(library.getVersion());
|
VersionNumber number = VersionNumber.asVersion(library.getVersion());
|
||||||
|
String serialized = Constants.GSON.toJson(library);
|
||||||
|
|
||||||
if (versionMap.containsKey(id)) {
|
if (versionMap.containsKey(id)) {
|
||||||
VersionNumber otherNumber = versionMap.get(id);
|
VersionNumber otherNumber = versionMap.get(id);
|
||||||
@@ -61,8 +63,15 @@ public class LibrariesUniqueTask extends TaskResult<Version> {
|
|||||||
// prevent from duplicated libraries
|
// prevent from duplicated libraries
|
||||||
for (Library otherLibrary : multimap.get(id))
|
for (Library otherLibrary : multimap.get(id))
|
||||||
if (library.equals(otherLibrary)) {
|
if (library.equals(otherLibrary)) {
|
||||||
flag = true;
|
String otherSerialized = Constants.GSON.toJson(otherLibrary);
|
||||||
break;
|
// A trick, the library that has more information is better, which can be
|
||||||
|
// considered whose serialized JSON text will be longer.
|
||||||
|
if (serialized.length() <= otherSerialized.length()) {
|
||||||
|
flag = true;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
multimap.removeValue(id, otherLibrary);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!flag)
|
if (!flag)
|
||||||
multimap.put(id, library);
|
multimap.put(id, library);
|
||||||
|
|||||||
@@ -24,22 +24,32 @@ public final class LibraryDownloadTask extends Task {
|
|||||||
private final File xzFile;
|
private final File xzFile;
|
||||||
private final Library library;
|
private final Library library;
|
||||||
|
|
||||||
|
private boolean downloaded = false;
|
||||||
|
|
||||||
public LibraryDownloadTask(AbstractDependencyManager dependencyManager, File file, Library library) {
|
public LibraryDownloadTask(AbstractDependencyManager dependencyManager, File file, Library library) {
|
||||||
|
if (library.is("net.minecraftforge", "forge"))
|
||||||
|
library = library.setClassifier("universal");
|
||||||
|
|
||||||
|
this.library = library;
|
||||||
|
|
||||||
String url = dependencyManager.getDownloadProvider().injectURL(library.getDownload().getUrl());
|
String url = dependencyManager.getDownloadProvider().injectURL(library.getDownload().getUrl());
|
||||||
jar = file;
|
jar = file;
|
||||||
this.library = library;
|
|
||||||
xzFile = new File(file.getAbsoluteFile().getParentFile(), file.getName() + ".pack.xz");
|
xzFile = new File(file.getAbsoluteFile().getParentFile(), file.getName() + ".pack.xz");
|
||||||
|
|
||||||
xzTask = new FileDownloadTask(NetworkUtils.toURL(url + ".pack.xz"),
|
xzTask = new FileDownloadTask(NetworkUtils.toURL(url + ".pack.xz"),
|
||||||
xzFile, dependencyManager.getProxy(), null, 1);
|
xzFile, dependencyManager.getProxy(), null, 1);
|
||||||
|
xzTask.setSignificance(TaskSignificance.MINOR);
|
||||||
|
|
||||||
task = new FileDownloadTask(NetworkUtils.toURL(url + ".pack.xz"),
|
setSignificance(TaskSignificance.MODERATE);
|
||||||
|
|
||||||
|
task = new FileDownloadTask(NetworkUtils.toURL(url),
|
||||||
file, dependencyManager.getProxy(), library.getDownload().getSha1());
|
file, dependencyManager.getProxy(), library.getDownload().getSha1());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<? extends Task> getDependents() {
|
public Collection<? extends Task> getDependents() {
|
||||||
return Collections.singleton(xzTask);
|
return library.getChecksums() != null ? Collections.singleton(xzTask) : Collections.emptySet();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -47,23 +57,20 @@ public final class LibraryDownloadTask extends Task {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public Scheduler getScheduler() {
|
|
||||||
return Schedulers.io();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void execute() throws Exception {
|
public void execute() throws Exception {
|
||||||
if (isDependentsSucceeded()) {
|
if (isDependentsSucceeded() && library.getChecksums() != null) {
|
||||||
unpackLibrary(jar, FileUtils.readBytes(xzFile));
|
unpackLibrary(jar, FileUtils.readBytes(xzFile));
|
||||||
if (!checksumValid(jar, library.getChecksums()))
|
if (!checksumValid(jar, library.getChecksums()))
|
||||||
throw new IOException("Checksum failed for " + library);
|
throw new IOException("Checksum failed for " + library);
|
||||||
|
|
||||||
|
downloaded = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<? extends Task> getDependencies() {
|
public Collection<? extends Task> getDependencies() {
|
||||||
return isDependentsSucceeded() ? Collections.emptySet() : Collections.singleton(task);
|
return downloaded ? Collections.emptySet() : Collections.singleton(task);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean checksumValid(File libPath, List<String> checksums) {
|
private static boolean checksumValid(File libPath, List<String> checksums) {
|
||||||
|
|||||||
@@ -40,11 +40,11 @@ public final class VersionJsonSaveTask extends Task {
|
|||||||
* Constructor.
|
* Constructor.
|
||||||
*
|
*
|
||||||
* @param repository the game repository
|
* @param repository the game repository
|
||||||
* @param version the **resolved** version
|
* @param version the game version
|
||||||
*/
|
*/
|
||||||
public VersionJsonSaveTask(DefaultGameRepository repository, Version version) {
|
public VersionJsonSaveTask(DefaultGameRepository repository, Version version) {
|
||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
this.version = version.requireResolved();
|
this.version = version;
|
||||||
|
|
||||||
setSignificance(TaskSignificance.MODERATE);
|
setSignificance(TaskSignificance.MODERATE);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,6 +147,10 @@ public class Library implements Comparable<Library> {
|
|||||||
return checksums;
|
return checksums;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean is(String groupId, String artifactId) {
|
||||||
|
return this.groupId.equals(groupId) && this.artifactId.equals(artifactId);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return new ToStringBuilder(this).append("name", getName()).toString();
|
return new ToStringBuilder(this).append("name", getName()).toString();
|
||||||
@@ -174,6 +178,10 @@ public class Library implements Comparable<Library> {
|
|||||||
return Objects.hash(getName(), isNative());
|
return Objects.hash(getName(), isNative());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Library setClassifier(String classifier) {
|
||||||
|
return new Library(groupId, artifactId, version, classifier, url, downloads, lateload, checksums, extract, natives, rules);
|
||||||
|
}
|
||||||
|
|
||||||
public static Library fromName(String name) {
|
public static Library fromName(String name) {
|
||||||
return fromName(name, null, null, null, null, null, null);
|
return fromName(name, null, null, null, null, null, null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,13 +143,6 @@ public class Version implements Comparable<Version>, Validation {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Version requireResolved() {
|
|
||||||
if (!resolved)
|
|
||||||
throw new RuntimeException("Version not resolved");
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve given version
|
* Resolve given version
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -220,7 +220,6 @@ public class FileDownloadTask extends Task {
|
|||||||
} catch (IOException | IllegalStateException e) {
|
} catch (IOException | IllegalStateException e) {
|
||||||
if (temp != null)
|
if (temp != null)
|
||||||
temp.delete();
|
temp.delete();
|
||||||
Logging.LOG.log(Level.WARNING, "Unable to download file " + currentURL, e);
|
|
||||||
exception = e;
|
exception = e;
|
||||||
} finally {
|
} finally {
|
||||||
closeFiles();
|
closeFiles();
|
||||||
@@ -228,7 +227,7 @@ public class FileDownloadTask extends Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (exception != null)
|
if (exception != null)
|
||||||
throw exception;
|
throw new IOException("Unable to download file " + currentURL, exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ public abstract class Task {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The collection of sub-tasks that should execute **after** this task running.
|
* The collection of sub-tasks that should execute **after** this task running.
|
||||||
|
* Will not be executed if execution fails.
|
||||||
*/
|
*/
|
||||||
public Collection<? extends Task> getDependencies() {
|
public Collection<? extends Task> getDependencies() {
|
||||||
return Collections.emptySet();
|
return Collections.emptySet();
|
||||||
@@ -350,6 +351,7 @@ public abstract class Task {
|
|||||||
public enum TaskState {
|
public enum TaskState {
|
||||||
READY,
|
READY,
|
||||||
RUNNING,
|
RUNNING,
|
||||||
|
EXECUTED,
|
||||||
SUCCEEDED,
|
SUCCEEDED,
|
||||||
FAILED
|
FAILED
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ public final class TaskExecutor {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
task.setState(Task.TaskState.RUNNING);
|
task.setState(Task.TaskState.READY);
|
||||||
|
|
||||||
if (task.getSignificance().shouldLog())
|
if (task.getSignificance().shouldLog())
|
||||||
Logging.LOG.log(Level.FINE, "Executing task: {0}", task.getName());
|
Logging.LOG.log(Level.FINE, "Executing task: {0}", task.getName());
|
||||||
@@ -150,6 +150,9 @@ public final class TaskExecutor {
|
|||||||
|
|
||||||
task.setVariables(variables);
|
task.setVariables(variables);
|
||||||
|
|
||||||
|
task.setState(Task.TaskState.RUNNING);
|
||||||
|
|
||||||
|
taskListeners.forEach(it -> it.onRunning(task));
|
||||||
try {
|
try {
|
||||||
task.getScheduler().schedule(task::execute).get();
|
task.getScheduler().schedule(task::execute).get();
|
||||||
} catch (ExecutionException e) {
|
} catch (ExecutionException e) {
|
||||||
@@ -157,6 +160,8 @@ public final class TaskExecutor {
|
|||||||
throw (Exception) e.getCause();
|
throw (Exception) e.getCause();
|
||||||
else
|
else
|
||||||
throw e;
|
throw e;
|
||||||
|
} finally {
|
||||||
|
task.setState(Task.TaskState.EXECUTED);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (task instanceof TaskResult<?>) {
|
if (task instanceof TaskResult<?>) {
|
||||||
@@ -172,22 +177,24 @@ public final class TaskExecutor {
|
|||||||
flag = true;
|
flag = true;
|
||||||
if (task.getSignificance().shouldLog()) {
|
if (task.getSignificance().shouldLog()) {
|
||||||
Logging.LOG.log(Level.FINER, "Task finished: {0}", task.getName());
|
Logging.LOG.log(Level.FINER, "Task finished: {0}", task.getName());
|
||||||
|
|
||||||
task.onDone().fireEvent(new TaskEvent(this, task, false));
|
|
||||||
taskListeners.forEach(it -> it.onFinished(task));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
task.onDone().fireEvent(new TaskEvent(this, task, false));
|
||||||
|
taskListeners.forEach(it -> it.onFinished(task));
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
if (task.getSignificance().shouldLog()) {
|
if (task.getSignificance().shouldLog()) {
|
||||||
Logging.LOG.log(Level.FINE, "Task aborted: " + task.getName());
|
Logging.LOG.log(Level.FINE, "Task aborted: " + task.getName());
|
||||||
task.onDone().fireEvent(new TaskEvent(this, task, true));
|
|
||||||
taskListeners.forEach(it -> it.onFailed(task, e));
|
|
||||||
}
|
}
|
||||||
|
task.onDone().fireEvent(new TaskEvent(this, task, true));
|
||||||
|
taskListeners.forEach(it -> it.onFailed(task, e));
|
||||||
} catch (SilentException | RejectedExecutionException e) {
|
} catch (SilentException | RejectedExecutionException e) {
|
||||||
// do nothing
|
// do nothing
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
lastException = e;
|
lastException = e;
|
||||||
variables.set(LAST_EXCEPTION_ID, e);
|
variables.set(LAST_EXCEPTION_ID, e);
|
||||||
Logging.LOG.log(Level.FINE, "Task failed: " + task.getName(), e);
|
if (task.getSignificance().shouldLog()) {
|
||||||
|
Logging.LOG.log(Level.FINE, "Task failed: " + task.getName(), e);
|
||||||
|
}
|
||||||
task.onDone().fireEvent(new TaskEvent(this, task, true));
|
task.onDone().fireEvent(new TaskEvent(this, task, true));
|
||||||
taskListeners.forEach(it -> it.onFailed(task, e));
|
taskListeners.forEach(it -> it.onFailed(task, e));
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ public abstract class TaskListener implements EventListener {
|
|||||||
public void onReady(Task task) {
|
public void onReady(Task task) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void onRunning(Task task) {
|
||||||
|
}
|
||||||
|
|
||||||
public void onFinished(Task task) {
|
public void onFinished(Task task) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -74,9 +74,15 @@ public final class SimpleMultimap<K, V> {
|
|||||||
return map.remove(key);
|
return map.remove(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeValue(V value) {
|
public boolean removeValue(V value) {
|
||||||
|
boolean flag = false;
|
||||||
for (Collection<V> c : map.values())
|
for (Collection<V> c : map.values())
|
||||||
c.remove(value);
|
flag |= c.remove(value);
|
||||||
|
return flag;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean removeValue(K key, V value) {
|
||||||
|
return get(key).remove(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clear() {
|
public void clear() {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ public class ToStringBuilder {
|
|||||||
if (!first)
|
if (!first)
|
||||||
stringBuilder.append(", ");
|
stringBuilder.append(", ");
|
||||||
first = false;
|
first = false;
|
||||||
stringBuilder.append(name).append('=').append(content.toString());
|
stringBuilder.append(name).append('=').append(content);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user