Add REST endpoint to get info about server configuration

Some of the Gerrit configuration parameters can now be accessed by

  GET /config/server/info

This REST endpoint can also be used anonymously.

For now the REST endpoint returns only a very limited set of
configuration parameters, which are needed by the Gerrit Mylyn
Connector. The result contains information about:

- auth type, editable account fields, if contributor agreements are
  used
- contact store
- download schemes, commands and archive formats
- All-Projects and All-Users project names

At the moment the Mylyn Gerrit Connector retrieves this information by
parsing the config from the HostPageData which is sent to the client.
This is an internal data structure which is used to exchange
information between Gerrit server and Gerrit WebUI. It's not an API
and third-party tools should not rely on it. As a result the Mylyn
Gerrit Connector is currently broken for Gerrit 2.11 and newer.

Provide the Mylyn Gerrit Connector team a stable API that provides
them the information they need so that future breakages can be
avoided. See Eclipse Bugzilla issue 465132 [1] for further details.

The structure of the returned JSON correlates to the structure in the
gerrit.config file.

[1] https://bugs.eclipse.org/bugs/show_bug.cgi?id=465132

Change-Id: Iac4be762bff971403438aa84923d9f0e11883366
Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
This commit is contained in:
Edwin Kempin 2015-04-30 12:55:34 +02:00
parent ed2064f05e
commit b8590bd990
9 changed files with 429 additions and 45 deletions

View File

