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()