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.Ordering;
23 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.GlobalPermission.ADMINISTER;
55 import static org.sonar.server.es.SearchOptions.MAX_PAGE_SIZE;
56 import static org.sonar.server.exceptions.NotFoundException.checkFoundWithOptional;
57 import static org.sonar.server.ws.WsUtils.writeProtobuf;
59 public class SearchMembersAction implements OrganizationsWsAction {
60 private static final String ORGANIZATION_PARAM = "organization";
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_PAGE_SIZE);
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(ORGANIZATION_PARAM)
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"));
108 UserQuery.Builder userQuery = buildUserQuery(request, organization);
109 SearchOptions searchOptions = buildSearchOptions(request);
111 SearchResult<UserDoc> searchResults = userIndex.search(userQuery.build(), searchOptions);
112 List<String> orderedLogins = searchResults.getDocs().stream().map(UserDoc::login).collect(MoreCollectors.toList());
114 List<UserDto> users = dbClient.userDao().selectByLogins(dbSession, orderedLogins).stream()
115 .sorted(Ordering.explicit(orderedLogins).onResultOf(UserDto::getLogin))
116 .collect(MoreCollectors.toList());
118 Map<String, Integer> groupCountByLogin = null;
119 if (userSession.hasPermission(ADMINISTER)) {
120 groupCountByLogin = dbClient.groupMembershipDao().countGroupsByUsers(dbSession, orderedLogins);
123 Common.Paging wsPaging = buildWsPaging(request, searchResults);
124 SearchMembersWsResponse wsResponse = buildResponse(users, wsPaging, groupCountByLogin);
126 writeProtobuf(wsResponse, request, response);
130 private SearchMembersWsResponse buildResponse(List<UserDto> users, Common.Paging wsPaging, @Nullable Map<String, Integer> groupCountByLogin) {
131 SearchMembersWsResponse.Builder response = SearchMembersWsResponse.newBuilder();
133 User.Builder wsUser = User.newBuilder();
136 String login = userDto.getLogin();
140 ofNullable(emptyToNull(userDto.getEmail())).ifPresent(text -> wsUser.setAvatar(avatarResolver.create(userDto)));
141 ofNullable(userDto.getName()).ifPresent(wsUser::setName);
142 ofNullable(groupCountByLogin).ifPresent(count -> wsUser.setGroupCount(groupCountByLogin.getOrDefault(login, 0)));
145 .forEach(response::addUsers);
146 response.setPaging(wsPaging);
148 return response.build();
151 private static UserQuery.Builder buildUserQuery(Request request, OrganizationDto organization) {
152 UserQuery.Builder userQuery = UserQuery.builder();
153 String textQuery = request.param(Param.TEXT_QUERY);
154 checkArgument(textQuery == null || textQuery.length() >= 2, "Query length must be greater than or equal to 2");
155 userQuery.setTextQuery(textQuery);
157 SelectionMode selectionMode = SelectionMode.fromParam(request.mandatoryParam(Param.SELECTED));
158 if (SelectionMode.DESELECTED.equals(selectionMode)) {
159 userQuery.setExcludedOrganizationUuid(organization.getUuid());
161 userQuery.setOrganizationUuid(organization.getUuid());
166 private static SearchOptions buildSearchOptions(Request request) {
167 int pageSize = request.mandatoryParamAsInt(Param.PAGE_SIZE);
168 return new SearchOptions().setPage(request.mandatoryParamAsInt(Param.PAGE), pageSize);
171 private static Common.Paging buildWsPaging(Request request, SearchResult<UserDoc> searchResults) {
172 return Common.Paging.newBuilder()
173 .setPageIndex(request.mandatoryParamAsInt(Param.PAGE))
174 .setPageSize(request.mandatoryParamAsInt(Param.PAGE_SIZE))
175 .setTotal((int) searchResults.getTotal())
179 private OrganizationDto getOrganization(DbSession dbSession, @Nullable String organizationParam) {
180 String organizationKey = Optional.ofNullable(organizationParam)
181 .orElseGet(organizationProvider.get()::getKey);
182 return checkFoundWithOptional(
183 dbClient.organizationDao().selectByKey(dbSession, organizationKey),
184 "No organization with key '%s'", organizationKey);