@ -30,6 +30,87 @@ Returns the version of the Gerrit server.
"2.7"
----
[[get-info]]
=== Get Server Info
--
'GET /config/server/info'
--
Returns the information about the Gerrit server configuration.
.Request
----
GET /config/server/info HTTP/1.0
----
As result a link:#server-info[ServerInfo] entity is returned.
.Response
----
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
)]}'
{
"auth": {
"auth_type": "LDAP",
"editable_account_fields": [
"FULL_NAME",
"REGISTER_NEW_EMAIL"
]
},
"download": {
"schemes": [
{
"name": "ssh",
"url": "ssh://jdoe@gerrithost:29418/${project}",
"is_auth_required": true,
"is_auth_supported": true,
"commands": {
"Checkout": "git fetch ssh://jdoe@gerrithost:29418/${project} ${ref} \u0026\u0026 git checkout FETCH_HEAD",
"Format Patch": "git fetch ssh://jdoe@gerrithost:29418/${project} ${ref} \u0026\u0026 git format-patch -1 --stdout FETCH_HEAD",
"Pull": "git pull ssh://jdoe@gerrithost:29418/${project} ${ref}",
"Cherry Pick": "git fetch ssh://jdoe@gerrithost:29418/${project} ${ref} \u0026\u0026 git cherry-pick FETCH_HEAD"
}
},
{
"name": "http",
"url": "http://jdoe@gerrithost:8080/${project}",
"is_auth_required": true,
"is_auth_supported": true,
"commands": {
"Checkout": "git fetch http://jdoe@gerrithost:8080/${project} ${ref} \u0026\u0026 git checkout FETCH_HEAD",
"Format Patch": "git fetch http://jdoe@gerrithost:8080/${project} ${ref} \u0026\u0026 git format-patch -1 --stdout FETCH_HEAD",
"Pull": "git pull http://jdoe@gerrithost:8080/${project} ${ref}",
"Cherry Pick": "git fetch http://jdoe@gerrithost:8080/${project} ${ref} \u0026\u0026 git cherry-pick FETCH_HEAD"
}
},
{
"name": "anonymous http",
"url": "http://gerrithost:8080/${project}",
"commands": {
"Checkout": "git fetch http://gerrithost:8080/${project} ${ref} \u0026\u0026 git checkout FETCH_HEAD",
"Format Patch": "git fetch http://gerrithost:8080/${project} ${ref} \u0026\u0026 git format-patch -1 --stdout FETCH_HEAD",
"Pull": "git pull http://gerrithost:8080/${project} ${ref}",
"Cherry Pick": "git fetch http://gerrithost:8080/${project} ${ref} \u0026\u0026 git cherry-pick FETCH_HEAD"
}
}
],
"archives": [
"TGZ",
"TAR",
"TBZ2",
"TXZ"
]
},
"gerrit": {
"all_projects": "All-Projects",
"all_users": "All-Users"
}
}
----
[[list-caches]]
=== List Caches
--
@ -822,6 +903,27 @@ The ID of the task (hex string).
[[json-entities]]
== JSON Entities
[[auth-info]]
=== AuthInfo
The `AuthInfo` entity contains information about the authentication
configuration of the Gerrit server.
[options="header",cols="1,^1,5"]
|==========================================
|Field Name ||Description
|`type` ||
The link:config-gerrit.html#auth.type[authentication type] that is
configured on the server. Can be `OPENID`, `OPENID_SSO`, `OAUTH`,
`HTTP`, `HTTP_LDAP`, `CLIENT_SSL_CERT_LDAP`, `LDAP`, `LDAP_BIND`,
`CUSTOM_EXTENSION` or `DEVELOPMENT_BECOME_ANY_ACCOUNT`.
|`use_contributor_agreements` |not set if `false`|
Whether link:config-gerrit.html#auth.contributorAgreements[contributor
agreements] are required.
|`editable_account_fields` ||
List of account fields that are editable. Possible values are
`FULL_NAME`, `USER_NAME` and `REGISTER_NEW_EMAIL`.
|==========================================
[[cache-info]]
=== CacheInfo
The `CacheInfo` entity contains information about a cache.
@ -878,6 +980,60 @@ The `CapabilityInfo` entity contains information about a capability.
|`name` |capability name
|=================================
[[contact-store-info]]
=== ContactStoreInfo
The `ContactStoreInfo` entity contains information about the contact
store.
[options="header",cols="1,6"]
|=======================
|Field Name |Description
|`url` |
The link:config-gerrit.html#contactstore.url[URL of the contact store].
|=======================
[[download-info]]
=== DownloadInfo
The `DownloadInfo` entity contains information about supported download
options.
[options="header",cols="1,6"]
|=======================
|Field Name |Description
|`schemes` |
The supported download schemes as list of link:#download-scheme-info[
DownloadSchemeInfo] entities.
|`archives` |
List of supported archive formats. Possible values are `TGZ`, `TAR`,
`TBZ2` and `TXZ`.
|=======================
[[download-scheme-info]]
=== DownloadSchemeInfo
The `DownloadSchemeInfo` entity contains information about a supported
download scheme and its commands.
[options="header",cols="1,^1,5"]
|=================================
|Field Name ||Description
|`name` ||
The name of the download scheme.
|`url` ||
The URL of the download scheme, where '${project}' is used as
placeholder for the project name.
|`is_auth_required` |not set if `false`|
Whether this download scheme requires authentication.
|`is_auth_supported` |not set if `false`|
Whether this download scheme supports authentication.
|`commands` ||
List of download commands, where '${project}' is used as
placeholder for the project name, and '${ref}' is used as
placeholder for the (change) ref.
Empty, if accessed anonymously and the download scheme requires
authentication.
|=================================
[[entries-info]]
=== EntriesInfo
The `EntriesInfo` entity contains information about the entries in a
@ -896,6 +1052,21 @@ with a unit abbreviation (`k`: kilobytes, `m`: megabytes,
`g`: gigabytes). Only set for disk caches.
|==================================
[[gerrit-info]]
=== GerritInfo
The `GerritInfo` entity contains information about Gerrit
configuration from the link:config-gerrit.html#gerrit[gerrit] section.
[options="header",cols="1,6"]
|================================
|Field Name |Description
|`all_projects_name` |
Name of the link:config-gerrit.html#gerrit.allProjects[root project].
|`all_users_name` |
Name of the link:config-gerrit.html#gerrit.allUsers[project in which
meta data of all users is stored].
|================================
[[hit-ration-info]]
=== HitRatioInfo
The `HitRatioInfo` entity contains information about the hit ratio of a
@ -958,6 +1129,30 @@ The maximal memory size. The value is returned with a unit abbreviation
The number of open files.
|============================
[[server-info]]
=== ServerInfo
The `ServerInfo` entity contains information about the configuration of
the Gerrit server.
[options="header",cols="1,^1,5"]
|=======================================
|Field Name ||Description
|`auth` ||
Information about the authentication configuration as
link:#auth-info[AuthInfo] entity.
|`contact_store` |optional|
Information about the contact store configuration as
link:#contact-store-info[ContactStoreInfo] entity.
|`download` ||
Information about the configured download options as
link:#download-info[DownloadInfo] entity.
information about Gerrit
|`gerrit` ||
Information about the configuration from the
link:config-gerrit.html#gerrit[gerrit] section as link:#gerrit-info[
GerritInfo] entity.
|=======================================
[[summary-info]]
=== SummaryInfo
The `SummaryInfo` entity contains information about the current state

View File

