Skip to content

Commit

Permalink
SONAR-9128 Use protobuf in api/users/search
Browse files Browse the repository at this point in the history
  • Loading branch information
julienlancelot committed Apr 21, 2017
1 parent 7c5b10e commit 9061336
Show file tree
Hide file tree
Showing 17 changed files with 313 additions and 85 deletions.
Expand Up @@ -19,41 +19,66 @@
*/ */
package org.sonar.server.user.ws; 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 com.google.common.collect.Multimap;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import javax.annotation.Nonnull; import java.util.function.Function;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import org.sonar.api.server.ws.Change;
import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Request;
import org.sonar.api.server.ws.Response; import org.sonar.api.server.ws.Response;
import org.sonar.api.server.ws.WebService; import org.sonar.api.server.ws.WebService;
import org.sonar.api.server.ws.WebService.Param; import org.sonar.api.utils.Paging;
import org.sonar.api.utils.text.JsonWriter;
import org.sonar.db.DbClient; import org.sonar.db.DbClient;
import org.sonar.db.DbSession; import org.sonar.db.DbSession;
import org.sonar.db.user.UserDto; import org.sonar.db.user.UserDto;
import org.sonar.server.es.SearchOptions; import org.sonar.server.es.SearchOptions;
import org.sonar.server.es.SearchResult; 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.UserDoc;
import org.sonar.server.user.index.UserIndex; import org.sonar.server.user.index.UserIndex;
import org.sonar.server.user.index.UserQuery; 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.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.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 { public class SearchAction implements UsersWsAction {


private static final int MAX_PAGE_SIZE = 500;

private final UserSession userSession;
private final UserIndex userIndex; private final UserIndex userIndex;
private final DbClient dbClient; 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.userIndex = userIndex;
this.dbClient = dbClient; this.dbClient = dbClient;
this.userWriter = userWriter;
} }


@Override @Override
Expand All @@ -63,54 +88,94 @@ public void define(WebService.NewController controller) {
"Administer System permission is required to show the 'groups' field.<br/>" + "Administer System permission is required to show the 'groups' field.<br/>" +
"When accessed anonymously, only logins and names are returned.") "When accessed anonymously, only logins and names are returned.")
.setSince("3.6") .setSince("3.6")
.setChangelog(new Change("6.4", "Paging response fields moved to a Paging object"))
.setHandler(this) .setHandler(this)
.setResponseExample(getClass().getResource("search-example.json")); .setResponseExample(getClass().getResource("search-example.json"));


action.createFieldsParam(UserJsonWriter.FIELDS) action.createFieldsParam(UserJsonWriter.FIELDS)
.setDeprecatedSince("5.4"); .setDeprecatedSince("5.4");
action.addPagingParams(50, MAX_LIMIT); action.addPagingParams(50, MAX_LIMIT);


action.createParam(Param.TEXT_QUERY) action.createParam(TEXT_QUERY)
.setDescription("Filter on login or name."); .setDescription("Filter on login or name.");
} }


@Override @Override
public void handle(Request request, Response response) throws Exception { public void handle(Request request, Response response) throws Exception {
SearchOptions options = new SearchOptions() WsUsers.SearchWsResponse wsResponse = doHandle(toSearchRequest(request));
.setPage(request.mandatoryParamAsInt(Param.PAGE), request.mandatoryParamAsInt(Param.PAGE_SIZE)); writeProtobuf(wsResponse, request, response);
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);


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)) { 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); Multimap<String, String> groupsByLogin = dbClient.groupMembershipDao().selectGroupsByLogins(dbSession, logins);
Map<String, Integer> tokenCountsByLogin = dbClient.userTokenDao().countTokensByLogins(dbSession, logins); Map<String, Integer> tokenCountsByLogin = dbClient.userTokenDao().countTokensByLogins(dbSession, logins);
JsonWriter json = response.newJsonWriter().beginObject(); List<UserDto> users = dbClient.userDao().selectByOrderedLogins(dbSession, logins);
options.writeJson(json, result.getTotal()); Paging paging = forPageIndex(request.getPage()).withPageSize(request.getPageSize()).andTotal((int) result.getTotal());
List<UserDto> userDtos = dbClient.userDao().selectByOrderedLogins(dbSession, logins); return buildResponse(users, groupsByLogin, tokenCountsByLogin, fields, paging);
writeUsers(json, userDtos, groupsByLogin, tokenCountsByLogin, fields);
json.endObject().close();
} }
} }


