add: support change download provider in install wizard

This commit is contained in:
huanghongxun
2020-04-12 00:37:42 +08:00
parent 986a430e92
commit c28dfd0ae0
26 changed files with 334 additions and 160 deletions

View File

@@ -17,23 +17,19 @@
*/
package org.jackhuang.hmcl.download;
import java.util.List;
/**
*
* @author huangyuhui
*/
public abstract class AbstractDependencyManager implements DependencyManager {
public abstract DownloadProvider getPrimaryDownloadProvider();
public abstract List<DownloadProvider> getPreferredDownloadProviders();
public abstract DownloadProvider getDownloadProvider();
@Override
public abstract DefaultCacheRepository getCacheRepository();
@Override
public VersionList<?> getVersionList(String id) {
return getPrimaryDownloadProvider().getVersionListById(id);
return getDownloadProvider().getVersionListById(id);
}
}

View File

@@ -0,0 +1,81 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.download;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
/**
* The download provider that changes the real download source in need.
*
* @author huangyuhui
*/
public class AdaptedDownloadProvider implements DownloadProvider {
private List<DownloadProvider> downloadProviderCandidates;
public void setDownloadProviderCandidates(List<DownloadProvider> downloadProviderCandidates) {
this.downloadProviderCandidates = new ArrayList<>(downloadProviderCandidates);
}
public DownloadProvider getPreferredDownloadProvider() {
List<DownloadProvider> d = downloadProviderCandidates;
if (d == null || d.isEmpty()) {
throw new IllegalStateException("No download provider candidate");
}
return d.get(0);
}
@Override
public String getVersionListURL() {
return getPreferredDownloadProvider().getVersionListURL();
}
@Override
public String getAssetBaseURL() {
return getPreferredDownloadProvider().getAssetBaseURL();
}
@Override
public String injectURL(String baseURL) {
return getPreferredDownloadProvider().injectURL(baseURL);
}
@Override
public List<URL> injectURLWithCandidates(String baseURL) {
List<DownloadProvider> d = downloadProviderCandidates;
List<URL> results = new ArrayList<>(d.size());
for (int i = 0; i < d.size(); i++) {
results.add(NetworkUtils.toURL(d.get(i).injectURL(baseURL)));
}
return results;
}
@Override
public VersionList<?> getVersionListById(String id) {
return getPreferredDownloadProvider().getVersionListById(id);
}
@Override
public int getConcurrency() {
return getPreferredDownloadProvider().getConcurrency();
}
}

View File

