Adds group suggestions into ListGroups REST API.

This is a different approach of
Ie86de1f73ccd7bdec041730d95301d6aa3bdbdc4. This allows group
auto completion to be used in a plugin's UI.

Change-Id: Ia1cfa068246127c29f1b74f6aa4562a9167c301e
This commit is contained in:
Yuxuan 'fishy' Wang 2015-09-08 17:53:23 -07:00
parent b5032e1aef
commit 8c48525834
6 changed files with 173 additions and 44 deletions

View File

@ -172,6 +172,46 @@ Query 25 groups starting from index 50.
GET /groups/?n=25&S=50 HTTP/1.0 GET /groups/?n=25&S=50 HTTP/1.0
---- ----
[[suggest-group]]
==== Suggest Group
The `suggest` option indicates a user-entered string that
should be auto-completed to group names.
If this option is set and `n` is not set, then `n` defaults to 10.
When using this option,
the `project` or `p` option can be used to name the current project,
to allow context-dependent suggestions.
Not compatible with `visible-to-all`, `owned`, `user`, `match`, `q`,
or `S`.
(Attempts to use one of those options combined with `suggest` will
error out.)
.Request
----
GET /groups/?suggest=ad&p=All-Projects HTTP/1.0
----
.Response
----
HTTP/1.1 200 OK
Content-Disposition: attachment
Content-Type: application/json; charset=UTF-8
)]}'
{
"Administrators": {
"url": "#/admin/groups/uuid-59b92f35489e62c80d1ab1bf0c2d17843038df8b",
"options": {},
"description": "Gerrit Site Administrators",
"group_id": 1,
"owner": "Administrators",
"owner_id": "59b92f35489e62c80d1ab1bf0c2d17843038df8b",
"id": "59b92f35489e62c80d1ab1bf0c2d17843038df8b"
}
}
----
[[get-group]] [[get-group]]
=== Get Group === Get Group
-- --

View File

