3 * Copyright (C) 2009-2020 SonarSource SA
4 * mailto:info AT sonarsource DOT com
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 3 of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 package org.sonar.server.organization.ws;
22 import com.google.common.collect.Multiset;
23 import com.google.common.collect.Ordering;
24 import java.util.List;
25 import java.util.Optional;
26 import javax.annotation.Nullable;
27 import org.sonar.api.server.ws.Change;
28 import org.sonar.api.server.ws.Request;
29 import org.sonar.api.server.ws.Response;
30 import org.sonar.api.server.ws.WebService;
31 import org.sonar.api.server.ws.WebService.Param;
32 import org.sonar.api.server.ws.WebService.SelectionMode;
33 import org.sonar.core.util.stream.MoreCollectors;
34 import org.sonar.db.DbClient;
35 import org.sonar.db.DbSession;
36 import org.sonar.db.organization.OrganizationDto;
37 import org.sonar.db.user.UserDto;
38 import org.sonar.server.es.SearchOptions;
39 import org.sonar.server.es.SearchResult;
40 import org.sonar.server.issue.AvatarResolver;
41 import org.sonar.server.organization.DefaultOrganizationProvider;
42 import org.sonar.server.user.UserSession;
43 import org.sonar.server.user.index.UserDoc;
44 import org.sonar.server.user.index.UserIndex;
45 import org.sonar.server.user.index.UserQuery;
46 import org.sonarqube.ws.Common;
47 import org.sonarqube.ws.Organizations.SearchMembersWsResponse;
48 import org.sonarqube.ws.Organizations.User;
50 import static com.google.common.base.Preconditions.checkArgument;
51 import static com.google.common.base.Strings.emptyToNull;
52 import static java.util.Optional.ofNullable;
53 import static org.sonar.api.server.ws.WebService.SelectionMode.SELECTED;
54 import static org.sonar.db.permission.OrganizationPermission.ADMINISTER;
55 import static org.sonar.server.es.SearchOptions.MAX_LIMIT;
56 import static org.sonar.server.organization.ws.OrganizationsWsSupport.PARAM_ORGANIZATION;
57 import static org.sonar.server.exceptions.NotFoundException.checkFoundWithOptional;
58 import static org.sonar.server.ws.WsUtils.writeProtobuf;
60 public class SearchMembersAction implements OrganizationsWsAction {
62 private final DbClient dbClient;
63 private final UserIndex userIndex;
64 private final DefaultOrganizationProvider organizationProvider;
65 private final UserSession userSession;
66 private final AvatarResolver avatarResolver;
68 public SearchMembersAction(DbClient dbClient, UserIndex userIndex, DefaultOrganizationProvider organizationProvider, UserSession userSession, AvatarResolver avatarResolver) {
69 this.dbClient = dbClient;
70 this.userIndex = userIndex;
71 this.organizationProvider = organizationProvider;
72 this.userSession = userSession;
73 this.avatarResolver = avatarResolver;
77 public void define(WebService.NewController context) {
78 WebService.NewAction action = context.createAction("search_members")
79 .setDescription("Search members of an organization.<br/>" +
80 "Require organization membership.")
81 .setResponseExample(getClass().getResource("search_members-example.json"))
84 .setChangelog(new Change("7.3", "This action now requires organization membership"))
87 action.createSearchQuery("orwe", "names", "logins")
89 action.addPagingParams(50, MAX_LIMIT);
91 action.createParam(Param.SELECTED)
92 .setDescription("Depending on the value, show only selected items (selected=selected) or deselected items (selected=deselected).")
94 .setDefaultValue(SELECTED.value())
95 .setPossibleValues(SELECTED.value(), SelectionMode.DESELECTED.value());
97 action.createParam(PARAM_ORGANIZATION)
98 .setDescription("Organization key")
104 public void handle(Request request, Response response) throws Exception {
105 try (DbSession dbSession = dbClient.openSession(false)) {
106 OrganizationDto organization = getOrganization(dbSession, request.param("organization"));
107 userSession.checkMembership(organization);
109 UserQuery.Builder userQuery = buildUserQuery(request, organization);
110 SearchOptions searchOptions = buildSearchOptions(request);
112 SearchResult<UserDoc> searchResults = userIndex.search(userQuery.build(), searchOptions);
113 List<String> orderedLogins = searchResults.getDocs().stream().map(UserDoc::login).collect(MoreCollectors.toList());
115 List<UserDto> users = dbClient.userDao().selectByLogins(dbSession, orderedLogins).stream()
116 .sorted(Ordering.explicit(orderedLogins).onResultOf(UserDto::getLogin))
117 .collect(MoreCollectors.toList());
119 Multiset<String> groupCountByLogin = null;
120 if (userSession.hasPermission(ADMINISTER, organization)) {
121 groupCountByLogin = dbClient.groupMembershipDao().countGroupByLoginsAndOrganization(dbSession, orderedLogins, organization.getUuid());
124 Common.Paging wsPaging = buildWsPaging(request, searchResults);
125 SearchMembersWsResponse wsResponse = buildResponse(users, wsPaging, groupCountByLogin);
127 writeProtobuf(wsResponse, request, response);
131 private SearchMembersWsResponse buildResponse(List<UserDto> users, Common.Paging wsPaging, @Nullable Multiset<String> groupCountByLogin) {
132 SearchMembersWsResponse.Builder response = SearchMembersWsResponse.newBuilder();
134 User.Builder wsUser = User.newBuilder();
137 String login = userDto.getLogin();
141 ofNullable(emptyToNull(userDto.getEmail())).ifPresent(text -> wsUser.setAvatar(avatarResolver.create(userDto)));
142 ofNullable(userDto.getName()).ifPresent(wsUser::setName);
143 ofNullable(groupCountByLogin).ifPresent(count -> wsUser.setGroupCount(groupCountByLogin.count(login)));
146 .forEach(response::addUsers);
147 response.setPaging(wsPaging);
149 return response.build();
152 private static UserQuery.Builder buildUserQuery(Request request, OrganizationDto organization) {
153 UserQuery.Builder userQuery = UserQuery.builder();
154 String textQuery = request.param(Param.TEXT_QUERY);
155 checkArgument(textQuery == null || textQuery.length() >= 2, "Query length must be greater than or equal to 2");
156 userQuery.setTextQuery(textQuery);
158 SelectionMode selectionMode = SelectionMode.fromParam(request.mandatoryParam(Param.SELECTED));
159 if (SelectionMode.DESELECTED.equals(selectionMode)) {
160 userQuery.setExcludedOrganizationUuid(organization.getUuid());
162 userQuery.setOrganizationUuid(organization.getUuid());
167 private static SearchOptions buildSearchOptions(Request request) {
168 int pageSize = request.mandatoryParamAsInt(Param.PAGE_SIZE);
169 return new SearchOptions().setPage(request.mandatoryParamAsInt(Param.PAGE), pageSize);
172 private static Common.Paging buildWsPaging(Request request, SearchResult<UserDoc> searchResults) {
173 return Common.Paging.newBuilder()
174 .setPageIndex(request.mandatoryParamAsInt(Param.PAGE))
175 .setPageSize(request.mandatoryParamAsInt(Param.PAGE_SIZE))
176 .setTotal((int) searchResults.getTotal())
180 private OrganizationDto getOrganization(DbSession dbSession, @Nullable String organizationParam) {
181 String organizationKey = Optional.ofNullable(organizationParam)
182 .orElseGet(organizationProvider.get()::getKey);
183 return checkFoundWithOptional(
184 dbClient.organizationDao().selectByKey(dbSession, organizationKey),
185 "No organization with key '%s'", organizationKey);