Support defaults for general preferences in All-Users

The All-Users repository contains a refs/users/default branch for
configuring defaults that apply to all users. It already has a
preferences.config file for configuring default preferences that
overwrite the defaults that are hard-coded in Gerrit. However so far
only defaults for diff preferences and my menu entries are supported.
With this change defaults for all general preferences are supported.

When comparing preferences in the test we exclude the my menus because
GeneralPreferencesInfo.defaults() doesn't contain the my menu
defaults, but they are computed on the fly.

General preferences are stored in the accounts, this is why we need to
flush the account cache when the defaults for the general preferences
are changed.

Change-Id: Icdea2bf81ce1a49aba095735099b9bfcafad5a73
Signed-off-by: Edwin Kempin <ekempin@google.com>
This commit is contained in:
Edwin Kempin
2016-06-03 17:08:34 +02:00
parent 5a42f00e40
commit e9f5da3e63
12 changed files with 438 additions and 41 deletions

View File

@@ -936,6 +936,155 @@ link:#top-menu-entry-info[TopMenuEntryInfo] entities is returned.
]
----
[[get-user-preferences]]
=== Get Default User Preferences
--
'GET /config/server/preferences'
--
Returns the default user preferences for the server.
.Request
----
GET /a/config/server/preferences HTTP/1.0
----
As response a link:rest-api-accounts.html#preferences-info[
PreferencesInfo] is returned.
.Response
----
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
)]}'
{
"changes_per_page": 25,
"show_site_header": true,
"use_flash_clipboard": true,
"download_command": "CHECKOUT",
"date_format": "STD",
"time_format": "HHMM_12",
"diff_view": "SIDE_BY_SIDE",
"size_bar_in_change_table": true,
"review_category_strategy": "NONE",
"mute_common_path_prefixes": true,
"my": [
{
"url": "#/dashboard/self",
"name": "Changes"
},
{
"url": "#/q/owner:self+is:draft",
"name": "Drafts"
},
{
"url": "#/q/has:draft",
"name": "Draft Comments"
},
{
"url": "#/q/has:edit",
"name": "Edits"
},
{
"url": "#/q/is:watched+is:open",
"name": "Watched Changes"
},
{
"url": "#/q/is:starred",
"name": "Starred Changes"
},
{
"url": "#/groups/self",
"name": "Groups"
}
],
"email_strategy": "ENABLED"
}
----
[[set-user-preferences]]
=== Set Default User Preferences
--
'PUT /config/server/preferences'
--
Sets the default user preferences for the server.
The new user preferences must be provided in the request body as a
link:rest-api-accounts.html#preferences-input[PreferencesInput]
entity.
To be allowed to set default preferences, a user must be a member of
a group that is granted the
link:access-control.html#capability_administrateServer[
Administrate Server] capability.
.Request
----
PUT /a/config/server/preferences HTTP/1.0
Content-Type: application/json; charset=UTF-8
{
"changes_per_page": 50
}
----
As response a link:rest-api-accounts.html#preferences-info[
PreferencesInfo] is returned.
.Response
----
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
)]}'
{
"changes_per_page": 50,
"show_site_header": true,
"use_flash_clipboard": true,
"download_command": "CHECKOUT",
"date_format": "STD",
"time_format": "HHMM_12",
"diff_view": "SIDE_BY_SIDE",
"size_bar_in_change_table": true,
"review_category_strategy": "NONE",
"mute_common_path_prefixes": true,
"my": [
{
"url": "#/dashboard/self",
"name": "Changes"
},
{
"url": "#/q/owner:self+is:draft",
"name": "Drafts"
},
{
"url": "#/q/has:draft",
"name": "Draft Comments"
},
{
"url": "#/q/has:edit",
"name": "Edits"
},
{
"url": "#/q/is:watched+is:open",
"name": "Watched Changes"
},
{
"url": "#/q/is:starred",
"name": "Starred Changes"
},
{
"url": "#/groups/self",
"name": "Groups"
}
],
"email_strategy": "ENABLED"
}
----
[[get-diff-preferences]]
=== Get Default Diff Preferences

View File

