]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9128 Use protobuf in api/users/search
authorJulien Lancelot <julien.lancelot@sonarsource.com>
Thu, 20 Apr 2017 09:06:01 +0000 (11:06 +0200)
committerJulien Lancelot <julien.lancelot@sonarsource.com>
Fri, 21 Apr 2017 14:01:31 +0000 (16:01 +0200)
17 files changed:
server/sonar-server/src/main/java/org/sonar/server/user/ws/SearchAction.java
server/sonar-server/src/main/java/org/sonar/server/user/ws/UserJsonWriter.java
server/sonar-server/src/main/resources/org/sonar/server/user/ws/search-example.json
server/sonar-server/src/test/java/org/sonar/server/user/ws/SearchActionTest.java
server/sonar-server/src/test/java/org/sonar/server/user/ws/UsersWsTest.java
server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/empty.json [deleted file]
server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/five_users.json
server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/page_one.json
server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/page_two.json
server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/user_one.json
server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/user_with_groups.json
server/sonar-web/src/main/js/apps/users/users.js
sonar-ws/src/main/java/org/sonarqube/ws/client/user/SearchRequest.java [new file with mode: 0644]
sonar-ws/src/main/java/org/sonarqube/ws/client/user/UsersService.java
sonar-ws/src/main/java/org/sonarqube/ws/client/user/UsersWsParameters.java
sonar-ws/src/main/protobuf/ws-users.proto
sonar-ws/src/test/java/org/sonarqube/ws/client/user/UsersServiceTest.java

index 4fd0d0da7615ffee3a5493e3ca4e03da0876094b..7e42599308b6f322214bab7870fc74ff1b9a6d90 100644 (file)
  */
 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 class SearchAction implements UsersWsAction {
         "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"));
 
@@ -70,47 +96,86 @@ public class SearchAction implements UsersWsAction {
       .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();
+  }
+
 }
index a26136bdc28cb038be218f21d1db5eb958f17083..f2a6aa933bf673f7e3900a887ab08870ac0ba629 100644 (file)
@@ -58,13 +58,6 @@ public class UserJsonWriter {
    * Serializes a user to the passed JsonWriter.
    */
   public void write(JsonWriter json, UserDto user, Collection<String> groups, @Nullable Collection<String> 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<String> groups, @Nullable Collection<String> fields) {
     json.beginObject();
     json.prop(FIELD_LOGIN, user.getLogin());
     writeIfNeeded(json, user.getName(), FIELD_NAME, fields);
@@ -76,7 +69,6 @@ public class UserJsonWriter {
       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 @@ public class UserJsonWriter {
     }
   }
 
-  private static void writeTokensCount(JsonWriter json, @Nullable Integer tokenCount) {
-    if (tokenCount == null) {
-      return;
-    }
-
-    json.prop(FIELD_TOKENS_COUNT, tokenCount);
-  }
 }
index 1746fe7b4c698a47cf34ecd4b15e788b82656908..b7b28fa2b1796469e0ed7555768f4917d5bce140 100644 (file)
@@ -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,
index cf5dc37bec4806dd849c8797484c8eb49a663039..c431021da91683bb303fd6013abda5df5b09de02 100644 (file)
@@ -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 class SearchActionTest {
   @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 class SearchActionTest {
   @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");
   }
@@ -229,6 +230,8 @@ public class SearchActionTest {
   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);
@@ -253,6 +256,8 @@ public class SearchActionTest {
           .setLogin(login)
           .setName(String.format("%s-%d", login, tokenIndex)));
       }
+      db.users().insertMember(group1, userDto);
+      db.users().insertMember(group2, userDto);
     }
     dbSession.commit();
     userIndexer.indexOnStartup(null);
index ed3bfeaffd2f1da1383b388965feb4de96e862fd..f336cdabaa6baa78f13e2dcf19f9ee8efa6debfb 100644 (file)
@@ -46,7 +46,7 @@ public class UsersWsTest {
       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 (file)
index bce6d1f..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-{
-  "p": 1,
-  "ps": 50,
-  "total": 0,
-  "users": []
-}
index 960c322ab49e7430e983e86bd5bb39b21e6f5f44..0f7676f95dc07e089fe0fd78dfccbebffc794658 100644 (file)
@@ -1,7 +1,9 @@
 {
-  "p": 1,
-  "ps": 50,
-  "total": 5,
+  "paging": {
+    "pageIndex": 1,
+    "pageSize": 50,
+    "total": 5
+  },
   "users": [
     {
       "login": "user-0",
index 0ecf67664bb68491f5da95ad5c6884505a838396..908dbf0f620ff7d59cf78f84dd84a51f9328f10a 100644 (file)
@@ -1,7 +1,9 @@
 {
-  "p": 1,
-  "ps": 5,
-  "total": 10,
+  "paging": {
+    "pageIndex": 1,
+    "pageSize": 5,
+    "total": 10
+  },
   "users": [
     {
       "login": "user-0",
index 43af76c6b1104dfed82c0ca64b0ce8ebb2728d93..31d17a4591a7c6fce693b7b711d4c511ba9df88c 100644 (file)
@@ -1,7 +1,9 @@
 {
-  "p": 2,
-  "ps": 5,
-  "total": 10,
+  "paging": {
+    "pageIndex": 2,
+    "pageSize": 5,
+    "total": 10
+  },
   "users": [
     {
       "login": "user-5",
index 6839d14f0f0d60ac0f367633e46eddbec6f3d73b..f6d1966de621659f32e41a49255150344e8892d6 100644 (file)
@@ -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
     }
index a0a2c157a7b409632fe4f5726da771e419ee9fbf..f9ffeda9b6f7b9b972602362f07c072670cb7b22 100644 (file)
@@ -1,7 +1,9 @@
 {
-  "p": 1,
-  "ps": 50,
-  "total": 1,
+  "paging": {
+    "pageIndex": 1,
+    "pageSize": 50,
+    "total": 1
+  },
   "users": [
     {
       "login": "user-0",
index a2a49dc4510f0b9336b80ebeaea518074ee4297f..228d19d73c571397543687ab711d18e9df96232e 100644 (file)
@@ -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 (file)
index 0000000..52e19f3
--- /dev/null
@@ -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<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);
+    }
+  }
+}
index d984c5ea55b069465b42f845dbcd3a9a08280f8f..8467336d1c05d6dc5719f058d75fe68dc6069cff 100644 (file)
  */
 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 class UsersService extends BaseService {
     super(wsConnector, CONTROLLER_USERS);
   }
 
+  public SearchWsResponse search(SearchRequest request) {
+    List<String> 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())
index ba674caf9d3f69037f042bec96c50c9959cab805..6f153218db998eba940f40229a43c27026e8d4ab 100644 (file)
@@ -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";
index d34882789a50e5a0af48f6956b3de63e32afe8a5..21aba165b0846695da8b289bbee458ad5837f72f 100644 (file)
@@ -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;
index c92f7619ffe138f4ccd97739d5ce6a4db17110d7..0933d16b26c25cf53217ac642c809b12fc825b7b 100644 (file)
@@ -21,6 +21,7 @@ package org.sonarqube.ws.client.user;
 
 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 org.sonarqube.ws.client.WsConnector;
 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()