private void writeUsers(JsonWriter json, List<UserDto> userDtos, Multimap<String, String> groupsByLogin, Map<String, Integer> tokenCountsByLogin, private SearchWsResponse buildResponse(List<UserDto> users, Multimap<String, String> groupsByLogin, Map<String, Integer> tokenCountsByLogin,
@Nullable List<String> fields) { @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(); private User towsUser(UserDto user, @Nullable Integer tokensCount, Collection<String> groups, @Nullable Collection<String> fields) {
for (UserDto user : userDtos) { User.Builder userBuilder = User.newBuilder()
Collection<String> groups = groupsByLogin.get(user.getLogin()); .setLogin(user.getLogin());
userWriter.write(json, user, firstNonNull(tokenCountsByLogin.get(user.getLogin()), 0), groups, fields); 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> { private static <PARAM> void setIfNeeded(String field, @Nullable Collection<String> fields, @Nullable PARAM parameter, Function<PARAM, ?> setter) {
INSTANCE; setIfNeeded(isNeeded(field, fields), parameter, setter);
}


@Override private static <PARAM> void setIfNeeded(boolean condition, @Nullable PARAM parameter, Function<PARAM, ?> setter) {
public String apply(@Nonnull UserDoc input) { if (parameter != null && condition) {
return input.login(); 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();
}

} }
Expand Up @@ -58,13 +58,6 @@ public UserJsonWriter(UserSession userSession) {
* Serializes a user to the passed JsonWriter. * Serializes a user to the passed JsonWriter.
*/ */
public void write(JsonWriter json, UserDto user, Collection<String> groups, @Nullable Collection<String> fields) { 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.beginObject();
json.prop(FIELD_LOGIN, user.getLogin()); json.prop(FIELD_LOGIN, user.getLogin());
writeIfNeeded(json, user.getName(), FIELD_NAME, fields); writeIfNeeded(json, user.getName(), FIELD_NAME, fields);
Expand All @@ -76,7 +69,6 @@ public void write(JsonWriter json, UserDto user, @Nullable Integer tokensCount,
writeIfNeeded(json, user.getExternalIdentityProvider(), FIELD_EXTERNAL_PROVIDER, fields); writeIfNeeded(json, user.getExternalIdentityProvider(), FIELD_EXTERNAL_PROVIDER, fields);
writeGroupsIfNeeded(json, groups, fields); writeGroupsIfNeeded(json, groups, fields);
writeScmAccountsIfNeeded(json, fields, user); writeScmAccountsIfNeeded(json, fields, user);
writeTokensCount(json, tokensCount);
} }
json.endObject(); json.endObject();
} }
Expand Down Expand Up @@ -111,11 +103,4 @@ private static void writeScmAccountsIfNeeded(JsonWriter json, Collection<String>
} }
} }


private static void writeTokensCount(JsonWriter json, @Nullable Integer tokenCount) {
if (tokenCount == null) {
return;
}

json.prop(FIELD_TOKENS_COUNT, tokenCount);
}
} }
@@ -1,14 +1,18 @@
{ {
"paging": {
"pageIndex": 1,
"pageSize": 50,
"total": 2
},
"users": [ "users": [
{ {
"login": "fmallet", "login": "fmallet",
"name": "Freddy Mallet", "name": "Freddy Mallet",
"active": true, "active": true,
"email": "f@m.com", "email": "f@m.com",
"scmAccounts": [],
"groups": [ "groups": [
"sonar-users", "sonar-administrators",
"sonar-administrators" "sonar-users"
], ],
"tokensCount": 1, "tokensCount": 1,
"local": true, "local": true,
Expand Down
Expand Up @@ -67,7 +67,7 @@ public class SearchActionTest {
private DbSession dbSession = db.getSession(); private DbSession dbSession = db.getSession();
private UserIndex index = new UserIndex(esTester.client()); private UserIndex index = new UserIndex(esTester.client());
private UserIndexer userIndexer = new UserIndexer(dbClient, 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 @Test
public void search_json_example() throws Exception { public void search_json_example() throws Exception {
Expand Down Expand Up @@ -103,7 +103,14 @@ public void search_json_example() throws Exception {
@Test @Test
public void search_empty() throws Exception { public void search_empty() throws Exception {
loginAsSimpleUser(); 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
Expand Down Expand Up @@ -196,13 +203,7 @@ public void search_with_fields() throws Exception {
@Test @Test
public void search_with_groups() throws Exception { public void search_with_groups() throws Exception {
loginAsSystemAdministrator(); loginAsSystemAdministrator();
List<UserDto> users = injectUsers(1); 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();


ws.newGetRequest("api/users", "search").execute().assertJson(getClass(), "user_with_groups.json"); ws.newGetRequest("api/users", "search").execute().assertJson(getClass(), "user_with_groups.json");
} }
Expand All @@ -229,6 +230,8 @@ public void only_return_login_and_name_when_not_logged() throws Exception {
private List<UserDto> injectUsers(int numberOfUsers) throws Exception { private List<UserDto> injectUsers(int numberOfUsers) throws Exception {
List<UserDto> userDtos = Lists.newArrayList(); List<UserDto> userDtos = Lists.newArrayList();
long createdAt = System.currentTimeMillis(); 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++) { for (int index = 0; index < numberOfUsers; index++) {
String email = String.format("user-%d@mail.com", index); String email = String.format("user-%d@mail.com", index);
String login = String.format("user-%d", index); String login = String.format("user-%d", index);
Expand All @@ -253,6 +256,8 @@ private List<UserDto> injectUsers(int numberOfUsers) throws Exception {
.setLogin(login) .setLogin(login)
.setName(String.format("%s-%d", login, tokenIndex))); .setName(String.format("%s-%d", login, tokenIndex)));
} }
db.users().insertMember(group1, userDto);
db.users().insertMember(group2, userDto);
} }
dbSession.commit(); dbSession.commit();
userIndexer.indexOnStartup(null); userIndexer.indexOnStartup(null);
Expand Down
Expand Up @@ -46,7 +46,7 @@ public void setUp() {
new UpdateAction(mock(UserUpdater.class), userSessionRule, mock(UserJsonWriter.class), mock(DbClient.class)), new UpdateAction(mock(UserUpdater.class), userSessionRule, mock(UserJsonWriter.class), mock(DbClient.class)),
new CurrentAction(userSessionRule, mock(DbClient.class), mock(DefaultOrganizationProvider.class)), new CurrentAction(userSessionRule, mock(DbClient.class), mock(DefaultOrganizationProvider.class)),
new ChangePasswordAction(mock(DbClient.class), mock(UserUpdater.class), userSessionRule), 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"); controller = tester.controller("api/users");
} }


Expand Down

This file was deleted.

@@ -1,7 +1,9 @@
{ {
"p": 1, "paging": {
"ps": 50, "pageIndex": 1,
"total": 5, "pageSize": 50,
"total": 5
},
"users": [ "users": [
{ {
"login": "user-0", "login": "user-0",
Expand Down
@@ -1,7 +1,9 @@
{ {
"p": 1, "paging": {
"ps": 5, "pageIndex": 1,
"total": 10, "pageSize": 5,
"total": 10
},
"users": [ "users": [
{ {
"login": "user-0", "login": "user-0",
Expand Down
@@ -1,7 +1,9 @@
{ {
"p": 2, "paging": {
"ps": 5, "pageIndex": 2,
"total": 10, "pageSize": 5,
"total": 10
},
"users": [ "users": [
{ {
"login": "user-5", "login": "user-5",
Expand Down
@@ -1,14 +1,18 @@
{ {
"total": 1, "paging": {
"p": 1, "pageIndex": 1,
"ps": 50, "pageSize": 50,
"total": 1
},
"users": [ "users": [
{ {
"login": "user-%_%-login", "login": "user-%_%-login",
"name": "user-name", "name": "user-name",
"email": "user@mail.com", "email": "user@mail.com",
"active": true, "active": true,
"scmAccounts": ["user1"], "scmAccounts": [
"user1"
],
"tokensCount": 0, "tokensCount": 0,
"local": true "local": true
} }
Expand Down
@@ -1,7 +1,9 @@
{ {
"p": 1, "paging": {
"ps": 50, "pageIndex": 1,
"total": 1, "pageSize": 50,
"total": 1
},
"users": [ "users": [
{ {
"login": "user-0", "login": "user-0",
Expand Down

0 comments on commit 9061336

Please sign in to comment.