@@ -28,7 +28,13 @@ import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo.ReviewCategoryStrategy;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo.TimeFormat;
import com.google.gerrit.extensions.client.MenuItem;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.config.AllUsersName;
import com.google.inject.Inject;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -37,6 +43,9 @@ import java.util.HashMap;
@NoHttpd
public class GeneralPreferencesIT extends AbstractDaemonTest {
@Inject
private AllUsersName allUsers;
private TestAccount user42;
@Before
@@ -45,6 +54,21 @@ public class GeneralPreferencesIT extends AbstractDaemonTest {
user42 = accounts.create(name, name + "@example.com", "User 42");
}
@After
public void cleanUp() throws Exception {
gApi.accounts().id(user42.getId().toString())
.setPreferences(GeneralPreferencesInfo.defaults());
try (Repository git = repoManager.openRepository(allUsers)) {
if (git.exactRef(RefNames.REFS_USERS_DEFAULT) != null) {
RefUpdate u = git.updateRef(RefNames.REFS_USERS_DEFAULT);
u.setForceUpdate(true);
assertThat(u.delete()).isEqualTo(RefUpdate.Result.FORCED);
}
}
accountCache.evictAll();
}
@Test
public void getAndSetPreferences() throws Exception {
GeneralPreferencesInfo o = gApi.accounts()
@@ -81,4 +105,23 @@ public class GeneralPreferencesIT extends AbstractDaemonTest {
assertPrefs(o, i, "my");
assertThat(o.my).hasSize(1);
}
@Test
public void getPreferencesWithConfiguredDefaults() throws Exception {
GeneralPreferencesInfo d = GeneralPreferencesInfo.defaults();
int newChangesPerPage = d.changesPerPage * 2;
GeneralPreferencesInfo update = new GeneralPreferencesInfo();
update.changesPerPage = newChangesPerPage;
gApi.config().server().setDefaultPreferences(update);
GeneralPreferencesInfo o = gApi.accounts()
.id(user42.getId().toString())
.getPreferences();
// assert configured defaults
assertThat(o.changesPerPage).isEqualTo(newChangesPerPage);
// assert hard-coded defaults
assertPrefs(o, d, "my", "changesPerPage");
}
}

View File

@@ -0,0 +1,70 @@
// Copyright (C) 2016 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.acceptance.api.config;
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.acceptance.AssertUtil.assertPrefs;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.config.AllUsersName;
import com.google.inject.Inject;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.junit.After;
import org.junit.Test;
@NoHttpd
public class GeneralPreferencesIT extends AbstractDaemonTest {
@Inject
private AllUsersName allUsers;
@After
public void cleanUp() throws Exception {
try (Repository git = repoManager.openRepository(allUsers)) {
if (git.exactRef(RefNames.REFS_USERS_DEFAULT) != null) {
RefUpdate u = git.updateRef(RefNames.REFS_USERS_DEFAULT);
u.setForceUpdate(true);
assertThat(u.delete()).isEqualTo(RefUpdate.Result.FORCED);
}
}
accountCache.evictAll();
}
@Test
public void getGeneralPreferences() throws Exception {
GeneralPreferencesInfo result =
gApi.config().server().getDefaultPreferences();
assertPrefs(result, GeneralPreferencesInfo.defaults(), "my");
}
@Test
public void setGeneralPreferences() throws Exception {
boolean newSignedOffBy = !GeneralPreferencesInfo.defaults().signedOffBy;
GeneralPreferencesInfo update = new GeneralPreferencesInfo();
update.signedOffBy = newSignedOffBy;
GeneralPreferencesInfo result =
gApi.config().server().setDefaultPreferences(update);
assertThat(result.signedOffBy).named("signedOffBy").isEqualTo(newSignedOffBy);
result = gApi.config().server().getDefaultPreferences();
GeneralPreferencesInfo expected = GeneralPreferencesInfo.defaults();
expected.signedOffBy = newSignedOffBy;
assertPrefs(result, expected, "my");
}
}

View File

@@ -15,6 +15,7 @@
package com.google.gerrit.extensions.api.config;
import com.google.gerrit.extensions.client.DiffPreferencesInfo;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
@@ -24,6 +25,9 @@ public interface Server {
*/
String getVersion() throws RestApiException;
GeneralPreferencesInfo getDefaultPreferences() throws RestApiException;
GeneralPreferencesInfo setDefaultPreferences(GeneralPreferencesInfo in)
throws RestApiException;
DiffPreferencesInfo getDefaultDiffPreferences() throws RestApiException;
DiffPreferencesInfo setDefaultDiffPreferences(DiffPreferencesInfo in)
throws RestApiException;
@@ -38,6 +42,18 @@ public interface Server {
throw new NotImplementedException();
}
@Override
public GeneralPreferencesInfo getDefaultPreferences()
throws RestApiException {
throw new NotImplementedException();
}
@Override
public GeneralPreferencesInfo setDefaultPreferences(
GeneralPreferencesInfo in) throws RestApiException {
throw new NotImplementedException();
}
@Override
public DiffPreferencesInfo getDefaultDiffPreferences()
throws RestApiException {

View File

@@ -29,4 +29,6 @@ public interface AccountCache {
void evict(Account.Id accountId) throws IOException;
void evictByUsername(String username);
void evictAll() throws IOException;
}

View File

@@ -129,6 +129,14 @@ public class AccountCacheImpl implements AccountCache {
}
}
@Override
public void evictAll() throws IOException {
byId.invalidateAll();
for (Account.Id accountId : byId.asMap().keySet()) {
indexer.get().index(accountId);
}
}
@Override
public void evictByUsername(String username) {
if (username != null) {

View File

@@ -15,6 +15,7 @@
package com.google.gerrit.server.account;
import static com.google.gerrit.server.config.ConfigUtil.loadSection;
import static com.google.gerrit.server.config.ConfigUtil.skipField;
import static com.google.gerrit.server.git.UserConfigSections.KEY_ID;
import static com.google.gerrit.server.git.UserConfigSections.KEY_MATCH;
import static com.google.gerrit.server.git.UserConfigSections.KEY_TARGET;
@@ -40,6 +41,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -75,31 +77,51 @@ public class GeneralPreferencesLoader {
GeneralPreferencesInfo in) throws IOException,
ConfigInvalidException, RepositoryNotFoundException {
try (Repository allUsers = gitMgr.openRepository(allUsersName)) {
VersionedAccountPreferences p =
VersionedAccountPreferences.forUser(id);
p.load(allUsers);
// Load all users default prefs
VersionedAccountPreferences dp = VersionedAccountPreferences.forDefault();
dp.load(allUsers);
GeneralPreferencesInfo allUserPrefs = new GeneralPreferencesInfo();
loadSection(dp.getConfig(), UserConfigSections.GENERAL, null, allUserPrefs,
GeneralPreferencesInfo.defaults(), in);
// Load user prefs
VersionedAccountPreferences p = VersionedAccountPreferences.forUser(id);
p.load(allUsers);
GeneralPreferencesInfo r =
loadSection(p.getConfig(), UserConfigSections.GENERAL, null,
new GeneralPreferencesInfo(),
GeneralPreferencesInfo.defaults(), in);
updateDefaults(allUserPrefs), in);
return loadFromAllUsers(r, p, allUsers);
return loadMyMenusAndUrlAliases(r, p, dp);
}
}
public GeneralPreferencesInfo loadFromAllUsers(
GeneralPreferencesInfo r, VersionedAccountPreferences v,
Repository allUsers) {
private GeneralPreferencesInfo updateDefaults(GeneralPreferencesInfo input) {
GeneralPreferencesInfo result = GeneralPreferencesInfo.defaults();
try {
for (Field field : input.getClass().getDeclaredFields()) {
if (skipField(field)) {
continue;
}
Object newVal = field.get(input);
if (newVal != null) {
field.set(result, newVal);
}
}
} catch (IllegalAccessException e) {
log.error(
"Cannot get default general preferences from " + allUsersName.get(),
e);
return GeneralPreferencesInfo.defaults();
}
return result;
}
public GeneralPreferencesInfo loadMyMenusAndUrlAliases(
GeneralPreferencesInfo r, VersionedAccountPreferences v, VersionedAccountPreferences d) {
r.my = my(v);
if (r.my.isEmpty() && !v.isDefaults()) {
try {
VersionedAccountPreferences d = VersionedAccountPreferences.forDefault();
d.load(allUsers);
r.my = my(d);
} catch (ConfigInvalidException | IOException e) {
log.warn("cannot read default preferences", e);
}
}
if (r.my.isEmpty()) {
r.my.add(new MenuItem("Changes", "#/dashboard/self", null));
@@ -113,6 +135,9 @@ public class GeneralPreferencesLoader {
}
r.urlAliases = urlAliases(v);
if (r.urlAliases == null && !v.isDefaults()) {
r.urlAliases = urlAliases(d);
}
return r;
}

View File

@@ -140,7 +140,7 @@ public class SetPreferences implements
}
}
private static void storeUrlAliases(VersionedAccountPreferences prefs,
public static void storeUrlAliases(VersionedAccountPreferences prefs,
Map<String, String> urlAliases) {
if (urlAliases != null) {
Config cfg = prefs.getConfig();

View File

@@ -17,10 +17,13 @@ package com.google.gerrit.server.api.config;
import com.google.gerrit.common.Version;
import com.google.gerrit.extensions.api.config.Server;
import com.google.gerrit.extensions.client.DiffPreferencesInfo;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.server.config.ConfigResource;
import com.google.gerrit.server.config.GetDiffPreferences;
import com.google.gerrit.server.config.GetPreferences;
import com.google.gerrit.server.config.SetDiffPreferences;
import com.google.gerrit.server.config.SetPreferences;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@@ -30,12 +33,18 @@ import java.io.IOException;
@Singleton
public class ServerImpl implements Server {
private final GetPreferences getPreferences;
private final SetPreferences setPreferences;
private final GetDiffPreferences getDiffPreferences;
private final SetDiffPreferences setDiffPreferences;
@Inject
ServerImpl(GetDiffPreferences getDiffPreferences,
ServerImpl(GetPreferences getPreferences,
SetPreferences setPreferences,
GetDiffPreferences getDiffPreferences,
SetDiffPreferences setDiffPreferences) {
this.getPreferences = getPreferences;
this.setPreferences = setPreferences;
this.getDiffPreferences = getDiffPreferences;
this.setDiffPreferences = setDiffPreferences;
}
@@ -45,6 +54,26 @@ public class ServerImpl implements Server {
return Version.getVersion();
}
@Override
public GeneralPreferencesInfo getDefaultPreferences()
throws RestApiException {
try {
return getPreferences.apply(new ConfigResource());
} catch (IOException | ConfigInvalidException e) {
throw new RestApiException("Cannot get default general preferences", e);
}
}
@Override
public GeneralPreferencesInfo setDefaultPreferences(
GeneralPreferencesInfo in) throws RestApiException {
try {
return setPreferences.apply(new ConfigResource(), in);
} catch (IOException | ConfigInvalidException e) {
throw new RestApiException("Cannot set default general preferences", e);
}
}
@Override
public DiffPreferencesInfo getDefaultDiffPreferences()
throws RestApiException {

View File

@@ -14,29 +14,32 @@
package com.google.gerrit.server.config;
import static com.google.gerrit.server.config.ConfigUtil.loadSection;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.server.account.GeneralPreferencesLoader;
import com.google.gerrit.server.account.VersionedAccountPreferences;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.UserConfigSections;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Repository;
import java.io.IOException;
@Singleton
public class GetPreferences implements RestReadView<ConfigResource> {
private GeneralPreferencesLoader loader;
private final GeneralPreferencesLoader loader;
private final GitRepositoryManager gitMgr;
private final AllUsersName allUsersName;
@Inject
public GetPreferences(GeneralPreferencesLoader loader,
GitRepositoryManager gitMgr,
AllUsersName allUsersName) {
GitRepositoryManager gitMgr, AllUsersName allUsersName) {
this.loader = loader;
this.gitMgr = gitMgr;
this.allUsersName = allUsersName;
@@ -45,14 +48,23 @@ public class GetPreferences implements RestReadView<ConfigResource> {
@Override
public GeneralPreferencesInfo apply(ConfigResource rsrc)
throws IOException, ConfigInvalidException {
return readFromGit(gitMgr, loader, allUsersName, null);
}
static GeneralPreferencesInfo readFromGit(GitRepositoryManager gitMgr,
GeneralPreferencesLoader loader, AllUsersName allUsersName,
GeneralPreferencesInfo in) throws IOException, ConfigInvalidException,
RepositoryNotFoundException {
try (Repository git = gitMgr.openRepository(allUsersName)) {
VersionedAccountPreferences p =
VersionedAccountPreferences.forDefault();
VersionedAccountPreferences p = VersionedAccountPreferences.forDefault();
p.load(git);
GeneralPreferencesInfo a = new GeneralPreferencesInfo();
GeneralPreferencesInfo r = loadSection(p.getConfig(),
UserConfigSections.GENERAL, null, new GeneralPreferencesInfo(),
GeneralPreferencesInfo.defaults(), in);
// TODO(davido): Maintain cache of default values in AllUsers repository
return loader.loadFromAllUsers(a, p, git);
return loader.loadMyMenusAndUrlAliases(r, p, null);
}
}
}

View File

@@ -14,67 +14,104 @@
package com.google.gerrit.server.config;
import static com.google.gerrit.server.config.ConfigUtil.loadSection;
import static com.google.gerrit.server.config.ConfigUtil.skipField;
import static com.google.gerrit.server.config.ConfigUtil.storeSection;
import static com.google.gerrit.server.config.GetPreferences.readFromGit;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.extensions.annotations.RequiresCapability;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.GeneralPreferencesLoader;
import com.google.gerrit.server.account.VersionedAccountPreferences;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.UserConfigSections;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.lang.reflect.Field;
@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
@Singleton
public class SetPreferences implements
RestModifyView<ConfigResource, GeneralPreferencesInfo> {
private static final Logger log =
LoggerFactory.getLogger(SetPreferences.class);
private final GeneralPreferencesLoader loader;
private final GitRepositoryManager gitManager;
private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
private final AllUsersName allUsersName;
private final AccountCache accountCache;
@Inject
SetPreferences(GeneralPreferencesLoader loader,
GitRepositoryManager gitManager,
Provider<MetaDataUpdate.User> metaDataUpdateFactory,
AllUsersName allUsersName) {
AllUsersName allUsersName,
AccountCache accountCache) {
this.loader = loader;
this.gitManager = gitManager;
this.metaDataUpdateFactory = metaDataUpdateFactory;
this.allUsersName = allUsersName;
this.accountCache = accountCache;
}
@Override
public GeneralPreferencesInfo apply(ConfigResource rsrc,
GeneralPreferencesInfo i)
throws BadRequestException, IOException, ConfigInvalidException {
if (i.changesPerPage != null || i.showSiteHeader != null
|| i.useFlashClipboard != null || i.downloadScheme != null
|| i.downloadCommand != null
|| i.dateFormat != null || i.timeFormat != null
|| i.relativeDateInChangeTable != null
|| i.sizeBarInChangeTable != null
|| i.legacycidInChangeTable != null
|| i.muteCommonPathPrefixes != null
|| i.reviewCategoryStrategy != null
|| i.signedOffBy != null
|| i.urlAliases != null
|| i.emailStrategy != null) {
if (!hasSetFields(i)) {
throw new BadRequestException("unsupported option");
}
return writeToGit(readFromGit(gitManager, loader, allUsersName, i));
}
VersionedAccountPreferences p;
private GeneralPreferencesInfo writeToGit(GeneralPreferencesInfo i)
throws RepositoryNotFoundException, IOException, ConfigInvalidException {
try (MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName)) {
p = VersionedAccountPreferences.forDefault();
VersionedAccountPreferences p = VersionedAccountPreferences.forDefault();
p.load(md);
storeSection(p.getConfig(), UserConfigSections.GENERAL, null, i,
GeneralPreferencesInfo.defaults());
com.google.gerrit.server.account.SetPreferences.storeMyMenus(p, i.my);
com.google.gerrit.server.account.SetPreferences.storeUrlAliases(p, i.urlAliases);
p.commit(md);
GeneralPreferencesInfo a = new GeneralPreferencesInfo();
return loader.loadFromAllUsers(a, p, md.getRepository());
accountCache.evictAll();
GeneralPreferencesInfo r = loadSection(p.getConfig(),
UserConfigSections.GENERAL, null, new GeneralPreferencesInfo(),
GeneralPreferencesInfo.defaults(), null);
return loader.loadMyMenusAndUrlAliases(r, p, null);
}
}
private static boolean hasSetFields(GeneralPreferencesInfo in) {
try {
for (Field field : in.getClass().getDeclaredFields()) {
if (skipField(field)) {
continue;
}
if (field.get(in) != null) {
return true;
}
}
} catch (IllegalAccessException e) {
log.warn("Unable to verify input", e);
}
return false;
}
}

View File

@@ -67,6 +67,12 @@ public class FakeAccountCache implements AccountCache {
byUsername.remove(username);
}
@Override
public synchronized void evictAll() {
byId.clear();
byUsername.clear();
}
public synchronized void put(Account account) {
AccountState state = newState(account);
byId.put(account.getId(), state);