summaryrefslogtreecommitdiffstats
path: root/java/com/google/gerrit/server/restapi/group/ListGroups.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/google/gerrit/server/restapi/group/ListGroups.java')
-rw-r--r--java/com/google/gerrit/server/restapi/group/ListGroups.java447
1 files changed, 447 insertions, 0 deletions
diff --git a/java/com/google/gerrit/server/restapi/group/ListGroups.java b/java/com/google/gerrit/server/restapi/group/ListGroups.java
new file mode 100644
index 0000000000..8d8dc3dafb
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/group/ListGroups.java
@@ -0,0 +1,447 @@
+// Copyright (C) 2013 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.restapi.group;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Streams;
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.client.ListGroupsOption;
+import com.google.gerrit.extensions.common.GroupInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountResource;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.GroupControl;
+import com.google.gerrit.server.group.GroupResolver;
+import com.google.gerrit.server.group.InternalGroupDescription;
+import com.google.gerrit.server.group.db.Groups;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.restapi.account.GetGroups;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.EnumSet;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.function.Predicate;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.kohsuke.args4j.Option;
+
+/** List groups visible to the calling user. */
+public class ListGroups implements RestReadView<TopLevelResource> {
+ private static final Comparator<GroupDescription.Internal> GROUP_COMPARATOR =
+ Comparator.comparing(GroupDescription.Basic::getName);
+
+ protected final GroupCache groupCache;
+
+ private final List<ProjectState> projects = new ArrayList<>();
+ private final Set<AccountGroup.UUID> groupsToInspect = new HashSet<>();
+ private final GroupControl.Factory groupControlFactory;
+ private final GroupControl.GenericFactory genericGroupControlFactory;
+ private final Provider<IdentifiedUser> identifiedUser;
+ private final IdentifiedUser.GenericFactory userFactory;
+ private final GetGroups accountGetGroups;
+ private final GroupJson json;
+ private final GroupBackend groupBackend;
+ private final Groups groups;
+ private final GroupResolver groupResolver;
+
+ private EnumSet<ListGroupsOption> options = EnumSet.noneOf(ListGroupsOption.class);
+ private boolean visibleToAll;
+ private Account.Id user;
+ private boolean owned;
+ private int limit;
+ private int start;
+ private String matchSubstring;
+ private String matchRegex;
+ private String suggest;
+ private String ownedBy;
+
+ @Option(
+ name = "--project",
+ aliases = {"-p"},
+ usage = "projects for which the groups should be listed")
+ public void addProject(ProjectState project) {
+ projects.add(project);
+ }
+
+ @Option(
+ name = "--visible-to-all",
+ usage = "to list only groups that are visible to all registered users")
+ public void setVisibleToAll(boolean visibleToAll) {
+ this.visibleToAll = visibleToAll;
+ }
+
+ @Option(
+ name = "--user",
+ aliases = {"-u"},
+ usage = "user for which the groups should be listed")
+ public void setUser(Account.Id user) {
+ this.user = user;
+ }
+
+ @Option(
+ name = "--owned",
+ usage =
+ "to list only groups that are owned by the"
+ + " specified user or by the calling user if no user was specifed")
+ public void setOwned(boolean owned) {
+ this.owned = owned;
+ }
+
+ /**
+ * Add a group to inspect.
+ *
+ * @param uuid UUID of the group
+ * @deprecated use {@link #addGroup(AccountGroup.UUID)}.
+ */
+ @Deprecated
+ @Option(
+ name = "--query",
+ aliases = {"-q"},
+ usage = "group to inspect (deprecated: use --group/-g instead)")
+ void addGroup_Deprecated(AccountGroup.UUID uuid) {
+ addGroup(uuid);
+ }
+
+ @Option(
+ name = "--group",
+ aliases = {"-g"},
+ usage = "group to inspect")
+ public void addGroup(AccountGroup.UUID uuid) {
+ groupsToInspect.add(uuid);
+ }
+
+ @Option(
+ name = "--limit",
+ aliases = {"-n"},
+ metaVar = "CNT",
+ usage = "maximum number of groups to list")
+ public void setLimit(int limit) {
+ this.limit = limit;
+ }
+
+ @Option(
+ name = "--start",
+ aliases = {"-S"},
+ metaVar = "CNT",
+ usage = "number of groups to skip")
+ public void setStart(int start) {
+ this.start = start;
+ }
+
+ @Option(
+ name = "--match",
+ aliases = {"-m"},
+ metaVar = "MATCH",
+ usage = "match group substring")
+ public void setMatchSubstring(String matchSubstring) {
+ this.matchSubstring = matchSubstring;
+ }
+
+ @Option(
+ name = "--regex",
+ aliases = {"-r"},
+ metaVar = "REGEX",
+ usage = "match group regex")
+ public void setMatchRegex(String matchRegex) {
+ this.matchRegex = matchRegex;
+ }
+
+ @Option(
+ name = "--suggest",
+ aliases = {"-s"},
+ usage = "to get a suggestion of groups")
+ public void setSuggest(String suggest) {
+ this.suggest = suggest;
+ }
+
+ @Option(name = "-o", usage = "Output options per group")
+ void addOption(ListGroupsOption o) {
+ options.add(o);
+ }
+
+ @Option(name = "-O", usage = "Output option flags, in hex")
+ void setOptionFlagsHex(String hex) {
+ options.addAll(ListGroupsOption.fromBits(Integer.parseInt(hex, 16)));
+ }
+
+ @Option(name = "--owned-by", usage = "list groups owned by the given group uuid")
+ public void setOwnedBy(String ownedBy) {
+ this.ownedBy = ownedBy;
+ }
+
+ @Inject
+ protected ListGroups(
+ final GroupCache groupCache,
+ final GroupControl.Factory groupControlFactory,
+ final GroupControl.GenericFactory genericGroupControlFactory,
+ final Provider<IdentifiedUser> identifiedUser,
+ final IdentifiedUser.GenericFactory userFactory,
+ final GetGroups accountGetGroups,
+ final GroupResolver groupResolver,
+ GroupJson json,
+ GroupBackend groupBackend,
+ Groups groups) {
+ this.groupCache = groupCache;
+ this.groupControlFactory = groupControlFactory;
+ this.genericGroupControlFactory = genericGroupControlFactory;
+ this.identifiedUser = identifiedUser;
+ this.userFactory = userFactory;
+ this.accountGetGroups = accountGetGroups;
+ this.json = json;
+ this.groupBackend = groupBackend;
+ this.groups = groups;
+ this.groupResolver = groupResolver;
+ }
+
+ public void setOptions(EnumSet<ListGroupsOption> options) {
+ this.options = options;
+ }
+
+ public Account.Id getUser() {
+ return user;
+ }
+
+ public List<ProjectState> getProjects() {
+ return projects;
+ }
+
+ @Override
+ public SortedMap<String, GroupInfo> apply(TopLevelResource resource)
+ throws OrmException, RestApiException, IOException, ConfigInvalidException,
+ PermissionBackendException {
+ SortedMap<String, GroupInfo> output = new TreeMap<>();
+ for (GroupInfo info : get()) {
+ output.put(MoreObjects.firstNonNull(info.name, "Group " + Url.decode(info.id)), info);
+ info.name = null;
+ }
+ return output;
+ }
+
+ public List<GroupInfo> get()
+ throws OrmException, RestApiException, IOException, ConfigInvalidException,
+ PermissionBackendException {
+ if (!Strings.isNullOrEmpty(suggest)) {
+ return suggestGroups();
+ }
+
+ if (!Strings.isNullOrEmpty(matchSubstring) && !Strings.isNullOrEmpty(matchRegex)) {
+ throw new BadRequestException("Specify one of m/r");
+ }
+
+ if (ownedBy != null) {
+ return getGroupsOwnedBy(ownedBy);
+ }
+
+ if (owned) {
+ return getGroupsOwnedBy(user != null ? userFactory.create(user) : identifiedUser.get());
+ }
+
+ if (user != null) {
+ return accountGetGroups.apply(new AccountResource(userFactory.create(user)));
+ }
+
+ return getAllGroups();
+ }
+
+ private List<GroupInfo> getAllGroups()
+ throws OrmException, IOException, ConfigInvalidException, PermissionBackendException {
+ Pattern pattern = getRegexPattern();
+ Stream<GroupDescription.Internal> existingGroups =
+ getAllExistingGroups()
+ .filter(group -> isRelevant(pattern, group))
+ .map(this::loadGroup)
+ .flatMap(Streams::stream)
+ .filter(this::isVisible)
+ .sorted(GROUP_COMPARATOR)
+ .skip(start);
+ if (limit > 0) {
+ existingGroups = existingGroups.limit(limit);
+ }
+ List<GroupDescription.Internal> relevantGroups = existingGroups.collect(toImmutableList());
+ List<GroupInfo> groupInfos = Lists.newArrayListWithCapacity(relevantGroups.size());
+ for (GroupDescription.Internal group : relevantGroups) {
+ groupInfos.add(json.addOptions(options).format(group));
+ }
+ return groupInfos;
+ }
+
+ private Stream<GroupReference> getAllExistingGroups() throws IOException, ConfigInvalidException {
+ if (!projects.isEmpty()) {
+ return projects.stream()
+ .map(ProjectState::getAllGroups)
+ .flatMap(Collection::stream)
+ .distinct();
+ }
+ return groups.getAllGroupReferences();
+ }
+
+ private List<GroupInfo> suggestGroups()
+ throws OrmException, BadRequestException, PermissionBackendException {
+ if (conflictingSuggestParameters()) {
+ throw new BadRequestException(
+ "You should only have no more than one --project and -n with --suggest");
+ }
+ List<GroupReference> groupRefs =
+ Lists.newArrayList(
+ Iterables.limit(
+ groupBackend.suggest(suggest, projects.stream().findFirst().orElse(null)),
+ limit <= 0 ? 10 : Math.min(limit, 10)));
+
+ List<GroupInfo> groupInfos = Lists.newArrayListWithCapacity(groupRefs.size());
+ for (GroupReference ref : groupRefs) {
+ GroupDescription.Basic desc = groupBackend.get(ref.getUUID());
+ if (desc != null) {
+ groupInfos.add(json.addOptions(options).format(desc));
+ }
+ }
+ 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 (ownedBy != null) {
+ return true;
+ }
+ if (start != 0) {
+ return true;
+ }
+ if (!groupsToInspect.isEmpty()) {
+ return true;
+ }
+ if (!Strings.isNullOrEmpty(matchSubstring)) {
+ return true;
+ }
+ if (!Strings.isNullOrEmpty(matchRegex)) {
+ return true;
+ }
+ return false;
+ }
+
+ private List<GroupInfo> filterGroupsOwnedBy(Predicate<GroupDescription.Internal> filter)
+ throws OrmException, IOException, ConfigInvalidException, PermissionBackendException {
+ Pattern pattern = getRegexPattern();
+ Stream<? extends GroupDescription.Internal> foundGroups =
+ groups
+ .getAllGroupReferences()
+ .filter(group -> isRelevant(pattern, group))
+ .map(this::loadGroup)
+ .flatMap(Streams::stream)
+ .filter(this::isVisible)
+ .filter(filter)
+ .sorted(GROUP_COMPARATOR)
+ .skip(start);
+ if (limit > 0) {
+ foundGroups = foundGroups.limit(limit);
+ }
+ List<GroupDescription.Internal> ownedGroups = foundGroups.collect(toImmutableList());
+ List<GroupInfo> groupInfos = new ArrayList<>(ownedGroups.size());
+ for (GroupDescription.Internal group : ownedGroups) {
+ groupInfos.add(json.addOptions(options).format(group));
+ }
+ return groupInfos;
+ }
+
+ private Optional<GroupDescription.Internal> loadGroup(GroupReference groupReference) {
+ return groupCache.get(groupReference.getUUID()).map(InternalGroupDescription::new);
+ }
+
+ private List<GroupInfo> getGroupsOwnedBy(String id)
+ throws OrmException, RestApiException, IOException, ConfigInvalidException,
+ PermissionBackendException {
+ String uuid = groupResolver.parse(id).getGroupUUID().get();
+ return filterGroupsOwnedBy(group -> group.getOwnerGroupUUID().get().equals(uuid));
+ }
+
+ private List<GroupInfo> getGroupsOwnedBy(IdentifiedUser user)
+ throws OrmException, IOException, ConfigInvalidException, PermissionBackendException {
+ return filterGroupsOwnedBy(group -> isOwner(user, group));
+ }
+
+ private boolean isOwner(CurrentUser user, GroupDescription.Internal group) {
+ try {
+ return genericGroupControlFactory.controlFor(user, group.getGroupUUID()).isOwner();
+ } catch (NoSuchGroupException e) {
+ return false;
+ }
+ }
+
+ private Pattern getRegexPattern() {
+ return Strings.isNullOrEmpty(matchRegex) ? null : Pattern.compile(matchRegex);
+ }
+
+ private boolean isRelevant(Pattern pattern, GroupReference group) {
+ if (!Strings.isNullOrEmpty(matchSubstring)) {
+ if (!group.getName().toLowerCase(Locale.US).contains(matchSubstring.toLowerCase(Locale.US))) {
+ return false;
+ }
+ } else if (pattern != null) {
+ if (!pattern.matcher(group.getName()).matches()) {
+ return false;
+ }
+ }
+ return groupsToInspect.isEmpty() || groupsToInspect.contains(group.getUUID());
+ }
+
+ private boolean isVisible(GroupDescription.Internal group) {
+ if (visibleToAll && !group.isVisibleToAll()) {
+ return false;
+ }
+ GroupControl c = groupControlFactory.controlFor(group);
+ return c.isVisible();
+ }
+}