*/
package org.sonar.server.user.ws;
-import com.google.common.base.Function;
-import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import java.util.Collection;
import java.util.List;
import java.util.Map;
-import javax.annotation.Nonnull;
+import java.util.function.Function;
import javax.annotation.Nullable;
+import org.sonar.api.server.ws.Change;
import org.sonar.api.server.ws.Request;
import org.sonar.api.server.ws.Response;
import org.sonar.api.server.ws.WebService;
-import org.sonar.api.server.ws.WebService.Param;
-import org.sonar.api.utils.text.JsonWriter;
+import org.sonar.api.utils.Paging;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.user.UserDto;
import org.sonar.server.es.SearchOptions;
import org.sonar.server.es.SearchResult;
+import org.sonar.server.user.UserSession;
import org.sonar.server.user.index.UserDoc;
import org.sonar.server.user.index.UserIndex;
import org.sonar.server.user.index.UserQuery;
+import org.sonarqube.ws.WsUsers;
+import org.sonarqube.ws.WsUsers.SearchWsResponse;
+import org.sonarqube.ws.client.user.SearchRequest;
import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.common.base.Preconditions.checkArgument;
+import static org.sonar.api.server.ws.WebService.Param.FIELDS;
+import static org.sonar.api.server.ws.WebService.Param.PAGE;
+import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE;
+import static org.sonar.api.server.ws.WebService.Param.TEXT_QUERY;
+import static org.sonar.api.utils.Paging.forPageIndex;
+import static org.sonar.core.util.stream.MoreCollectors.toList;
import static org.sonar.server.es.SearchOptions.MAX_LIMIT;
+import static org.sonar.server.user.ws.UserJsonWriter.FIELD_ACTIVE;
+import static org.sonar.server.user.ws.UserJsonWriter.FIELD_EMAIL;
+import static org.sonar.server.user.ws.UserJsonWriter.FIELD_EXTERNAL_IDENTITY;
+import static org.sonar.server.user.ws.UserJsonWriter.FIELD_EXTERNAL_PROVIDER;
+import static org.sonar.server.user.ws.UserJsonWriter.FIELD_GROUPS;
+import static org.sonar.server.user.ws.UserJsonWriter.FIELD_LOCAL;
+import static org.sonar.server.user.ws.UserJsonWriter.FIELD_NAME;
+import static org.sonar.server.user.ws.UserJsonWriter.FIELD_SCM_ACCOUNTS;
+import static org.sonar.server.user.ws.UserJsonWriter.FIELD_TOKENS_COUNT;
+import static org.sonar.server.ws.WsUtils.writeProtobuf;
+import static org.sonarqube.ws.WsUsers.SearchWsResponse.Groups;
+import static org.sonarqube.ws.WsUsers.SearchWsResponse.ScmAccounts;
+import static org.sonarqube.ws.WsUsers.SearchWsResponse.User;
+import static org.sonarqube.ws.WsUsers.SearchWsResponse.newBuilder;
public class SearchAction implements UsersWsAction {
+ private static final int MAX_PAGE_SIZE = 500;
+
+ private final UserSession userSession;
private final UserIndex userIndex;
private final DbClient dbClient;
- private final UserJsonWriter userWriter;
- public SearchAction(UserIndex userIndex, DbClient dbClient, UserJsonWriter userWriter) {
+ public SearchAction(UserSession userSession, UserIndex userIndex, DbClient dbClient) {
+ this.userSession = userSession;
this.userIndex = userIndex;
this.dbClient = dbClient;
- this.userWriter = userWriter;
}
@Override
"Administer System permission is required to show the 'groups' field.<br/>" +
"When accessed anonymously, only logins and names are returned.")
.setSince("3.6")
+ .setChangelog(new Change("6.4", "Paging response fields moved to a Paging object"))
.setHandler(this)
.setResponseExample(getClass().getResource("search-example.json"));
.setDeprecatedSince("5.4");
action.addPagingParams(50, MAX_LIMIT);
- action.createParam(Param.TEXT_QUERY)
+ action.createParam(TEXT_QUERY)
.setDescription("Filter on login or name.");
}
@Override
public void handle(Request request, Response response) throws Exception {
- SearchOptions options = new SearchOptions()
- .setPage(request.mandatoryParamAsInt(Param.PAGE), request.mandatoryParamAsInt(Param.PAGE_SIZE));
- List<String> fields = request.paramAsStrings(Param.FIELDS);
- String textQuery = request.param(Param.TEXT_QUERY);
- SearchResult<UserDoc> result = userIndex.search(UserQuery.builder().setTextQuery(textQuery).build(), options);
+ WsUsers.SearchWsResponse wsResponse = doHandle(toSearchRequest(request));
+ writeProtobuf(wsResponse, request, response);
+ }
+ private WsUsers.SearchWsResponse doHandle(SearchRequest request) {
+ SearchOptions options = new SearchOptions().setPage(request.getPage(), request.getPageSize());
+ List<String> fields = request.getPossibleFields();
+ SearchResult<UserDoc> result = userIndex.search(UserQuery.builder().setTextQuery(request.getQuery()).build(), options);
try (DbSession dbSession = dbClient.openSession(false)) {
- List<String> logins = Lists.transform(result.getDocs(), UserDocToLogin.INSTANCE);
+ List<String> logins = result.getDocs().stream().map(UserDoc::login).collect(toList());
Multimap<String, String> groupsByLogin = dbClient.groupMembershipDao().selectGroupsByLogins(dbSession, logins);
Map<String, Integer> tokenCountsByLogin = dbClient.userTokenDao().countTokensByLogins(dbSession, logins);
- JsonWriter json = response.newJsonWriter().beginObject();
- options.writeJson(json, result.getTotal());
- List<UserDto> userDtos = dbClient.userDao().selectByOrderedLogins(dbSession, logins);
- writeUsers(json, userDtos, groupsByLogin, tokenCountsByLogin, fields);
- json.endObject().close();
+ List<UserDto> users = dbClient.userDao().selectByOrderedLogins(dbSession, logins);
+ Paging paging = forPageIndex(request.getPage()).withPageSize(request.getPageSize()).andTotal((int) result.getTotal());
+ return buildResponse(users, groupsByLogin, tokenCountsByLogin, fields, paging);
}
}
- private void writeUsers(JsonWriter json, List<UserDto> userDtos, Multimap<String, String> groupsByLogin, Map<String, Integer> tokenCountsByLogin,
- @Nullable List<String> fields) {
+ private SearchWsResponse buildResponse(List<UserDto> users, Multimap<String, String> groupsByLogin, Map<String, Integer> tokenCountsByLogin,
+ @Nullable List<String> fields, Paging paging) {
+ SearchWsResponse.Builder responseBuilder = newBuilder();
+ users.forEach(user -> responseBuilder.addUsers(towsUser(user, firstNonNull(tokenCountsByLogin.get(user.getLogin()), 0), groupsByLogin.get(user.getLogin()), fields)));
+ responseBuilder.getPagingBuilder()
+ .setPageIndex(paging.pageIndex())
+ .setPageSize(paging.pageSize())
+ .setTotal(paging.total())
+ .build();
+ return responseBuilder.build();
+ }
- json.name("users").beginArray();
- for (UserDto user : userDtos) {
- Collection<String> groups = groupsByLogin.get(user.getLogin());
- userWriter.write(json, user, firstNonNull(tokenCountsByLogin.get(user.getLogin()), 0), groups, fields);
+ private User towsUser(UserDto user, @Nullable Integer tokensCount, Collection<String> groups, @Nullable Collection<String> fields) {
+ User.Builder userBuilder = User.newBuilder()
+ .setLogin(user.getLogin());
+ setIfNeeded(FIELD_NAME, fields, user.getName(), userBuilder::setName);
+ if (userSession.isLoggedIn()) {
+ setIfNeeded(FIELD_EMAIL, fields, user.getEmail(), userBuilder::setEmail);
+ setIfNeeded(FIELD_ACTIVE, fields, user.isActive(), userBuilder::setActive);
+ setIfNeeded(FIELD_LOCAL, fields, user.isLocal(), userBuilder::setLocal);
+ setIfNeeded(FIELD_EXTERNAL_IDENTITY, fields, user.getExternalIdentity(), userBuilder::setExternalIdentity);
+ setIfNeeded(FIELD_EXTERNAL_PROVIDER, fields, user.getExternalIdentityProvider(), userBuilder::setExternalProvider);
+ setIfNeeded(FIELD_TOKENS_COUNT, fields, tokensCount, userBuilder::setTokensCount);
+ setIfNeeded(isNeeded(FIELD_SCM_ACCOUNTS, fields) && !user.getScmAccountsAsList().isEmpty(), user.getScmAccountsAsList(),
+ scm -> userBuilder.setScmAccounts(ScmAccounts.newBuilder().addAllScmAccounts(scm)));
}
- json.endArray();
+ if (userSession.isSystemAdministrator()) {
+ setIfNeeded(isNeeded(FIELD_GROUPS, fields) && !groups.isEmpty(), groups,
+ g -> userBuilder.setGroups(Groups.newBuilder().addAllGroups(g)));
+ }
+ return userBuilder.build();
}
- private enum UserDocToLogin implements Function<UserDoc, String> {
- INSTANCE;
+ private static <PARAM> void setIfNeeded(String field, @Nullable Collection<String> fields, @Nullable PARAM parameter, Function<PARAM, ?> setter) {
+ setIfNeeded(isNeeded(field, fields), parameter, setter);
+ }
- @Override
- public String apply(@Nonnull UserDoc input) {
- return input.login();
+ private static <PARAM> void setIfNeeded(boolean condition, @Nullable PARAM parameter, Function<PARAM, ?> setter) {
+ if (parameter != null && condition) {
+ setter.apply(parameter);
}
}
+
+ private static boolean isNeeded(String field, @Nullable Collection<String> fields) {
+ return fields == null || fields.isEmpty() || fields.contains(field);
+ }
+
+ private static SearchRequest toSearchRequest(Request request) {
+ int pageSize = request.mandatoryParamAsInt(PAGE_SIZE);
+ checkArgument(pageSize <= MAX_PAGE_SIZE, "The '%s' parameter must be less than %s", PAGE_SIZE, MAX_PAGE_SIZE);
+ return SearchRequest.builder()
+ .setQuery(request.param(TEXT_QUERY))
+ .setPage(request.mandatoryParamAsInt(PAGE))
+ .setPageSize(pageSize)
+ .setPossibleFields(request.paramAsStrings(FIELDS))
+ .build();
+ }
+
}
private DbSession dbSession = db.getSession();
private UserIndex index = new UserIndex(esTester.client());
private UserIndexer userIndexer = new UserIndexer(dbClient, esTester.client());
- private WsTester ws = new WsTester(new UsersWs(new SearchAction(index, dbClient, new UserJsonWriter(userSession))));
+ private WsTester ws = new WsTester(new UsersWs(new SearchAction(userSession, index, dbClient)));
@Test
public void search_json_example() throws Exception {
@Test
public void search_empty() throws Exception {
loginAsSimpleUser();
- ws.newGetRequest("api/users", "search").execute().assertJson(getClass(), "empty.json");
+ ws.newGetRequest("api/users", "search").execute().assertJson("{\n" +
+ " \"paging\": {\n" +
+ " \"pageIndex\": 1,\n" +
+ " \"pageSize\": 50,\n" +
+ " \"total\": 0\n" +
+ " },\n" +
+ " \"users\": []\n" +
+ "}");
}
@Test
@Test
public void search_with_groups() throws Exception {
loginAsSystemAdministrator();
- List<UserDto> users = injectUsers(1);
-
- GroupDto group1 = dbClient.groupDao().insert(dbSession, newGroupDto().setName("sonar-users"));
- GroupDto group2 = dbClient.groupDao().insert(dbSession, newGroupDto().setName("sonar-admins"));
- dbClient.userGroupDao().insert(dbSession, new UserGroupDto().setGroupId(group1.getId()).setUserId(users.get(0).getId()));
- dbClient.userGroupDao().insert(dbSession, new UserGroupDto().setGroupId(group2.getId()).setUserId(users.get(0).getId()));
- dbSession.commit();
+ injectUsers(1);
ws.newGetRequest("api/users", "search").execute().assertJson(getClass(), "user_with_groups.json");
}
private List<UserDto> injectUsers(int numberOfUsers) throws Exception {
List<UserDto> userDtos = Lists.newArrayList();
long createdAt = System.currentTimeMillis();
+ GroupDto group1 = db.users().insertGroup(newGroupDto().setName("sonar-users"));
+ GroupDto group2 = db.users().insertGroup(newGroupDto().setName("sonar-admins"));
for (int index = 0; index < numberOfUsers; index++) {
String email = String.format("user-%d@mail.com", index);
String login = String.format("user-%d", index);
.setLogin(login)
.setName(String.format("%s-%d", login, tokenIndex)));
}
+ db.users().insertMember(group1, userDto);
+ db.users().insertMember(group2, userDto);
}
dbSession.commit();
userIndexer.indexOnStartup(null);
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonarqube.ws.client.user;
+
+import java.util.ArrayList;
+import java.util.List;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+public class SearchRequest {
+
+ private final Integer page;
+ private final Integer pageSize;
+ private final String query;
+ private final List<String> possibleFields;
+
+ private SearchRequest(Builder builder) {
+ this.page = builder.page;
+ this.pageSize = builder.pageSize;
+ this.query = builder.query;
+ this.possibleFields = builder.additionalFields;
+ }
+
+ @CheckForNull
+ public Integer getPage() {
+ return page;
+ }
+
+ @CheckForNull
+ public Integer getPageSize() {
+ return pageSize;
+ }
+
+ @CheckForNull
+ public String getQuery() {
+ return query;
+ }
+
+ public List<String> getPossibleFields() {
+ return possibleFields;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+ private Integer page;
+ private Integer pageSize;
+ private String query;
+ private List<String> additionalFields = new ArrayList<>();
+
+ private Builder() {
+ // enforce factory method use
+ }
+
+ public Builder setPage(@Nullable Integer page) {
+ this.page = page;
+ return this;
+ }
+
+ public Builder setPageSize(@Nullable Integer pageSize) {
+ this.pageSize = pageSize;
+ return this;
+ }
+
+ public Builder setQuery(@Nullable String query) {
+ this.query = query;
+ return this;
+ }
+
+ public Builder setPossibleFields(List<String> possibleFields) {
+ this.additionalFields = possibleFields;
+ return this;
+ }
+
+ public SearchRequest build() {
+ return new SearchRequest(this);
+ }
+ }
+}