@ -20,7 +20,6 @@ import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.gerrit.common.data.GerritConfig;
import com.google.gerrit.common.data.GitwebConfig;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.account.Realm;
import com.google.gerrit.server.change.ArchiveFormat;
import com.google.gerrit.server.change.GetArchive;
@ -31,7 +30,6 @@ import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.DownloadConfig;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.contact.ContactStore;
import com.google.gerrit.server.mail.EmailSender;
import com.google.gerrit.server.ssh.SshInfo;
import com.google.inject.Inject;
import com.google.inject.Provider;
@ -40,8 +38,6 @@ import com.google.inject.ProvisionException;
import org.eclipse.jgit.lib.Config;
import java.net.MalformedURLException;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.servlet.ServletContext;
@ -56,7 +52,6 @@ class GerritConfigProvider implements Provider<GerritConfig> {
private final AllProjectsName wildProject;
private final SshInfo sshInfo;
private EmailSender emailSender;
private final ContactStore contactStore;
private final ServletContext servletContext;
private final String anonymousCowardName;
@ -81,11 +76,6 @@ class GerritConfigProvider implements Provider<GerritConfig> {
anonymousCowardName = acn;
}
@Inject(optional = true)
void setEmailSender(final EmailSender d) {
emailSender = d;
}
private GerritConfig create() throws MalformedURLException {
final GerritConfig config = new GerritConfig();
switch (authConfig.getAuthType()) {
@ -118,8 +108,7 @@ class GerritConfigProvider implements Provider<GerritConfig> {
break;
}
config.setSwitchAccountUrl(cfg.getString("auth", null, "switchAccountUrl"));
config.setUseContributorAgreements(cfg.getBoolean("auth",
"contributoragreements", false));
config.setUseContributorAgreements(authConfig.isUseContributorAgreements());
config.setGitDaemonUrl(cfg.getString("gerrit", null, "canonicalgiturl"));
config.setGitHttpUrl(cfg.getString("gerrit", null, "gitHttpUrl"));
config.setUseContactInfo(contactStore != null && contactStore.isEnabled());
@ -146,17 +135,7 @@ class GerritConfigProvider implements Provider<GerritConfig> {
config.setReportBugUrl(cfg.getString("gerrit", null, "reportBugUrl"));
config.setReportBugText(cfg.getString("gerrit", null, "reportBugText"));
Set<Account.FieldName> fields = new HashSet<>();
for (Account.FieldName n : Account.FieldName.values()) {
if (realm.allowsEdit(n)) {
if (n == Account.FieldName.REGISTER_NEW_EMAIL
&& (emailSender == null || !emailSender.isEnabled())) {
continue;
}
fields.add(n);
}
}
config.setEditableAccountFields(fields);
config.setEditableAccountFields(realm.getEditableFields());
if (gitWebConfig.getUrl() != null) {
config.setGitwebLink(new GitwebConfig(gitWebConfig.getUrl(), gitWebConfig

View File

@ -16,15 +16,44 @@ package com.google.gerrit.server.account;
import com.google.common.base.Strings;
import com.google.common.collect.Sets;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.client.Account.FieldName;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.mail.EmailSender;
import com.google.inject.Inject;
import java.util.Collection;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
/** Basic implementation of {@link Realm}. */
public abstract class AbstractRealm implements Realm {
private EmailSender emailSender;
@Inject(optional = true)
void setEmailSender(EmailSender emailSender) {
this.emailSender = emailSender;
}
@Override
public Set<FieldName> getEditableFields() {
Set<Account.FieldName> fields = new HashSet<>();
for (Account.FieldName n : Account.FieldName.values()) {
if (allowsEdit(n)) {
if (n == Account.FieldName.REGISTER_NEW_EMAIL) {
if (emailSender != null && emailSender.isEnabled()) {
fields.add(n);
}
} else {
fields.add(n);
}
}
}
return fields;
}
@Override
public boolean hasEmailAddress(IdentifiedUser user, String email) {
for (AccountExternalId ext : user.state().getExternalIds()) {

View File

@ -24,6 +24,9 @@ public interface Realm {
/** Can the end-user modify this field of their own account? */
public boolean allowsEdit(Account.FieldName field);
/** Returns the account fields that the end-user can modify. */
public Set<Account.FieldName> getEditableFields();
public AuthRequest authenticate(AuthRequest who) throws AccountException;
public AuthRequest link(ReviewDb db, Account.Id to, AuthRequest who)

View File

@ -19,15 +19,13 @@ import com.google.common.collect.ImmutableMap;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.DownloadConfig;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.eclipse.jgit.api.ArchiveCommand;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
@ -36,11 +34,7 @@ import org.kohsuke.args4j.Option;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
@ -51,28 +45,16 @@ public class GetArchive implements RestReadView<RevisionResource> {
final Set<ArchiveFormat> allowed;
@Inject
AllowedFormats(@GerritServerConfig Config cfg) {
Collection<ArchiveFormat> enabled;
String v = cfg.getString("download", null, "archive");
if (v == null) {
enabled = Arrays.asList(ArchiveFormat.values());
} else if (v.isEmpty() || "off".equalsIgnoreCase(v)) {
enabled = Collections.emptyList();
} else {
enabled = ConfigUtil.getEnumList(cfg,
"download", null, "archive",
ArchiveFormat.TGZ);
}
AllowedFormats(DownloadConfig cfg) {
Map<String, ArchiveFormat> exts = new HashMap<>();
for (ArchiveFormat format : enabled) {
for (ArchiveFormat format : cfg.getArchiveFormats()) {
for (String ext : format.getSuffixes()) {
exts.put(ext, format);
}
exts.put(format.name().toLowerCase(), format);
}
extensions = ImmutableMap.copyOf(exts);
allowed = Collections.unmodifiableSet(new LinkedHashSet<>(enabled));
allowed = cfg.getArchiveFormats();
}
public Set<ArchiveFormat> getAllowed() {

View File

@ -44,6 +44,7 @@ public class AuthConfig {
private final boolean enableRunAs;
private final boolean userNameToLowerCase;
private final boolean gitBasicAuth;
private final boolean useContributorAgreements;
private final String loginUrl;
private final String logoutUrl;
private final String openIdSsoUrl;
@ -75,6 +76,8 @@ public class AuthConfig {
trustContainerAuth = cfg.getBoolean("auth", "trustContainerAuth", false);
enableRunAs = cfg.getBoolean("auth", null, "enableRunAs", true);
gitBasicAuth = cfg.getBoolean("auth", "gitBasicAuth", false);
useContributorAgreements =
cfg.getBoolean("auth", "contributoragreements", false);
userNameToLowerCase = cfg.getBoolean("auth", "userNameToLowerCase", false);
@ -194,6 +197,11 @@ public class AuthConfig {
return gitBasicAuth;
}
/** Whether contributor agreements are used. */
public boolean isUseContributorAgreements() {
return useContributorAgreements;
}
public boolean isIdentityTrustable(final Collection<AccountExternalId> ids) {
switch (getAuthType()) {
case DEVELOPMENT_BECOME_ANY_ACCOUNT:

View File

@ -16,12 +16,14 @@ package com.google.gerrit.server.config;
import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
import com.google.gerrit.server.change.ArchiveFormat;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.eclipse.jgit.lib.Config;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@ -31,6 +33,7 @@ import java.util.Set;
public class DownloadConfig {
private final Set<DownloadScheme> downloadSchemes;
private final Set<DownloadCommand> downloadCommands;
private final Set<ArchiveFormat> archiveFormats;
@Inject
DownloadConfig(@GerritServerConfig final Config cfg) {
@ -45,6 +48,17 @@ public class DownloadConfig {
DownloadCommand.DEFAULT_DOWNLOADS);
downloadCommands =
Collections.unmodifiableSet(new HashSet<>(allCommands));
String v = cfg.getString("download", null, "archive");
if (v == null) {
archiveFormats = EnumSet.allOf(ArchiveFormat.class);
} else if (v.isEmpty() || "off".equalsIgnoreCase(v)) {
archiveFormats = Collections.emptySet();
} else {
archiveFormats = new HashSet<>(ConfigUtil.getEnumList(cfg,
"download", null, "archive",
ArchiveFormat.TGZ));
}
}
/** Scheme used to download. */
@ -56,4 +70,9 @@ public class DownloadConfig {
public Set<DownloadCommand> getDownloadCommands() {
return downloadCommands;
}
/** Archive formats for downloading. */
public Set<ArchiveFormat> getArchiveFormats() {
return archiveFormats;
}
}

View File

@ -0,0 +1,168 @@
// Copyright (C) 2015 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.config;
import com.google.gerrit.extensions.config.DownloadCommand;
import com.google.gerrit.extensions.config.DownloadScheme;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AuthType;
import com.google.gerrit.server.account.Realm;
import com.google.gerrit.server.change.ArchiveFormat;
import com.google.inject.Inject;
import org.eclipse.jgit.lib.Config;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class GetServerInfo implements RestReadView<ConfigResource> {
private final Config config;
private final AuthConfig authConfig;
private final Realm realm;
private final DownloadConfig downloadConfig;
private final DynamicMap<DownloadScheme> downloadSchemes;
private final DynamicMap<DownloadCommand> downloadCommands;
private final AllProjectsName allProjectsName;
private final AllUsersName allUsersName;
@Inject
public GetServerInfo(
@GerritServerConfig Config config,
AuthConfig authConfig,
Realm realm,
DownloadConfig downloadConfig,
DynamicMap<DownloadScheme> downloadSchemes,
DynamicMap<DownloadCommand> downloadCommands,
AllProjectsName allProjectsName,
AllUsersName allUsersName) {
this.config = config;
this.authConfig = authConfig;
this.realm = realm;
this.downloadConfig = downloadConfig;
this.downloadSchemes = downloadSchemes;
this.downloadCommands = downloadCommands;
this.allProjectsName = allProjectsName;
this.allUsersName = allUsersName;
}
@Override
public ServerInfo apply(ConfigResource rsrc) throws MalformedURLException {
ServerInfo info = new ServerInfo();
info.auth = new AuthInfo(authConfig, realm);
info.contactStore = getContactStoreInfo();
info.download =
new DownloadInfo(downloadConfig, downloadSchemes, downloadCommands);
info.gerrit = new GerritInfo(allProjectsName, allUsersName);
return info;
}
private ContactStoreInfo getContactStoreInfo() {
String url = config.getString("contactstore", null, "url");
if (url == null) {
return null;
}
ContactStoreInfo contactStore = new ContactStoreInfo();
contactStore.url = url;
return contactStore;
}
private static Boolean toBoolean(boolean v) {
return v ? v : null;
}
public static class ServerInfo {
public AuthInfo auth;
public ContactStoreInfo contactStore;
public DownloadInfo download;
public GerritInfo gerrit;
}
public static class AuthInfo {
public AuthType authType;
public Boolean useContributorAgreements;
public List<Account.FieldName> editableAccountFields;
public AuthInfo(AuthConfig cfg, Realm realm) {
authType = cfg.getAuthType();
useContributorAgreements = toBoolean(cfg.isUseContributorAgreements());
editableAccountFields = new ArrayList<>(realm.getEditableFields());
}
}
public static class ContactStoreInfo {
public String url;
}
public static class DownloadInfo {
public List<DownloadSchemeInfo> schemes;
public List<ArchiveFormat> archives;
public DownloadInfo(DownloadConfig downloadConfig,
DynamicMap<DownloadScheme> downloadSchemes,
DynamicMap<DownloadCommand> downloadCommands) {
schemes = new ArrayList<>();
for (DynamicMap.Entry<DownloadScheme> e : downloadSchemes) {
DownloadScheme scheme = e.getProvider().get();
if (scheme.isEnabled()) {
schemes.add(
new DownloadSchemeInfo(e.getExportName(), scheme, downloadCommands));
}
}
archives = new ArrayList<>(downloadConfig.getArchiveFormats());
}
}
public static class DownloadSchemeInfo {
public String name;
public String url;
public Boolean isAuthRequired;
public Boolean isAuthSupported;
public Map<String, String> commands;
public DownloadSchemeInfo(String schemeName, DownloadScheme scheme,
DynamicMap<DownloadCommand> downloadCommands) {
name = schemeName;
url = scheme.getUrl("${project}");
isAuthRequired = toBoolean(scheme.isAuthRequired());
isAuthSupported = toBoolean(scheme.isAuthSupported());
commands = new HashMap<>();
for (DynamicMap.Entry<DownloadCommand> e : downloadCommands) {
String commandName = e.getExportName();
DownloadCommand command = e.getProvider().get();
String c = command.getCommand(scheme, "${project}", "${ref}");
if (c != null) {
commands.put(commandName, c);
}
}
}
}
public static class GerritInfo {
public String allProjects;
public String allUsers;
public GerritInfo(AllProjectsName allProjectsName, AllUsersName allUsersName) {
allProjects = allProjectsName.get();
allUsers = allUsersName.get();
}
}
}

View File

@ -35,6 +35,7 @@ public class Module extends RestApiModule {
delete(TASK_KIND).to(DeleteTask.class);
child(CONFIG_KIND, "top-menus").to(TopMenuCollection.class);
get(CONFIG_KIND, "version").to(GetVersion.class);
get(CONFIG_KIND, "info").to(GetServerInfo.class);
get(CONFIG_KIND, "preferences").to(GetPreferences.class);
put(CONFIG_KIND, "preferences").to(SetPreferences.class);
}