diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/ws/SearchAction.java b/server/sonar-server/src/main/java/org/sonar/server/user/ws/SearchAction.java
index 4fd0d0da7615..7e42599308b6 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/user/ws/SearchAction.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/user/ws/SearchAction.java
@@ -19,41 +19,66 @@
*/
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
@@ -63,6 +88,7 @@ public void define(WebService.NewController controller) {
"Administer System permission is required to show the 'groups' field.
" +
"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"));
@@ -70,47 +96,86 @@ public void define(WebService.NewController controller) {
.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 fields = request.paramAsStrings(Param.FIELDS);
- String textQuery = request.param(Param.TEXT_QUERY);
- SearchResult 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 fields = request.getPossibleFields();
+ SearchResult result = userIndex.search(UserQuery.builder().setTextQuery(request.getQuery()).build(), options);
try (DbSession dbSession = dbClient.openSession(false)) {
- List logins = Lists.transform(result.getDocs(), UserDocToLogin.INSTANCE);
+ List logins = result.getDocs().stream().map(UserDoc::login).collect(toList());
Multimap groupsByLogin = dbClient.groupMembershipDao().selectGroupsByLogins(dbSession, logins);
Map tokenCountsByLogin = dbClient.userTokenDao().countTokensByLogins(dbSession, logins);
- JsonWriter json = response.newJsonWriter().beginObject();
- options.writeJson(json, result.getTotal());
- List userDtos = dbClient.userDao().selectByOrderedLogins(dbSession, logins);
- writeUsers(json, userDtos, groupsByLogin, tokenCountsByLogin, fields);
- json.endObject().close();
+ List 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 userDtos, Multimap groupsByLogin, Map tokenCountsByLogin,
- @Nullable List fields) {
+ private SearchWsResponse buildResponse(List users, Multimap groupsByLogin, Map tokenCountsByLogin,
+ @Nullable List 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 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 groups, @Nullable Collection 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 {
- INSTANCE;
+ private static void setIfNeeded(String field, @Nullable Collection fields, @Nullable PARAM parameter, Function setter) {
+ setIfNeeded(isNeeded(field, fields), parameter, setter);
+ }
- @Override
- public String apply(@Nonnull UserDoc input) {
- return input.login();
+ private static void setIfNeeded(boolean condition, @Nullable PARAM parameter, Function setter) {
+ if (parameter != null && condition) {
+ setter.apply(parameter);
}
}
+
+ private static boolean isNeeded(String field, @Nullable Collection 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();
+ }
+
}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/ws/UserJsonWriter.java b/server/sonar-server/src/main/java/org/sonar/server/user/ws/UserJsonWriter.java
index a26136bdc28c..f2a6aa933bf6 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/user/ws/UserJsonWriter.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/user/ws/UserJsonWriter.java
@@ -58,13 +58,6 @@ public UserJsonWriter(UserSession userSession) {
* Serializes a user to the passed JsonWriter.
*/
public void write(JsonWriter json, UserDto user, Collection groups, @Nullable Collection fields) {
- write(json, user, null, groups, fields);
- }
-
- /**
- * Serializes a user to the passed JsonWriter.
- */
- public void write(JsonWriter json, UserDto user, @Nullable Integer tokensCount, Collection groups, @Nullable Collection fields) {
json.beginObject();
json.prop(FIELD_LOGIN, user.getLogin());
writeIfNeeded(json, user.getName(), FIELD_NAME, fields);
@@ -76,7 +69,6 @@ public void write(JsonWriter json, UserDto user, @Nullable Integer tokensCount,
writeIfNeeded(json, user.getExternalIdentityProvider(), FIELD_EXTERNAL_PROVIDER, fields);
writeGroupsIfNeeded(json, groups, fields);
writeScmAccountsIfNeeded(json, fields, user);
- writeTokensCount(json, tokensCount);
}
json.endObject();
}
@@ -111,11 +103,4 @@ private static void writeScmAccountsIfNeeded(JsonWriter json, Collection
}
}
- private static void writeTokensCount(JsonWriter json, @Nullable Integer tokenCount) {
- if (tokenCount == null) {
- return;
- }
-
- json.prop(FIELD_TOKENS_COUNT, tokenCount);
- }
}
diff --git a/server/sonar-server/src/main/resources/org/sonar/server/user/ws/search-example.json b/server/sonar-server/src/main/resources/org/sonar/server/user/ws/search-example.json
index 1746fe7b4c69..b7b28fa2b179 100644
--- a/server/sonar-server/src/main/resources/org/sonar/server/user/ws/search-example.json
+++ b/server/sonar-server/src/main/resources/org/sonar/server/user/ws/search-example.json
@@ -1,14 +1,18 @@
{
+ "paging": {
+ "pageIndex": 1,
+ "pageSize": 50,
+ "total": 2
+ },
"users": [
{
"login": "fmallet",
"name": "Freddy Mallet",
"active": true,
"email": "f@m.com",
- "scmAccounts": [],
"groups": [
- "sonar-users",
- "sonar-administrators"
+ "sonar-administrators",
+ "sonar-users"
],
"tokensCount": 1,
"local": true,
diff --git a/server/sonar-server/src/test/java/org/sonar/server/user/ws/SearchActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/user/ws/SearchActionTest.java
index cf5dc37bec48..c431021da916 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/user/ws/SearchActionTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/user/ws/SearchActionTest.java
@@ -67,7 +67,7 @@ public class SearchActionTest {
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 {
@@ -103,7 +103,14 @@ 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
@@ -196,13 +203,7 @@ public void search_with_fields() throws Exception {
@Test
public void search_with_groups() throws Exception {
loginAsSystemAdministrator();
- List 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");
}
@@ -229,6 +230,8 @@ public void only_return_login_and_name_when_not_logged() throws Exception {
private List injectUsers(int numberOfUsers) throws Exception {
List 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);
@@ -253,6 +256,8 @@ private List injectUsers(int numberOfUsers) throws Exception {
.setLogin(login)
.setName(String.format("%s-%d", login, tokenIndex)));
}
+ db.users().insertMember(group1, userDto);
+ db.users().insertMember(group2, userDto);
}
dbSession.commit();
userIndexer.indexOnStartup(null);
diff --git a/server/sonar-server/src/test/java/org/sonar/server/user/ws/UsersWsTest.java b/server/sonar-server/src/test/java/org/sonar/server/user/ws/UsersWsTest.java
index ed3bfeaffd2f..f336cdabaa6b 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/user/ws/UsersWsTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/user/ws/UsersWsTest.java
@@ -46,7 +46,7 @@ public void setUp() {
new UpdateAction(mock(UserUpdater.class), userSessionRule, mock(UserJsonWriter.class), mock(DbClient.class)),
new CurrentAction(userSessionRule, mock(DbClient.class), mock(DefaultOrganizationProvider.class)),
new ChangePasswordAction(mock(DbClient.class), mock(UserUpdater.class), userSessionRule),
- new SearchAction(mock(UserIndex.class), mock(DbClient.class), mock(UserJsonWriter.class))));
+ new SearchAction(userSessionRule, mock(UserIndex.class), mock(DbClient.class))));
controller = tester.controller("api/users");
}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/empty.json b/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/empty.json
deleted file mode 100644
index bce6d1f86c2d..000000000000
--- a/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/empty.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "p": 1,
- "ps": 50,
- "total": 0,
- "users": []
-}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/five_users.json b/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/five_users.json
index 960c322ab49e..0f7676f95dc0 100644
--- a/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/five_users.json
+++ b/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/five_users.json
@@ -1,7 +1,9 @@
{
- "p": 1,
- "ps": 50,
- "total": 5,
+ "paging": {
+ "pageIndex": 1,
+ "pageSize": 50,
+ "total": 5
+ },
"users": [
{
"login": "user-0",
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/page_one.json b/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/page_one.json
index 0ecf67664bb6..908dbf0f620f 100644
--- a/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/page_one.json
+++ b/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/page_one.json
@@ -1,7 +1,9 @@
{
- "p": 1,
- "ps": 5,
- "total": 10,
+ "paging": {
+ "pageIndex": 1,
+ "pageSize": 5,
+ "total": 10
+ },
"users": [
{
"login": "user-0",
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/page_two.json b/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/page_two.json
index 43af76c6b110..31d17a4591a7 100644
--- a/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/page_two.json
+++ b/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/page_two.json
@@ -1,7 +1,9 @@
{
- "p": 2,
- "ps": 5,
- "total": 10,
+ "paging": {
+ "pageIndex": 2,
+ "pageSize": 5,
+ "total": 10
+ },
"users": [
{
"login": "user-5",
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/user_one.json b/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/user_one.json
index 6839d14f0f0d..f6d1966de621 100644
--- a/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/user_one.json
+++ b/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/user_one.json
@@ -1,14 +1,18 @@
{
- "total": 1,
- "p": 1,
- "ps": 50,
+ "paging": {
+ "pageIndex": 1,
+ "pageSize": 50,
+ "total": 1
+ },
"users": [
{
"login": "user-%_%-login",
"name": "user-name",
"email": "user@mail.com",
"active": true,
- "scmAccounts": ["user1"],
+ "scmAccounts": [
+ "user1"
+ ],
"tokensCount": 0,
"local": true
}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/user_with_groups.json b/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/user_with_groups.json
index a0a2c157a7b4..f9ffeda9b6f7 100644
--- a/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/user_with_groups.json
+++ b/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/user_with_groups.json
@@ -1,7 +1,9 @@
{
- "p": 1,
- "ps": 50,
- "total": 1,
+ "paging": {
+ "pageIndex": 1,
+ "pageSize": 50,
+ "total": 1
+ },
"users": [
{
"login": "user-0",
diff --git a/server/sonar-web/src/main/js/apps/users/users.js b/server/sonar-web/src/main/js/apps/users/users.js
index a2a49dc4510f..228d19d73c57 100644
--- a/server/sonar-web/src/main/js/apps/users/users.js
+++ b/server/sonar-web/src/main/js/apps/users/users.js
@@ -28,9 +28,9 @@ export default Backbone.Collection.extend({
},
parse(r) {
- this.total = +r.total;
- this.p = +r.p;
- this.ps = +r.ps;
+ this.total = +r.paging.total;
+ this.p = +r.paging.pageIndex;
+ this.ps = +r.paging.pageSize;
return r.users;
},
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/user/SearchRequest.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/user/SearchRequest.java
new file mode 100644
index 000000000000..52e19f346076
--- /dev/null
+++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/user/SearchRequest.java
@@ -0,0 +1,100 @@
+/*
+ * 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 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 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 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 possibleFields) {
+ this.additionalFields = possibleFields;
+ return this;
+ }
+
+ public SearchRequest build() {
+ return new SearchRequest(this);
+ }
+ }
+}
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/user/UsersService.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/user/UsersService.java
index d984c5ea55b0..8467336d1c05 100644
--- a/sonar-ws/src/main/java/org/sonarqube/ws/client/user/UsersService.java
+++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/user/UsersService.java
@@ -19,18 +19,22 @@
*/
package org.sonarqube.ws.client.user;
+import java.util.List;
import org.sonarqube.ws.WsUsers.CreateWsResponse;
import org.sonarqube.ws.WsUsers.GroupsWsResponse;
+import org.sonarqube.ws.WsUsers.SearchWsResponse;
import org.sonarqube.ws.client.BaseService;
import org.sonarqube.ws.client.GetRequest;
import org.sonarqube.ws.client.PostRequest;
import org.sonarqube.ws.client.WsConnector;
+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.sonarqube.ws.client.user.UsersWsParameters.ACTION_CREATE;
import static org.sonarqube.ws.client.user.UsersWsParameters.ACTION_GROUPS;
+import static org.sonarqube.ws.client.user.UsersWsParameters.ACTION_SEARCH;
import static org.sonarqube.ws.client.user.UsersWsParameters.ACTION_UPDATE;
import static org.sonarqube.ws.client.user.UsersWsParameters.CONTROLLER_USERS;
import static org.sonarqube.ws.client.user.UsersWsParameters.PARAM_EMAIL;
@@ -48,6 +52,16 @@ public UsersService(WsConnector wsConnector) {
super(wsConnector, CONTROLLER_USERS);
}
+ public SearchWsResponse search(SearchRequest request) {
+ List additionalFields = request.getPossibleFields();
+ return call(new GetRequest(path(ACTION_SEARCH))
+ .setParam(PAGE, request.getPage())
+ .setParam(PAGE_SIZE, request.getPageSize())
+ .setParam(TEXT_QUERY, request.getQuery())
+ .setParam(FIELDS, !additionalFields.isEmpty() ? inlineMultipleParamValue(additionalFields) : null),
+ SearchWsResponse.parser());
+ }
+
public CreateWsResponse create(CreateRequest request) {
return call(new PostRequest(path(ACTION_CREATE))
.setParam(PARAM_LOGIN, request.getLogin())
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/user/UsersWsParameters.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/user/UsersWsParameters.java
index ba674caf9d3f..6f153218db99 100644
--- a/sonar-ws/src/main/java/org/sonarqube/ws/client/user/UsersWsParameters.java
+++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/user/UsersWsParameters.java
@@ -23,6 +23,7 @@ public class UsersWsParameters {
public static final String CONTROLLER_USERS = "api/users";
+ public static final String ACTION_SEARCH = "search";
public static final String ACTION_CREATE = "create";
public static final String ACTION_UPDATE = "update";
public static final String ACTION_GROUPS = "groups";
diff --git a/sonar-ws/src/main/protobuf/ws-users.proto b/sonar-ws/src/main/protobuf/ws-users.proto
index d34882789a50..21aba165b084 100644
--- a/sonar-ws/src/main/protobuf/ws-users.proto
+++ b/sonar-ws/src/main/protobuf/ws-users.proto
@@ -26,6 +26,34 @@ option java_package = "org.sonarqube.ws";
option java_outer_classname = "WsUsers";
option optimize_for = SPEED;
+// WS api/users/search
+message SearchWsResponse {
+ optional sonarqube.ws.commons.Paging paging = 1;
+ repeated User users = 2;
+
+ message User {
+ optional string login = 1;
+ optional string name = 2;
+ optional bool active = 3;
+ optional string email = 4;
+ optional ScmAccounts scmAccounts = 5;
+ optional Groups groups = 6;
+ optional int32 tokensCount = 7;
+ optional bool local = 8;
+ optional string externalIdentity = 9;
+ optional string externalProvider = 10;
+ optional string avatar = 11;
+ }
+
+ message Groups {
+ repeated string groups = 1;
+ }
+
+ message ScmAccounts {
+ repeated string scmAccounts = 1;
+ }
+}
+
// WS api/users/identity_providers
message IdentityProvidersWsResponse {
repeated IdentityProvider identityProviders = 1;
diff --git a/sonar-ws/src/test/java/org/sonarqube/ws/client/user/UsersServiceTest.java b/sonar-ws/src/test/java/org/sonarqube/ws/client/user/UsersServiceTest.java
index c92f7619ffe1..0933d16b26c2 100644
--- a/sonar-ws/src/test/java/org/sonarqube/ws/client/user/UsersServiceTest.java
+++ b/sonar-ws/src/test/java/org/sonarqube/ws/client/user/UsersServiceTest.java
@@ -21,6 +21,7 @@
import org.junit.Rule;
import org.junit.Test;
+import org.sonarqube.ws.WsUsers;
import org.sonarqube.ws.WsUsers.CreateWsResponse;
import org.sonarqube.ws.WsUsers.GroupsWsResponse;
import org.sonarqube.ws.client.ServiceTester;
@@ -29,6 +30,7 @@
import static java.util.Arrays.asList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
+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;
@@ -48,6 +50,24 @@ public class UsersServiceTest {
private UsersService underTest = serviceTester.getInstanceUnderTest();
+ @Test
+ public void search() {
+ underTest.search(SearchRequest.builder()
+ .setQuery("john")
+ .setPage(10)
+ .setPageSize(50)
+ .setPossibleFields(asList("email", "name"))
+ .build());
+
+ assertThat(serviceTester.getGetParser()).isSameAs(WsUsers.SearchWsResponse.parser());
+ serviceTester.assertThat(serviceTester.getGetRequest())
+ .hasParam(TEXT_QUERY, "john")
+ .hasParam(PAGE, 10)
+ .hasParam(PAGE_SIZE, 50)
+ .hasParam(FIELDS, "email,name")
+ .andNoOtherParam();
+ }
+
@Test
public void create() {
underTest.create(CreateRequest.builder()