@@ -28,7 +28,6 @@ import org.jackhuang.hmcl.task.Task;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
/**
* Note: This class has no state.
@@ -39,13 +38,11 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
private final DefaultGameRepository repository;
private final DownloadProvider downloadProvider;
private final List<DownloadProvider> preferredDownloadProviders;
private final DefaultCacheRepository cacheRepository;
public DefaultDependencyManager(DefaultGameRepository repository, DownloadProvider downloadProvider, List<DownloadProvider> preferredDownloadProviders, DefaultCacheRepository cacheRepository) {
public DefaultDependencyManager(DefaultGameRepository repository, DownloadProvider downloadProvider, DefaultCacheRepository cacheRepository) {
this.repository = repository;
this.downloadProvider = downloadProvider;
this.preferredDownloadProviders = preferredDownloadProviders;
this.cacheRepository = cacheRepository;
}
@@ -55,15 +52,10 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
}
@Override
public DownloadProvider getPrimaryDownloadProvider() {
public DownloadProvider getDownloadProvider() {
return downloadProvider;
}
@Override
public List<DownloadProvider> getPreferredDownloadProviders() {
return preferredDownloadProviders;
}
@Override
public DefaultCacheRepository getCacheRepository() {
return cacheRepository;

View File

@@ -32,21 +32,15 @@ import java.util.Map;
public class DefaultGameBuilder extends GameBuilder {
private final DefaultDependencyManager dependencyManager;
private final DownloadProvider downloadProvider;
public DefaultGameBuilder(DefaultDependencyManager dependencyManager) {
this.dependencyManager = dependencyManager;
this.downloadProvider = dependencyManager.getPrimaryDownloadProvider();
}
public DefaultDependencyManager getDependencyManager() {
return dependencyManager;
}
public DownloadProvider getDownloadProvider() {
return downloadProvider;
}
@Override
public Task<?> buildAsync() {
List<String> stages = new ArrayList<>();

View File

@@ -17,6 +17,13 @@
*/
package org.jackhuang.hmcl.download;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.net.URL;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
/**
* The service provider that provides Minecraft online file downloads.
*
@@ -28,6 +35,10 @@ public interface DownloadProvider {
String getAssetBaseURL();
default List<URL> getAssetObjectCandidates(String assetObjectLocation) {
return Collections.singletonList(NetworkUtils.toURL(getAssetBaseURL() + assetObjectLocation));
}
/**
* Inject into original URL provided by Mojang and Forge.
*
@@ -40,9 +51,26 @@ public interface DownloadProvider {
String injectURL(String baseURL);
/**
* the specific version list that this download provider provides. i.e. "forge", "liteloader", "game", "optifine"
* Inject into original URL provided by Mojang and Forge.
*
* @param id the id of specific version list that this download provider provides. i.e. "forge", "liteloader", "game", "optifine"
* Since there are many provided URLs that are written in JSONs and are unmodifiable,
* this method provides a way to change them.
*
* @param baseURL original URL provided by Mojang and Forge.
* @return the URL that is equivalent to [baseURL], but belongs to your own service provider.
*/
default List<URL> injectURLWithCandidates(String baseURL) {
return Collections.singletonList(NetworkUtils.toURL(baseURL));
}
default List<URL> injectURLsWithCandidates(List<String> urls) {
return urls.stream().flatMap(url -> injectURLWithCandidates(url).stream()).collect(Collectors.toList());
}
/**
* the specific version list that this download provider provides. i.e. "fabric", "forge", "liteloader", "game", "optifine"
*
* @param id the id of specific version list that this download provider provides. i.e. "fabric", "forge", "liteloader", "game", "optifine"
* @return the version list
* @throws IllegalArgumentException if the version list does not exist
*/

View File

