// 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.group; 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.gerrit.common.data.GroupDescription; import com.google.gerrit.common.data.GroupDescriptions; 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.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.IdentifiedUser; import com.google.gerrit.server.account.AccountResource; 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.GroupComparator; import com.google.gerrit.server.account.GroupControl; import com.google.gerrit.server.project.ProjectControl; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import org.kohsuke.args4j.Option; /** List groups visible to the calling user. */ public class ListGroups implements RestReadView<TopLevelResource> { protected final GroupCache groupCache; private final List<ProjectControl> 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 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 suggest; @Option( name = "--project", aliases = {"-p"}, usage = "projects for which the groups should be listed" ) public void addProject(ProjectControl 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 = "--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))); } @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, GroupJson json, GroupBackend groupBackend) { this.groupCache = groupCache; this.groupControlFactory = groupControlFactory; this.genericGroupControlFactory = genericGroupControlFactory; this.identifiedUser = identifiedUser; this.userFactory = userFactory; this.accountGetGroups = accountGetGroups; this.json = json; this.groupBackend = groupBackend; } public void setOptions(EnumSet<ListGroupsOption> options) { this.options = options; } public Account.Id getUser() { return user; } public List<ProjectControl> getProjects() { return projects; } @Override public SortedMap<String, GroupInfo> apply(TopLevelResource resource) throws OrmException, BadRequestException { 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, BadRequestException { if (!Strings.isNullOrEmpty(suggest)) { return suggestGroups(); } 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 { List<GroupInfo> groupInfos; List<AccountGroup> groupList; if (!projects.isEmpty()) { Map<AccountGroup.UUID, AccountGroup> groups = new HashMap<>(); 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 { groupList = filterGroups(groupCache.all()); } groupInfos = Lists.newArrayListWithCapacity(groupList.size()); int found = 0; int foundIndex = 0; for (AccountGroup group : groupList) { if (foundIndex++ < start) { continue; } if (limit > 0 && ++found > limit) { break; } groupInfos.add(json.addOptions(options).format(GroupDescriptions.forAccountGroup(group))); } return groupInfos; } private List<GroupInfo> suggestGroups() throws OrmException, BadRequestException { 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, Iterables.getFirst(projects, null)), limit <= 0 ? 10 : Math.min(limit, 10))); List<GroupInfo> groupInfos = Lists.newArrayListWithCapacity(groupRefs.size()); for (final 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 (start != 0) { return true; } if (!groupsToInspect.isEmpty()) { return true; } if (!Strings.isNullOrEmpty(matchSubstring)) { return true; } return false; } private List<GroupInfo> getGroupsOwnedBy(IdentifiedUser user) throws OrmException { List<GroupInfo> groups = new ArrayList<>(); int found = 0; int foundIndex = 0; for (AccountGroup g : filterGroups(groupCache.all())) { GroupControl ctl = groupControlFactory.controlFor(g); try { if (genericGroupControlFactory.controlFor(user, g.getGroupUUID()).isOwner()) { if (foundIndex++ < start) { continue; } if (limit > 0 && ++found > limit) { break; } groups.add(json.addOptions(options).format(ctl.getGroup())); } } catch (NoSuchGroupException e) { continue; } } return groups; } private List<AccountGroup> filterGroups(Collection<AccountGroup> groups) { List<AccountGroup> filteredGroups = new ArrayList<>(groups.size()); for (AccountGroup group : groups) { if (!Strings.isNullOrEmpty(matchSubstring)) { if (!group .getName() .toLowerCase(Locale.US) .contains(matchSubstring.toLowerCase(Locale.US))) { continue; } } if (visibleToAll && !group.isVisibleToAll()) { continue; } if (!groupsToInspect.isEmpty() && !groupsToInspect.contains(group.getGroupUUID())) { continue; } GroupControl c = groupControlFactory.controlFor(group); if (c.isVisible()) { filteredGroups.add(group); } } Collections.sort(filteredGroups, new GroupComparator()); return filteredGroups; } }