@ -401,6 +401,13 @@ public class GroupsIT extends AbstractDaemonTest {
assertThat(gApi.groups().list().getAsMap()).containsKey(newGroupName); assertThat(gApi.groups().list().getAsMap()).containsKey(newGroupName);
} }
@Test
public void testSuggestGroup() throws Exception {
Map<String, GroupInfo> groups = gApi.groups().list().withSuggest("adm").getAsMap();
assertThat(groups).containsKey("Administrators");
assertThat(groups).hasSize(1);
}
@Test @Test
public void testAllGroupInfoFieldsSetCorrectly() throws Exception { public void testAllGroupInfoFieldsSetCorrectly() throws Exception {
AccountGroup adminGroup = getFromCache("Administrators"); AccountGroup adminGroup = getFromCache("Administrators");

View File

@ -63,6 +63,7 @@ public interface Groups {
private int limit; private int limit;
private int start; private int start;
private String substring; private String substring;
private String suggest;
public List<GroupInfo> get() throws RestApiException { public List<GroupInfo> get() throws RestApiException {
Map<String, GroupInfo> map = getAsMap(); Map<String, GroupInfo> map = getAsMap();
@ -128,6 +129,11 @@ public interface Groups {
return this; return this;
} }
public ListRequest withSuggest(String suggest) {
this.suggest = suggest;
return this;
}
public EnumSet<ListGroupsOption> getOptions() { public EnumSet<ListGroupsOption> getOptions() {
return options; return options;
} }
@ -163,5 +169,9 @@ public interface Groups {
public String getSubstring() { public String getSubstring() {
return substring; return substring;
} }
public String getSuggest() {
return suggest;
}
} }
} }

View File

@ -138,6 +138,7 @@ class GroupsImpl implements Groups {
list.setLimit(req.getLimit()); list.setLimit(req.getLimit());
list.setStart(req.getStart()); list.setStart(req.getStart());
list.setMatchSubstring(req.getSubstring()); list.setMatchSubstring(req.getSubstring());
list.setSuggest(req.getSuggest());
try { try {
return list.apply(tlr); return list.apply(tlr);
} catch (OrmException e) { } catch (OrmException e) {

View File

@ -16,14 +16,18 @@ package com.google.gerrit.server.group;
import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.GroupDescription;
import com.google.gerrit.common.data.GroupDescriptions; import com.google.gerrit.common.data.GroupDescriptions;
import com.google.gerrit.common.data.GroupReference; import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.errors.NoSuchGroupException; import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.extensions.client.ListGroupsOption; import com.google.gerrit.extensions.client.ListGroupsOption;
import com.google.gerrit.extensions.common.GroupInfo; import com.google.gerrit.extensions.common.GroupInfo;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.RestReadView; import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.extensions.restapi.TopLevelResource; import com.google.gerrit.extensions.restapi.TopLevelResource;
import com.google.gerrit.extensions.restapi.Url; import com.google.gerrit.extensions.restapi.Url;
@ -32,6 +36,7 @@ import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountResource; import com.google.gerrit.server.account.AccountResource;
import com.google.gerrit.server.account.GetGroups; import com.google.gerrit.server.account.GetGroups;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupCache; import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.account.GroupComparator; import com.google.gerrit.server.account.GroupComparator;
import com.google.gerrit.server.account.GroupControl; import com.google.gerrit.server.account.GroupControl;
@ -64,6 +69,7 @@ public class ListGroups implements RestReadView<TopLevelResource> {
private final IdentifiedUser.GenericFactory userFactory; private final IdentifiedUser.GenericFactory userFactory;
private final Provider<GetGroups> accountGetGroups; private final Provider<GetGroups> accountGetGroups;
private final GroupJson json; private final GroupJson json;
private final GroupBackend groupBackend;
private EnumSet<ListGroupsOption> options = private EnumSet<ListGroupsOption> options =
EnumSet.noneOf(ListGroupsOption.class); EnumSet.noneOf(ListGroupsOption.class);
@ -73,6 +79,7 @@ public class ListGroups implements RestReadView<TopLevelResource> {
private int limit; private int limit;
private int start; private int start;
private String matchSubstring; private String matchSubstring;
private String suggest;
@Option(name = "--project", aliases = {"-p"}, @Option(name = "--project", aliases = {"-p"},
usage = "projects for which the groups should be listed") usage = "projects for which the groups should be listed")
@ -121,6 +128,11 @@ public class ListGroups implements RestReadView<TopLevelResource> {
this.matchSubstring = matchSubstring; this.matchSubstring = matchSubstring;
} }
@Option(name = "--suggest", usage = "to get a suggestion of groups")
public void setSuggest(String suggest) {
this.suggest = suggest;
}
@Option(name = "-o", usage = "Output options per group") @Option(name = "-o", usage = "Output options per group")
void addOption(ListGroupsOption o) { void addOption(ListGroupsOption o) {
options.add(o); options.add(o);
@ -137,7 +149,8 @@ public class ListGroups implements RestReadView<TopLevelResource> {
final GroupControl.GenericFactory genericGroupControlFactory, final GroupControl.GenericFactory genericGroupControlFactory,
final Provider<IdentifiedUser> identifiedUser, final Provider<IdentifiedUser> identifiedUser,
final IdentifiedUser.GenericFactory userFactory, final IdentifiedUser.GenericFactory userFactory,
final Provider<GetGroups> accountGetGroups, GroupJson json) { final Provider<GetGroups> accountGetGroups, GroupJson json,
GroupBackend groupBackend) {
this.groupCache = groupCache; this.groupCache = groupCache;
this.groupControlFactory = groupControlFactory; this.groupControlFactory = groupControlFactory;
this.genericGroupControlFactory = genericGroupControlFactory; this.genericGroupControlFactory = genericGroupControlFactory;
@ -145,6 +158,7 @@ public class ListGroups implements RestReadView<TopLevelResource> {
this.userFactory = userFactory; this.userFactory = userFactory;
this.accountGetGroups = accountGetGroups; this.accountGetGroups = accountGetGroups;
this.json = json; this.json = json;
this.groupBackend = groupBackend;
} }
public void setOptions(EnumSet<ListGroupsOption> options) { public void setOptions(EnumSet<ListGroupsOption> options) {
@ -161,7 +175,7 @@ public class ListGroups implements RestReadView<TopLevelResource> {
@Override @Override
public SortedMap<String, GroupInfo> apply(TopLevelResource resource) public SortedMap<String, GroupInfo> apply(TopLevelResource resource)
throws OrmException { throws OrmException, BadRequestException {
SortedMap<String, GroupInfo> output = Maps.newTreeMap(); SortedMap<String, GroupInfo> output = Maps.newTreeMap();
for (GroupInfo info : get()) { for (GroupInfo info : get()) {
output.put(MoreObjects.firstNonNull( output.put(MoreObjects.firstNonNull(
@ -172,53 +186,107 @@ public class ListGroups implements RestReadView<TopLevelResource> {
return output; return output;
} }
public List<GroupInfo> get() throws OrmException { public List<GroupInfo> get() throws OrmException, BadRequestException {
List<GroupInfo> groupInfos; if (!Strings.isNullOrEmpty(suggest)) {
return suggestGroups();
}
if (owned) {
return getGroupsOwnedBy(
user != null ? userFactory.create(user) : identifiedUser.get());
}
if (user != null) { if (user != null) {
if (owned) { return accountGetGroups.get().apply(
groupInfos = getGroupsOwnedBy(userFactory.create(user)); new AccountResource(userFactory.create(user)));
} else { }
groupInfos = accountGetGroups.get().apply(
new AccountResource(userFactory.create(user))); return getAllGroups();
}
private List<GroupInfo> getAllGroups() throws OrmException {
List<GroupInfo> groupInfos;
List<AccountGroup> groupList;
if (!projects.isEmpty()) {
Map<AccountGroup.UUID, AccountGroup> groups = Maps.newHashMap();
for (final ProjectControl projectControl : projects) {
final Set<GroupReference> groupsRefs = projectControl.getAllGroups();
for (final GroupReference groupRef : groupsRefs) {
final AccountGroup group = groupCache.get(groupRef.getUUID());
if (group != null) {
groups.put(group.getGroupUUID(), group);
}
}
} }
groupList = filterGroups(groups.values());
} else { } else {
if (owned) { groupList = filterGroups(groupCache.all());
groupInfos = getGroupsOwnedBy(identifiedUser.get()); }
} else { groupInfos = Lists.newArrayListWithCapacity(groupList.size());
List<AccountGroup> groupList; int found = 0;
if (!projects.isEmpty()) { int foundIndex = 0;
Map<AccountGroup.UUID, AccountGroup> groups = Maps.newHashMap(); for (AccountGroup group : groupList) {
for (final ProjectControl projectControl : projects) { if (foundIndex++ < start) {
final Set<GroupReference> groupsRefs = projectControl.getAllGroups(); continue;
for (final GroupReference groupRef : groupsRefs) { }
final AccountGroup group = groupCache.get(groupRef.getUUID()); if (limit > 0 && ++found > limit) {
if (group != null) { break;
groups.put(group.getGroupUUID(), group); }
} groupInfos.add(json.addOptions(options).format(
} GroupDescriptions.forAccountGroup(group)));
} }
groupList = filterGroups(groups.values()); return groupInfos;
} else { }
groupList = filterGroups(groupCache.all());
} private List<GroupInfo> suggestGroups() throws OrmException, BadRequestException {
groupInfos = Lists.newArrayListWithCapacity(groupList.size()); if (conflictingSuggestParameters()) {
int found = 0; throw new BadRequestException(
int foundIndex = 0; "You should only have no more than one --project and -n with --suggest");
for (AccountGroup group : groupList) { }
if (foundIndex++ < start) {
continue; List<GroupReference> groupRefs = Lists.newArrayList(Iterables.limit(
} groupBackend.suggest(
if (limit > 0 && ++found > limit) { suggest, Iterables.getFirst(projects, null)),
break; limit <= 0 ? 10 : Math.min(limit, 10)));
}
groupInfos.add(json.addOptions(options).format( List<GroupInfo> groupInfos = Lists.newArrayListWithCapacity(groupRefs.size());
GroupDescriptions.forAccountGroup(group))); for (final GroupReference ref : groupRefs) {
} GroupDescription.Basic desc = groupBackend.get(ref.getUUID());
if (desc != null) {
groupInfos.add(json.addOptions(options).format(desc));
} }
} }
return groupInfos; return groupInfos;
} }
private boolean conflictingSuggestParameters() {
if (Strings.isNullOrEmpty(suggest)) {
return false;
}
if (projects.size() > 1) {
return true;
}
if (visibleToAll) {
return true;
}
if (user != null) {
return true;
}
if (owned) {
return true;
}
if (start != 0) {
return true;
}
if (!groupsToInspect.isEmpty()) {
return true;
}
if (!Strings.isNullOrEmpty(matchSubstring)) {
return true;
}
return false;
}
private List<GroupInfo> getGroupsOwnedBy(IdentifiedUser user) private List<GroupInfo> getGroupsOwnedBy(IdentifiedUser user)
throws OrmException { throws OrmException {
List<GroupInfo> groups = Lists.newArrayList(); List<GroupInfo> groups = Lists.newArrayList();

View File

@ -19,10 +19,12 @@ import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.gerrit.extensions.common.GroupInfo; import com.google.gerrit.extensions.common.GroupInfo;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.Url; import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.GetGroups; import com.google.gerrit.server.account.GetGroups;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupCache; import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.account.GroupControl; import com.google.gerrit.server.account.GroupControl;
import com.google.gerrit.server.group.GroupJson; import com.google.gerrit.server.group.GroupJson;
@ -71,12 +73,13 @@ public class ListGroupsCommand extends SshCommand {
final Provider<IdentifiedUser> identifiedUser, final Provider<IdentifiedUser> identifiedUser,
final IdentifiedUser.GenericFactory userFactory, final IdentifiedUser.GenericFactory userFactory,
final Provider<GetGroups> accountGetGroups, final Provider<GetGroups> accountGetGroups,
final GroupJson json) { final GroupJson json,
GroupBackend groupBackend) {
super(groupCache, groupControlFactory, genericGroupControlFactory, super(groupCache, groupControlFactory, genericGroupControlFactory,
identifiedUser, userFactory, accountGetGroups, json); identifiedUser, userFactory, accountGetGroups, json, groupBackend);
} }
void display(final PrintWriter out) throws OrmException { void display(final PrintWriter out) throws OrmException, BadRequestException {
final ColumnFormatter formatter = new ColumnFormatter(out, '\t'); final ColumnFormatter formatter = new ColumnFormatter(out, '\t');
for (final GroupInfo info : get()) { for (final GroupInfo info : get()) {
formatter.addColumn(MoreObjects.firstNonNull(info.name, "n/a")); formatter.addColumn(MoreObjects.firstNonNull(info.name, "n/a"));