@@ -35,7 +35,7 @@ public class RemoteVersion implements Comparable<RemoteVersion> {
private final String libraryId;
private final String gameVersion;
private final String selfVersion;
private final String[] url;
private final List<String> urls;
private final Type type;
/**
@@ -43,10 +43,10 @@ public class RemoteVersion implements Comparable<RemoteVersion> {
*
* @param gameVersion the Minecraft version that this remote version suits.
* @param selfVersion the version string of the remote version.
* @param url the installer or universal jar URL.
* @param urls the installer or universal jar original URL.
*/
public RemoteVersion(String libraryId, String gameVersion, String selfVersion, String... url) {
this(libraryId, gameVersion, selfVersion, Type.UNCATEGORIZED, url);
public RemoteVersion(String libraryId, String gameVersion, String selfVersion, List<String> urls) {
this(libraryId, gameVersion, selfVersion, Type.UNCATEGORIZED, urls);
}
/**
@@ -54,13 +54,13 @@ public class RemoteVersion implements Comparable<RemoteVersion> {
*
* @param gameVersion the Minecraft version that this remote version suits.
* @param selfVersion the version string of the remote version.
* @param url the installer or universal jar URL.
* @param urls the installer or universal jar URL.
*/
public RemoteVersion(String libraryId, String gameVersion, String selfVersion, Type type, String... url) {
public RemoteVersion(String libraryId, String gameVersion, String selfVersion, Type type, List<String> urls) {
this.libraryId = Objects.requireNonNull(libraryId);
this.gameVersion = Objects.requireNonNull(gameVersion);
this.selfVersion = Objects.requireNonNull(selfVersion);
this.url = Objects.requireNonNull(url);
this.urls = Objects.requireNonNull(urls);
this.type = Objects.requireNonNull(type);
}
@@ -76,8 +76,8 @@ public class RemoteVersion implements Comparable<RemoteVersion> {
return selfVersion;
}
public String[] getUrl() {
return url;
public List<String> getUrls() {
return urls;
}
public Type getVersionType() {

View File

@@ -28,10 +28,8 @@ import org.jackhuang.hmcl.game.Version;
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.NetworkUtils;
import java.util.*;
import java.util.stream.Collectors;
/**
* <b>Note</b>: Fabric should be installed first.
@@ -51,9 +49,7 @@ public final class FabricInstallTask extends Task<Version> {
this.version = version;
this.remote = remoteVersion;
launchMetaTask = new GetTask(dependencyManager.getPreferredDownloadProviders().stream()
.map(downloadProvider -> downloadProvider.injectURL(getLaunchMetaUrl(remote.getGameVersion(), remote.getSelfVersion())))
.map(NetworkUtils::toURL).collect(Collectors.toList()))
launchMetaTask = new GetTask(dependencyManager.getDownloadProvider().injectURLsWithCandidates(remoteVersion.getUrls()))
.setCacheRepository(dependencyManager.getCacheRepository());
}
@@ -90,10 +86,6 @@ public final class FabricInstallTask extends Task<Version> {
dependencies.add(dependencyManager.checkLibraryCompletionAsync(getResult(), true));
}
private static String getLaunchMetaUrl(String gameVersion, String loaderVersion) {
return String.format("https://meta.fabricmc.net/v2/versions/loader/%s/%s", gameVersion, loaderVersion);
}
private Version getPatch(FabricInfo fabricInfo, String gameVersion, String loaderVersion) {
JsonObject launcherMeta = fabricInfo.launcherMeta;
Arguments arguments = new Arguments();

View File

@@ -48,7 +48,7 @@ public final class FabricVersionList extends VersionList<FabricRemoteVersion> {
public Task<?> refreshAsync() {
return new Task<Void>() {
@Override
public void execute() throws IOException, XMLStreamException {
public void execute() throws IOException {
List<String> gameVersions = getGameVersions(GAME_META_URL);
List<String> loaderVersions = getGameVersions(LOADER_META_URL);

View File

@@ -27,15 +27,15 @@ import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import static org.jackhuang.hmcl.util.StringUtils.removePrefix;
import static org.jackhuang.hmcl.util.StringUtils.removeSuffix;
@@ -69,9 +69,7 @@ public final class ForgeInstallTask extends Task<Version> {
installer = Files.createTempFile("forge-installer", ".jar");
dependent = new FileDownloadTask(
dependencyManager.getPreferredDownloadProviders().stream()
.flatMap(downloadProvider -> Arrays.stream(remote.getUrl()).map(downloadProvider::injectURL))
.map(NetworkUtils::toURL).collect(Collectors.toList()),
dependencyManager.getDownloadProvider().injectURLsWithCandidates(remote.getUrls()),
installer.toFile(), null)
.setCacheRepository(dependencyManager.getCacheRepository())
.setCaching(true);

View File

@@ -23,15 +23,17 @@ import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.Task;
import java.util.List;
public class ForgeRemoteVersion extends RemoteVersion {
/**
* Constructor.
*
* @param gameVersion the Minecraft version that this remote version suits.
* @param selfVersion the version string of the remote version.
* @param url the installer or universal jar URL.
* @param url the installer or universal jar original URL.
*/
public ForgeRemoteVersion(String gameVersion, String selfVersion, String... url) {
public ForgeRemoteVersion(String gameVersion, String selfVersion, List<String> url) {
super(LibraryAnalyzer.LibraryType.FORGE.getPatchId(), gameVersion, selfVersion, url);
}

View File

@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.download.game;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSyntaxException;
import org.jackhuang.hmcl.download.AbstractDependencyManager;
import org.jackhuang.hmcl.game.AssetIndex;
import org.jackhuang.hmcl.game.AssetIndexInfo;
@@ -30,7 +29,6 @@ import org.jackhuang.hmcl.util.CacheRepository;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.File;
import java.io.IOException;
@@ -39,7 +37,6 @@ import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import java.util.stream.Collectors;
/**
*
@@ -112,10 +109,7 @@ public final class GameAssetDownloadTask extends Task<Void> {
Logging.LOG.log(Level.WARNING, "Unable to calc hash value of file " + file.toPath(), e);
}
if (download) {
List<URL> urls = dependencyManager.getPreferredDownloadProviders().stream()
.map(downloadProvider -> downloadProvider.getAssetBaseURL() + assetObject.getLocation())
.map(NetworkUtils::toURL)
.collect(Collectors.toList());
List<URL> urls = dependencyManager.getDownloadProvider().getAssetObjectCandidates(assetObject.getLocation());
FileDownloadTask task = new FileDownloadTask(urls, file, new FileDownloadTask.IntegrityCheck("SHA-1", assetObject.getHash()));
task.setName(assetObject.getHash());

View File

@@ -22,7 +22,6 @@ import org.jackhuang.hmcl.game.AssetIndexInfo;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.File;
import java.io.IOException;
@@ -65,7 +64,7 @@ public final class GameAssetIndexDownloadTask extends Task<Void> {
// We should not check the hash code of asset index file since this file is not consistent
// And Mojang will modify this file anytime. So assetIndex.hash might be outdated.
dependencies.add(new FileDownloadTask(
NetworkUtils.toURL(dependencyManager.getPrimaryDownloadProvider().injectURL(assetIndexInfo.getUrl())),
dependencyManager.getDownloadProvider().injectURLWithCandidates(assetIndexInfo.getUrl()),
assetIndexFile
).setCacheRepository(dependencyManager.getCacheRepository()));
}

View File

@@ -23,13 +23,11 @@ import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.CacheRepository;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.File;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
/**
* Task to download Minecraft jar
@@ -59,9 +57,7 @@ public final class GameDownloadTask extends Task<Void> {
File jar = dependencyManager.getGameRepository().getVersionJar(version);
FileDownloadTask task = new FileDownloadTask(
dependencyManager.getPreferredDownloadProviders().stream()
.map(downloadProvider -> downloadProvider.injectURL(version.getDownloadInfo().getUrl()))
.map(NetworkUtils::toURL).collect(Collectors.toList()),
dependencyManager.getDownloadProvider().injectURLWithCandidates(version.getDownloadInfo().getUrl()),
jar,
IntegrityCheck.of(CacheRepository.SHA1, version.getDownloadInfo().getSha1()))
.setCaching(true)

View File

@@ -33,7 +33,7 @@ import org.tukaani.xz.XZInputStream;
import java.io.*;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
@@ -42,7 +42,6 @@ import java.util.jar.JarInputStream;
import java.util.jar.JarOutputStream;
import java.util.jar.Pack200;
import java.util.logging.Level;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.DigestUtils.digest;
import static org.jackhuang.hmcl.util.Hex.encodeHex;
@@ -130,21 +129,15 @@ public class LibraryDownloadTask extends Task<Void> {
}
try {
URL packXz = NetworkUtils.toURL(dependencyManager.getPrimaryDownloadProvider().injectURL(url) + ".pack.xz");
URL packXz = NetworkUtils.toURL(dependencyManager.getDownloadProvider().injectURL(url) + ".pack.xz");
if (NetworkUtils.urlExists(packXz)) {
List<URL> urls = dependencyManager.getPreferredDownloadProviders().stream()
.map(downloadProvider -> downloadProvider.injectURL(url) + ".pack.xz")
.map(NetworkUtils::toURL)
.collect(Collectors.toList());
List<URL> urls = dependencyManager.getDownloadProvider().injectURLWithCandidates(url + ".pack.xz");
task = new FileDownloadTask(urls, xzFile, null)
.setCacheRepository(cacheRepository)
.setCaching(true);
xz = true;
} else {
List<URL> urls = dependencyManager.getPreferredDownloadProviders().stream()
.map(downloadProvider -> downloadProvider.injectURL(url))
.map(NetworkUtils::toURL)
.collect(Collectors.toList());
List<URL> urls = dependencyManager.getDownloadProvider().injectURLWithCandidates(url);
task = new FileDownloadTask(urls, jar,
library.getDownload().getSha1() != null ? new IntegrityCheck("SHA-1", library.getDownload().getSha1()) : null)
.setCacheRepository(cacheRepository)
@@ -192,7 +185,7 @@ public class LibraryDownloadTask extends Task<Void> {
while (entry != null) {
byte[] eData = IOUtils.readFullyWithoutClosing(jar);
if (entry.getName().equals("checksums.sha1")) {
hashes = new String(eData, Charset.forName("UTF-8")).split("\n");
hashes = new String(eData, StandardCharsets.UTF_8).split("\n");
}
if (!entry.isDirectory()) {
files.put(entry.getName(), encodeHex(digest("SHA-1", eData)));

View File

@@ -22,14 +22,11 @@ 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.NetworkUtils;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
/**
*
@@ -46,9 +43,8 @@ public final class VersionJsonDownloadTask extends Task<String> {
this.gameVersion = gameVersion;
this.dependencyManager = dependencyManager;
this.gameVersionList = dependencyManager.getVersionList("game");
if (!gameVersionList.isLoaded())
dependents.add(gameVersionList.refreshAsync());
dependents.add(gameVersionList.loadAsync());
setSignificance(TaskSignificance.MODERATE);
}
@@ -67,10 +63,6 @@ public final class VersionJsonDownloadTask extends Task<String> {
public void execute() throws IOException {
RemoteVersion remoteVersion = gameVersionList.getVersion(gameVersion, gameVersion)
.orElseThrow(() -> new IOException("Cannot find specific version " + gameVersion + " in remote repository"));
dependencies.add(new GetTask(
dependencyManager.getPreferredDownloadProviders().stream()
.flatMap(downloadProvider -> Arrays.stream(remoteVersion.getUrl()).map(downloadProvider::injectURL))
.map(NetworkUtils::toURL).collect(Collectors.toList())
).storeTo(this::setResult));
dependencies.add(new GetTask(dependencyManager.getDownloadProvider().injectURLsWithCandidates(remoteVersion.getUrls())).storeTo(this::setResult));
}
}

View File

@@ -25,7 +25,6 @@ import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import org.jenkinsci.constant_pool_scanner.ConstantPool;
@@ -96,9 +95,7 @@ public final class OptiFineInstallTask extends Task<Version> {
if (installer == null) {
dependents.add(new FileDownloadTask(
dependencyManager.getPreferredDownloadProviders().stream()
.flatMap(downloadProvider -> Arrays.stream(remote.getUrl()).map(downloadProvider::injectURL))
.map(NetworkUtils::toURL).collect(Collectors.toList()),
dependencyManager.getDownloadProvider().injectURLsWithCandidates(remote.getUrls()),
dest.toFile(), null)
.setCacheRepository(dependencyManager.getCacheRepository())
.setCaching(true));

View File

@@ -119,6 +119,15 @@ public class FileDownloadTask extends Task<Void> {
this(Collections.singletonList(url), file, integrityCheck, retry);
}
/**
* Constructor.
* @param urls urls of remote file, will be attempted in order.
* @param file the location that download to.
*/
public FileDownloadTask(List<URL> urls, File file) {
this(urls, file, null);
}
/**
* Constructor.
* @param urls urls of remote file, will be attempted in order.

View File

@@ -17,7 +17,12 @@
*/
package org.jackhuang.hmcl.util.javafx;
import static org.jackhuang.hmcl.util.Pair.pair;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.Property;
import javafx.collections.ObservableList;
import javafx.scene.control.*;
import java.util.Objects;
import java.util.Optional;
@@ -25,16 +30,7 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Function;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.Property;
import javafx.collections.ObservableList;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.SelectionModel;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;
import static org.jackhuang.hmcl.util.Pair.pair;
/**
* @author yushijinhun
@@ -126,7 +122,7 @@ public final class ExtendedProperties {
// ==== CheckBox ====
@SuppressWarnings("unchecked")
public static ObjectProperty<Boolean> reservedSelectedPropertyFor(CheckBox checkbox) {
public static ObjectProperty<Boolean> reversedSelectedPropertyFor(CheckBox checkbox) {
return (ObjectProperty<Boolean>) checkbox.getProperties().computeIfAbsent(
PROP_PREFIX + ".checkbox.reservedSelected",
any -> new MappedProperty<Boolean, Boolean>(checkbox, "ext.reservedSelected",