@@ -20,6 +20,7 @@ | |||
package org.sonar.db.organization; | |||
import java.util.List; | |||
import java.util.Optional; | |||
import java.util.Set; | |||
import org.sonar.db.Dao; | |||
@@ -34,6 +35,10 @@ public class OrganizationMemberDao implements Dao { | |||
return Optional.ofNullable(mapper(dbSession).select(organizationUuid, userId)); | |||
} | |||
public List<String> selectLoginsByOrganizationUuid(DbSession dbSession, String organizationUuid) { | |||
return mapper(dbSession).selectLogins(organizationUuid); | |||
} | |||
public void insert(DbSession dbSession, OrganizationMemberDto organizationMemberDto) { | |||
mapper(dbSession).insert(organizationMemberDto); | |||
} |
@@ -20,6 +20,7 @@ | |||
package org.sonar.db.organization; | |||
import java.util.List; | |||
import java.util.Set; | |||
import org.apache.ibatis.annotations.Param; | |||
@@ -28,6 +29,8 @@ public interface OrganizationMemberMapper { | |||
Set<String> selectOrganizationUuidsByUser(@Param("userId") int userId); | |||
List<String> selectLogins(String organizationUuid); | |||
void insert(OrganizationMemberDto organizationMember); | |||
void delete(@Param("organizationUuid") String organizationUuid, @Param("userId") Integer userId); |
@@ -23,6 +23,7 @@ import com.google.common.collect.ArrayListMultimap; | |||
import com.google.common.collect.ImmutableMap; | |||
import com.google.common.collect.Maps; | |||
import com.google.common.collect.Multimap; | |||
import com.google.common.collect.Multiset; | |||
import java.util.Collection; | |||
import java.util.List; | |||
import java.util.Map; | |||
@@ -73,6 +74,21 @@ public class GroupMembershipDao implements Dao { | |||
return mapper(dbSession).selectGroupIdsByUserId(userId); | |||
} | |||
public Multiset<String> countGroupByLoginsAndOrganization(DbSession dbSession, Collection<String> logins, String organizationUuid) { | |||
Multimap<String, String> result = ArrayListMultimap.create(); | |||
executeLargeInputs( | |||
logins, | |||
input -> { | |||
List<LoginGroup> groupMemberships = mapper(dbSession).selectGroupsByLoginsAndOrganization(input, organizationUuid); | |||
for (LoginGroup membership : groupMemberships) { | |||
result.put(membership.login(), membership.groupName()); | |||
} | |||
return groupMemberships; | |||
}); | |||
return result.keys(); | |||
} | |||
public Multimap<String, String> selectGroupsByLogins(DbSession session, Collection<String> logins) { | |||
Multimap<String, String> result = ArrayListMultimap.create(); | |||
executeLargeInputs( |
@@ -38,6 +38,7 @@ public interface GroupMembershipMapper { | |||
List<LoginGroup> selectGroupsByLogins(@Param("logins") List<String> logins); | |||
List<LoginGroup> selectGroupsByLoginsAndOrganization(@Param("logins") List<String> logins, @Param("organizationUuid") String organizationUuid); | |||
List<Integer> selectGroupIdsByUserId(@Param("userId") int userId); | |||
} |
@@ -17,6 +17,13 @@ | |||
and om.user_id = #{userId, jdbcType=INTEGER} | |||
</select> | |||
<select id="selectLogins" resultType="string"> | |||
select u.login | |||
from organization_members om | |||
inner join users u on om.user_id = u.id | |||
where om.organization_uuid=#{organizationUuid,jdbcType=VARCHAR} | |||
</select> | |||
<select id="selectOrganizationUuidsByUser" resultType="String"> | |||
select om.organization_uuid as "organizationUuid" | |||
from organization_members om | |||
@@ -36,7 +43,7 @@ | |||
) | |||
</insert> | |||
<delete id="delete" parameterType="map"> | |||
<delete id="delete"> | |||
delete from organization_members | |||
where | |||
organization_uuid = #{organizationUuid, jdbcType=VARCHAR} |
@@ -59,6 +59,21 @@ | |||
ORDER BY u.login, g.name, g.id | |||
</select> | |||
<select id="selectGroupsByLoginsAndOrganization" parameterType="map" resultType="org.sonar.db.user.LoginGroup"> | |||
SELECT u.login as login, g.name as groupName | |||
FROM users u | |||
LEFT JOIN groups_users gu ON gu.user_id=u.id | |||
INNER JOIN groups g ON gu.group_id=g.id | |||
<where> | |||
u.login in | |||
<foreach collection="logins" open="(" close=")" item="login" separator=","> | |||
#{login} | |||
</foreach> | |||
and g.organization_uuid=#{organizationUuid,jdbcType=VARCHAR} | |||
</where> | |||
ORDER BY u.login, g.name, g.id | |||
</select> | |||
<sql id="userCommonClauses"> | |||
FROM users u | |||
LEFT JOIN groups_users gu ON gu.user_id=u.id AND gu.group_id=#{groupId} |
@@ -20,6 +20,7 @@ | |||
package org.sonar.db.organization; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.Optional; | |||
import org.apache.ibatis.exceptions.PersistenceException; | |||
@@ -29,6 +30,7 @@ import org.junit.rules.ExpectedException; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.DbTester; | |||
import org.sonar.db.user.UserDto; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.assertj.core.api.Assertions.entry; | |||
@@ -56,6 +58,22 @@ public class OrganizationMemberDaoTest { | |||
assertThat(underTest.select(dbSession, "O2", 512)).isNotPresent(); | |||
} | |||
@Test | |||
public void select_logins() { | |||
OrganizationDto organization = db.organizations().insert(); | |||
OrganizationDto anotherOrganization = db.organizations().insert(); | |||
UserDto user = db.users().insertUser(); | |||
UserDto anotherUser = db.users().insertUser(); | |||
UserDto userInAnotherOrganization = db.users().insertUser(); | |||
db.organizations().addMember(organization, user); | |||
db.organizations().addMember(organization, anotherUser); | |||
db.organizations().addMember(anotherOrganization, userInAnotherOrganization); | |||
List<String> result = underTest.selectLoginsByOrganizationUuid(dbSession, organization.getUuid()); | |||
assertThat(result).containsOnly(user.getLogin(), anotherUser.getLogin()); | |||
} | |||
@Test | |||
public void select_organization_uuids_by_user_id() { | |||
OrganizationDto organizationDto1 = db.organizations().insert(); |
@@ -63,7 +63,7 @@ public class GroupMembershipDaoTest { | |||
} | |||
@Test | |||
public void count_groups_by_login() { | |||
public void count_groups_by_logins() { | |||
dbTester.prepareDbUnit(getClass(), "shared.xml"); | |||
assertThat(underTest.selectGroupsByLogins(dbTester.getSession(), Arrays.<String>asList()).keys()).isEmpty(); |
@@ -20,25 +20,74 @@ | |||
package org.sonar.server.organization.ws; | |||
import com.google.common.collect.Multiset; | |||
import com.google.common.collect.Ordering; | |||
import com.google.common.hash.Hashing; | |||
import java.nio.charset.StandardCharsets; | |||
import java.util.List; | |||
import java.util.Locale; | |||
import java.util.Optional; | |||
import javax.annotation.Nullable; | |||
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.server.ws.WebService.SelectionMode; | |||
import org.sonar.core.util.stream.Collectors; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.organization.OrganizationDto; | |||
import org.sonar.db.permission.OrganizationPermission; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.es.SearchOptions; | |||
import org.sonar.server.es.SearchResult; | |||
import org.sonar.server.organization.DefaultOrganizationProvider; | |||
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.Common; | |||
import org.sonarqube.ws.Organizations.SearchMembersWsResponse; | |||
import org.sonarqube.ws.Organizations.User; | |||
import static com.google.common.base.Preconditions.checkArgument; | |||
import static org.sonar.core.util.Protobuf.setNullable; | |||
import static org.sonar.server.es.SearchOptions.MAX_LIMIT; | |||
import static org.sonar.server.ws.WsUtils.checkFoundWithOptional; | |||
import static org.sonar.server.ws.WsUtils.writeProtobuf; | |||
public class SearchMembersAction implements OrganizationsWsAction { | |||
private final DbClient dbClient; | |||
private final UserIndex userIndex; | |||
private final DefaultOrganizationProvider organizationProvider; | |||
private final UserSession userSession; | |||
public SearchMembersAction(DbClient dbClient, UserIndex userIndex, DefaultOrganizationProvider organizationProvider, UserSession userSession) { | |||
this.dbClient = dbClient; | |||
this.userIndex = userIndex; | |||
this.organizationProvider = organizationProvider; | |||
this.userSession = userSession; | |||
} | |||
@Override | |||
public void define(WebService.NewController context) { | |||
WebService.NewAction action = context.createAction("search_members") | |||
.setDescription("Search members of an organization") | |||
.setResponseExample(getClass().getResource("search_members-example.json")) | |||
.setSince("6.4") | |||
.setInternal(true) | |||
.setHandler(this); | |||
action.addSelectionModeParam(); | |||
action.addSearchQuery("freddy", "names", "logins"); | |||
action.addSearchQuery("orwe", "names", "logins"); | |||
action.addPagingParams(50, MAX_LIMIT); | |||
action.createParam(Param.SELECTED) | |||
.setDescription("Depending on the value, show only selected items (selected=selected) or deselected items (selected=deselected).") | |||
.setInternal(true) | |||
.setDefaultValue(SelectionMode.SELECTED.value()) | |||
.setPossibleValues(SelectionMode.SELECTED.value(), SelectionMode.DESELECTED.value()); | |||
action.createParam("organization") | |||
.setDescription("Organization key") | |||
.setInternal(true) | |||
@@ -47,6 +96,92 @@ public class SearchMembersAction implements OrganizationsWsAction { | |||
@Override | |||
public void handle(Request request, Response response) throws Exception { | |||
// TODO | |||
try (DbSession dbSession = dbClient.openSession(false)) { | |||
OrganizationDto organization = getOrganization(dbSession, request.param("organization")); | |||
List<String> memberLogins = dbClient.organizationMemberDao().selectLoginsByOrganizationUuid(dbSession, organization.getUuid()); | |||
UserQuery.Builder userQuery = buildUserQuery(request, memberLogins); | |||
SearchOptions searchOptions = buildSearchOptions(request); | |||
SearchResult<UserDoc> searchResults = userIndex.search(userQuery.build(), searchOptions); | |||
List<String> orderedLogins = searchResults.getDocs().stream().map(UserDoc::login).collect(Collectors.toList()); | |||
List<UserDto> users = dbClient.userDao().selectByLogins(dbSession, orderedLogins).stream() | |||
.sorted(Ordering.explicit(orderedLogins).onResultOf(UserDto::getLogin)) | |||
.collect(Collectors.toList()); | |||
Multiset<String> groupCountByLogin = null; | |||
if (userSession.hasPermission(OrganizationPermission.ADMINISTER, organization)) { | |||
groupCountByLogin = dbClient.groupMembershipDao().countGroupByLoginsAndOrganization(dbSession, orderedLogins, organization.getUuid()); | |||
} | |||
Common.Paging wsPaging = buildWsPaging(request, searchResults); | |||
SearchMembersWsResponse wsResponse = buildResponse(users, wsPaging, groupCountByLogin); | |||
writeProtobuf(wsResponse, request, response); | |||
} | |||
} | |||
private static SearchMembersWsResponse buildResponse(List<UserDto> users, Common.Paging wsPaging, @Nullable Multiset<String> groupCountByLogin) { | |||
SearchMembersWsResponse.Builder response = SearchMembersWsResponse.newBuilder(); | |||
User.Builder wsUser = User.newBuilder(); | |||
users.stream() | |||
.map(userDto -> { | |||
String login = userDto.getLogin(); | |||
wsUser | |||
.clear() | |||
.setLogin(login) | |||
.setName(userDto.getName()); | |||
setNullable(userDto.getEmail(), text -> wsUser.setAvatar(hash(text))); | |||
setNullable(groupCountByLogin, count -> wsUser.setGroupCount(groupCountByLogin.count(login))); | |||
return wsUser; | |||
}) | |||
.forEach(response::addUsers); | |||
response.setPaging(wsPaging); | |||
return response.build(); | |||
} | |||
private static UserQuery.Builder buildUserQuery(Request request, List<String> memberLogins) { | |||
UserQuery.Builder userQuery = UserQuery.builder(); | |||
String textQuery = request.param(Param.TEXT_QUERY); | |||
checkArgument(textQuery == null || textQuery.length() >= 2, "Query length must be greater than or equal to 2"); | |||
userQuery.setTextQuery(textQuery); | |||
SelectionMode selectionMode = SelectionMode.fromParam(request.mandatoryParam(Param.SELECTED)); | |||
if (SelectionMode.DESELECTED.equals(selectionMode)) { | |||
userQuery.setExcludedLogins(memberLogins); | |||
} else { | |||
userQuery.setLogins(memberLogins); | |||
} | |||
return userQuery; | |||
} | |||
private static SearchOptions buildSearchOptions(Request request) { | |||
int pageSize = request.mandatoryParamAsInt(Param.PAGE_SIZE); | |||
checkArgument(pageSize <= SearchOptions.MAX_LIMIT, "Page size must lower than or equal to %s", SearchOptions.MAX_LIMIT); | |||
return new SearchOptions().setPage(request.mandatoryParamAsInt(Param.PAGE), pageSize); | |||
} | |||
private static String hash(String text) { | |||
return Hashing.md5().hashString(text.toLowerCase(Locale.ENGLISH), StandardCharsets.UTF_8).toString(); | |||
} | |||
private static Common.Paging buildWsPaging(Request request, SearchResult<UserDoc> searchResults) { | |||
return Common.Paging.newBuilder() | |||
.setPageIndex(request.mandatoryParamAsInt(Param.PAGE)) | |||
.setPageSize(request.mandatoryParamAsInt(Param.PAGE_SIZE)) | |||
.setTotal((int) searchResults.getTotal()) | |||
.build(); | |||
} | |||
private OrganizationDto getOrganization(DbSession dbSession, @Nullable String organizationParam) { | |||
String organizationKey = Optional.ofNullable(organizationParam) | |||
.orElseGet(organizationProvider.get()::getKey); | |||
return checkFoundWithOptional( | |||
dbClient.organizationDao().selectByKey(dbSession, organizationKey), | |||
"No organization with key '%s'", organizationKey); | |||
} | |||
} |
@@ -24,8 +24,8 @@ import java.util.ArrayList; | |||
import java.util.Iterator; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.Optional; | |||
import javax.annotation.CheckForNull; | |||
import javax.annotation.Nullable; | |||
import org.apache.commons.lang.StringUtils; | |||
import org.elasticsearch.action.get.GetRequestBuilder; | |||
import org.elasticsearch.action.get.GetResponse; | |||
@@ -133,20 +133,24 @@ public class UserIndex { | |||
return EsUtils.scroll(esClient, response.getScrollId(), DOC_CONVERTER); | |||
} | |||
public SearchResult<UserDoc> search(@Nullable String searchText, SearchOptions options) { | |||
public SearchResult<UserDoc> search(UserQuery userQuery, SearchOptions options) { | |||
SearchRequestBuilder request = esClient.prepareSearch(UserIndexDefinition.INDEX_TYPE_USER) | |||
.setSize(options.getLimit()) | |||
.setFrom(options.getOffset()) | |||
.addSort(FIELD_NAME, SortOrder.ASC); | |||
BoolQueryBuilder filter = boolQuery() | |||
.must(termQuery(FIELD_ACTIVE, true)); | |||
BoolQueryBuilder filter = boolQuery().must(termQuery(FIELD_ACTIVE, true)); | |||
userQuery.getLogins().ifPresent( | |||
logins -> filter.must(termsQuery(FIELD_LOGIN, userQuery.getLogins().get()))); | |||
userQuery.getExcludedLogins().ifPresent( | |||
excludedLogins -> filter.mustNot(termsQuery(FIELD_LOGIN, userQuery.getExcludedLogins().get()))); | |||
QueryBuilder query; | |||
if (StringUtils.isEmpty(searchText)) { | |||
query = matchAllQuery(); | |||
} else { | |||
query = QueryBuilders.multiMatchQuery(searchText, | |||
QueryBuilder esQuery = matchAllQuery(); | |||
Optional<String> textQuery = userQuery.getTextQuery(); | |||
if (textQuery.isPresent()) { | |||
esQuery = QueryBuilders.multiMatchQuery(textQuery.get(), | |||
FIELD_LOGIN, | |||
USER_SEARCH_GRAMS_ANALYZER.subField(FIELD_LOGIN), | |||
FIELD_NAME, | |||
@@ -156,7 +160,7 @@ public class UserIndex { | |||
.operator(MatchQueryBuilder.Operator.AND); | |||
} | |||
request.setQuery(boolQuery().must(query).filter(filter)); | |||
request.setQuery(boolQuery().must(esQuery).filter(filter)); | |||
return new SearchResult<>(request.get(), DOC_CONVERTER); | |||
} |
@@ -0,0 +1,87 @@ | |||
/* | |||
* 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.sonar.server.user.index; | |||
import com.google.common.collect.ImmutableList; | |||
import java.util.List; | |||
import java.util.Optional; | |||
import javax.annotation.Nullable; | |||
import javax.annotation.concurrent.Immutable; | |||
import static org.apache.commons.lang.StringUtils.isBlank; | |||
@Immutable | |||
public class UserQuery { | |||
private final String textQuery; | |||
private final List<String> logins; | |||
private final List<String> excludedLogins; | |||
private UserQuery(Builder builder) { | |||
this.textQuery = builder.textQuery; | |||
this.logins = builder.logins == null ? null : ImmutableList.copyOf(builder.logins); | |||
this.excludedLogins = builder.excludedLogins == null ? null : ImmutableList.copyOf(builder.excludedLogins); | |||
} | |||
public Optional<String> getTextQuery() { | |||
return Optional.ofNullable(textQuery); | |||
} | |||
public Optional<List<String>> getLogins() { | |||
return Optional.ofNullable(logins); | |||
} | |||
public Optional<List<String>> getExcludedLogins() { | |||
return Optional.ofNullable(excludedLogins); | |||
} | |||
public static Builder builder() { | |||
return new Builder(); | |||
} | |||
public static class Builder { | |||
private String textQuery; | |||
private List<String> logins; | |||
private List<String> excludedLogins; | |||
private Builder() { | |||
// enforce factory method | |||
} | |||
public UserQuery build() { | |||
return new UserQuery(this); | |||
} | |||
public Builder setTextQuery(@Nullable String textQuery) { | |||
this.textQuery = isBlank(textQuery) ? null : textQuery; | |||
return this; | |||
} | |||
public Builder setLogins(@Nullable List<String> logins) { | |||
this.logins = logins; | |||
return this; | |||
} | |||
public Builder setExcludedLogins(@Nullable List<String> excludedLogins) { | |||
this.excludedLogins = excludedLogins; | |||
return this; | |||
} | |||
} | |||
} |
@@ -39,6 +39,7 @@ import org.sonar.server.es.SearchOptions; | |||
import org.sonar.server.es.SearchResult; | |||
import org.sonar.server.user.index.UserDoc; | |||
import org.sonar.server.user.index.UserIndex; | |||
import org.sonar.server.user.index.UserQuery; | |||
import static com.google.common.base.MoreObjects.firstNonNull; | |||
import static org.sonar.server.es.SearchOptions.MAX_LIMIT; | |||
@@ -78,7 +79,8 @@ public class SearchAction implements UsersWsAction { | |||
SearchOptions options = new SearchOptions() | |||
.setPage(request.mandatoryParamAsInt(Param.PAGE), request.mandatoryParamAsInt(Param.PAGE_SIZE)); | |||
List<String> fields = request.paramAsStrings(Param.FIELDS); | |||
SearchResult<UserDoc> result = userIndex.search(request.param(Param.TEXT_QUERY), options); | |||
String textQuery = request.param(Param.TEXT_QUERY); | |||
SearchResult<UserDoc> result = userIndex.search(UserQuery.builder().setTextQuery(textQuery).build(), options); | |||
try (DbSession dbSession = dbClient.openSession(false)) { | |||
List<String> logins = Lists.transform(result.getDocs(), UserDocToLogin.INSTANCE); |
@@ -1,2 +1,19 @@ | |||
{ | |||
"users": [ | |||
{ | |||
"login": "ada.lovelace", | |||
"name": "Ada Lovelace", | |||
"avatar": "680b0001b4952664631511a81a4edc59" | |||
}, | |||
{ | |||
"login": "grace.hopper", | |||
"name": "Grace Hopper", | |||
"avatar": "36d90b0b8cc8639e4960e46116dce02c" | |||
} | |||
], | |||
"paging": { | |||
"pageIndex": 1, | |||
"pageSize": 50, | |||
"total": 2 | |||
} | |||
} |
@@ -20,26 +20,324 @@ | |||
package org.sonar.server.organization.ws; | |||
import com.google.common.base.Throwables; | |||
import java.io.IOException; | |||
import java.util.stream.IntStream; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.junit.rules.ExpectedException; | |||
import org.sonar.api.config.MapSettings; | |||
import org.sonar.api.server.ws.WebService; | |||
import org.sonar.api.server.ws.WebService.Param; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.db.DbTester; | |||
import org.sonar.db.organization.OrganizationDto; | |||
import org.sonar.db.permission.OrganizationPermission; | |||
import org.sonar.db.user.GroupDto; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.es.EsTester; | |||
import org.sonar.server.exceptions.NotFoundException; | |||
import org.sonar.server.organization.DefaultOrganizationProvider; | |||
import org.sonar.server.organization.TestDefaultOrganizationProvider; | |||
import org.sonar.server.tester.UserSessionRule; | |||
import org.sonar.server.user.index.UserIndex; | |||
import org.sonar.server.user.index.UserIndexDefinition; | |||
import org.sonar.server.user.index.UserIndexer; | |||
import org.sonar.server.ws.TestRequest; | |||
import org.sonar.server.ws.WsActionTester; | |||
import org.sonarqube.ws.Common.Paging; | |||
import org.sonarqube.ws.MediaTypes; | |||
import org.sonarqube.ws.Organizations.SearchMembersWsResponse; | |||
import org.sonarqube.ws.Organizations.User; | |||
import org.sonarqube.ws.client.organization.SearchMembersWsRequest; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.assertj.core.api.Assertions.tuple; | |||
import static org.sonar.core.util.Protobuf.setNullable; | |||
import static org.sonar.test.JsonAssert.assertJson; | |||
public class SearchMembersActionTest { | |||
private WsActionTester ws = new WsActionTester(new SearchMembersAction()); | |||
@Rule | |||
public UserSessionRule userSession = UserSessionRule.standalone(); | |||
@Rule | |||
public ExpectedException expectedException = ExpectedException.none(); | |||
@Rule | |||
public EsTester es = new EsTester(new UserIndexDefinition(new MapSettings())); | |||
@Rule | |||
public DbTester db = DbTester.create(); | |||
private DbClient dbClient = db.getDbClient(); | |||
private DefaultOrganizationProvider organizationProvider = TestDefaultOrganizationProvider.from(db); | |||
private UserIndexer indexer = new UserIndexer(dbClient, es.client()); | |||
private WsActionTester ws = new WsActionTester(new SearchMembersAction(dbClient, new UserIndex(es.client()), organizationProvider, userSession)); | |||
private SearchMembersWsRequest request = new SearchMembersWsRequest(); | |||
@Test | |||
public void empty_response() { | |||
SearchMembersWsResponse result = call(); | |||
assertThat(result.getUsersList()).isEmpty(); | |||
assertThat(result.getPaging()) | |||
.extracting(Paging::getPageIndex, Paging::getPageSize, Paging::getTotal) | |||
.containsExactly(1, 50, 0); | |||
} | |||
@Test | |||
public void search_members_of_default_organization() { | |||
OrganizationDto defaultOrganization = db.getDefaultOrganization(); | |||
OrganizationDto anotherOrganization = db.organizations().insert(); | |||
UserDto user = insertUser(); | |||
UserDto anotherUser = insertUser(); | |||
UserDto userInAnotherOrganization = insertUser(); | |||
db.organizations().addMember(defaultOrganization, user); | |||
db.organizations().addMember(defaultOrganization, anotherUser); | |||
db.organizations().addMember(anotherOrganization, userInAnotherOrganization); | |||
SearchMembersWsResponse result = call(); | |||
assertThat(result.getUsersList()) | |||
.extracting(User::getLogin, User::getName) | |||
.containsOnly( | |||
tuple(user.getLogin(), user.getName()), | |||
tuple(anotherUser.getLogin(), anotherUser.getName())); | |||
} | |||
@Test | |||
public void search_members_of_specified_organization() { | |||
OrganizationDto organization = db.organizations().insert(); | |||
OrganizationDto anotherOrganization = db.organizations().insert(); | |||
UserDto user = insertUser(); | |||
UserDto anotherUser = insertUser(); | |||
UserDto userInAnotherOrganization = insertUser(); | |||
db.organizations().addMember(organization, user); | |||
db.organizations().addMember(organization, anotherUser); | |||
db.organizations().addMember(anotherOrganization, userInAnotherOrganization); | |||
request.setOrganization(organization.getKey()); | |||
SearchMembersWsResponse result = call(); | |||
assertThat(result.getUsersList()) | |||
.extracting(User::getLogin, User::getName) | |||
.containsOnly( | |||
tuple(user.getLogin(), user.getName()), | |||
tuple(anotherUser.getLogin(), anotherUser.getName())); | |||
} | |||
@Test | |||
public void return_avatar() { | |||
UserDto user = db.users().insertUser(u -> u.setEmail("email@domain.com")); | |||
indexer.index(user.getLogin()); | |||
db.organizations().addMember(db.getDefaultOrganization(), user); | |||
SearchMembersWsResponse result = call(); | |||
assertThat(result.getUsers(0).getAvatar()).isEqualTo("7328fddefd53de471baeb6e2b764f78a"); | |||
} | |||
@Test | |||
public void do_not_return_group_count_if_no_admin_permission() { | |||
UserDto user = insertUser(); | |||
GroupDto group = db.users().insertGroup(); | |||
db.users().insertMember(group, user); | |||
db.organizations().addMember(db.getDefaultOrganization(), user); | |||
SearchMembersWsResponse result = call(); | |||
assertThat(result.getUsers(0).hasGroupCount()).isFalse(); | |||
} | |||
@Test | |||
public void return_group_counts_if_org_admin() { | |||
userSession.addPermission(OrganizationPermission.ADMINISTER, db.getDefaultOrganization()); | |||
UserDto user = insertUser(); | |||
UserDto anotherUser = insertUser(); | |||
IntStream.range(0, 10) | |||
.mapToObj(i -> db.users().insertGroup()) | |||
.forEach(g -> db.users().insertMembers(g, user)); | |||
OrganizationDto anotherOrganization = db.organizations().insert(); | |||
GroupDto anotherGroup = db.users().insertGroup(anotherOrganization); | |||
db.users().insertMember(anotherGroup, user); | |||
db.organizations().addMember(db.getDefaultOrganization(), user); | |||
db.organizations().addMember(db.getDefaultOrganization(), anotherUser); | |||
db.organizations().addMember(anotherOrganization, user); | |||
SearchMembersWsResponse result = call(); | |||
assertThat(result.getUsersList()).extracting(User::getLogin, User::getGroupCount).containsOnly( | |||
tuple(user.getLogin(), 10), | |||
tuple(anotherUser.getLogin(), 0)); | |||
} | |||
@Test | |||
public void search_non_members() { | |||
OrganizationDto defaultOrganization = db.getDefaultOrganization(); | |||
OrganizationDto anotherOrganization = db.organizations().insert(); | |||
UserDto user = insertUser(); | |||
UserDto anotherUser = insertUser(); | |||
UserDto userInAnotherOrganization = insertUser(); | |||
db.organizations().addMember(anotherOrganization, user); | |||
db.organizations().addMember(anotherOrganization, anotherUser); | |||
db.organizations().addMember(defaultOrganization, userInAnotherOrganization); | |||
request.setSelected(WebService.SelectionMode.DESELECTED.value()); | |||
SearchMembersWsResponse result = call(); | |||
assertThat(result.getUsersList()) | |||
.extracting(User::getLogin, User::getName) | |||
.containsOnly( | |||
tuple(user.getLogin(), user.getName()), | |||
tuple(anotherUser.getLogin(), anotherUser.getName())); | |||
} | |||
@Test | |||
public void search_members_pagination() { | |||
IntStream.range(0, 10).forEach(i -> { | |||
UserDto userDto = db.users().insertUser(user -> user.setName("USER_" + i)); | |||
db.organizations().addMember(db.getDefaultOrganization(), userDto); | |||
indexer.index(userDto.getLogin()); | |||
}); | |||
request.setPage(2).setPageSize(3); | |||
SearchMembersWsResponse result = call(); | |||
assertThat(result.getUsersList()).extracting(User::getName) | |||
.containsExactly("USER_3", "USER_4", "USER_5"); | |||
assertThat(result.getPaging()) | |||
.extracting(Paging::getPageIndex, Paging::getPageSize, Paging::getTotal) | |||
.containsExactly(2, 3, 10); | |||
} | |||
@Test | |||
public void search_members_by_name() { | |||
IntStream.range(0, 10).forEach(i -> { | |||
UserDto userDto = db.users().insertUser(user -> user.setName("USER_" + i)); | |||
db.organizations().addMember(db.getDefaultOrganization(), userDto); | |||
indexer.index(userDto.getLogin()); | |||
}); | |||
request.setQuery("_9"); | |||
SearchMembersWsResponse result = call(); | |||
assertThat(result.getUsersList()).extracting(User::getName).containsExactly("USER_9"); | |||
} | |||
@Test | |||
public void search_members_by_login() { | |||
IntStream.range(0, 10).forEach(i -> { | |||
UserDto userDto = db.users().insertUser(user -> user.setLogin("USER_" + i)); | |||
db.organizations().addMember(db.getDefaultOrganization(), userDto); | |||
indexer.index(userDto.getLogin()); | |||
}); | |||
request.setQuery("_9"); | |||
SearchMembersWsResponse result = call(); | |||
assertThat(result.getUsersList()).extracting(User::getLogin).containsExactly("USER_9"); | |||
} | |||
@Test | |||
public void search_members_by_email() { | |||
IntStream.range(0, 10).forEach(i -> { | |||
UserDto userDto = db.users().insertUser(user -> user | |||
.setLogin("L" + i) | |||
.setEmail("USER_" + i + "@email.com")); | |||
db.organizations().addMember(db.getDefaultOrganization(), userDto); | |||
indexer.index(userDto.getLogin()); | |||
}); | |||
request.setQuery("_9"); | |||
SearchMembersWsResponse result = call(); | |||
assertThat(result.getUsersList()).extracting(User::getLogin).containsExactly("L9"); | |||
} | |||
@Test | |||
public void json_example() { | |||
UserDto ada = db.users().insertUser(u -> u.setLogin("ada.lovelace").setName("Ada Lovelace").setEmail("ada@lovelace.com")); | |||
indexer.index(ada.getLogin()); | |||
UserDto grace = db.users().insertUser(u -> u.setLogin("grace.hopper").setName("Grace Hopper").setEmail("grace@hopper.com")); | |||
indexer.index(grace.getLogin()); | |||
db.organizations().addMember(db.getDefaultOrganization(), ada); | |||
db.organizations().addMember(db.getDefaultOrganization(), grace); | |||
String result = ws.newRequest().execute().getInput(); | |||
assertJson(result).isSimilarTo(ws.getDef().responseExampleAsString()); | |||
} | |||
@Test | |||
public void fail_if_organization_is_unknown() { | |||
request.setOrganization("ORGA 42"); | |||
expectedException.expect(NotFoundException.class); | |||
expectedException.expectMessage("No organization with key 'ORGA 42'"); | |||
call(); | |||
} | |||
@Test | |||
public void fail_if_page_size_greater_than_500() { | |||
request.setPageSize(501); | |||
expectedException.expect(IllegalArgumentException.class); | |||
expectedException.expectMessage("Page size must lower than or equal to 500"); | |||
call(); | |||
} | |||
@Test | |||
public void fail_if_query_length_lower_than_2() { | |||
request.setQuery("a"); | |||
expectedException.expect(IllegalArgumentException.class); | |||
expectedException.expectMessage("Query length must be greater than or equal to 2"); | |||
call(); | |||
} | |||
@Test | |||
public void definition() { | |||
WebService.Action action = ws.getDef(); | |||
assertThat(action.key()).isEqualTo("search_members"); | |||
assertThat(action.params()).extracting(WebService.Param::key) | |||
.containsOnly("q", "selected", "p", "ps", "organization"); | |||
assertThat(action.params()).extracting(Param::key).containsOnly("q", "selected", "p", "ps", "organization"); | |||
assertThat(action.description()).isNotEmpty(); | |||
assertThat(action.responseExampleAsString()).isNotEmpty(); | |||
assertThat(action.since()).isEqualTo("6.4"); | |||
assertThat(action.isInternal()).isTrue(); | |||
assertThat(action.isPost()).isFalse(); | |||
assertThat(action.param("organization").isInternal()).isTrue(); | |||
Param selected = action.param("selected"); | |||
assertThat(selected.possibleValues()).containsOnly("selected", "deselected"); | |||
assertThat(selected.isInternal()).isTrue(); | |||
assertThat(selected.defaultValue()).isEqualTo("selected"); | |||
} | |||
private UserDto insertUser() { | |||
UserDto userDto = db.users().insertUser(); | |||
indexer.index(userDto.getLogin()); | |||
return userDto; | |||
} | |||
private SearchMembersWsResponse call() { | |||
TestRequest wsRequest = ws.newRequest() | |||
.setMediaType(MediaTypes.PROTOBUF); | |||
setNullable(request.getOrganization(), o -> wsRequest.setParam("organization", o)); | |||
setNullable(request.getQuery(), q -> wsRequest.setParam(Param.TEXT_QUERY, q)); | |||
setNullable(request.getPage(), p -> wsRequest.setParam(Param.PAGE, String.valueOf(p))); | |||
setNullable(request.getPageSize(), ps -> wsRequest.setParam(Param.PAGE_SIZE, String.valueOf(ps))); | |||
setNullable(request.getSelected(), s -> wsRequest.setParam(Param.SELECTED, s)); | |||
try { | |||
return SearchMembersWsResponse.parseFrom(wsRequest.execute().getInputStream()); | |||
} catch (IOException e) { | |||
throw Throwables.propagate(e); | |||
} | |||
} | |||
} |
@@ -19,7 +19,6 @@ | |||
*/ | |||
package org.sonar.server.user.index; | |||
import java.util.Arrays; | |||
import java.util.Collections; | |||
import java.util.List; | |||
import java.util.Locale; | |||
@@ -29,8 +28,11 @@ import org.junit.Test; | |||
import org.sonar.api.config.MapSettings; | |||
import org.sonar.server.es.EsTester; | |||
import org.sonar.server.es.SearchOptions; | |||
import org.sonar.server.es.SearchResult; | |||
import static java.util.Arrays.asList; | |||
import static java.util.Collections.emptyList; | |||
import static java.util.Collections.singletonList; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.sonar.server.user.index.UserIndexDefinition.INDEX_TYPE_USER; | |||
@@ -46,6 +48,8 @@ public class UserIndexTest { | |||
private UserIndex underTest; | |||
private UserQuery.Builder userQuery = UserQuery.builder(); | |||
@Before | |||
public void setUp() { | |||
underTest = new UserIndex(esTester.client()); | |||
@@ -127,16 +131,49 @@ public class UserIndexTest { | |||
@Test | |||
public void searchUsers() throws Exception { | |||
esTester.putDocuments(INDEX_TYPE_USER.getIndex(), INDEX_TYPE_USER.getType(), newUser(USER1_LOGIN, Arrays.asList("user_1", "u1")).setEmail("email1")); | |||
esTester.putDocuments(INDEX_TYPE_USER.getIndex(), INDEX_TYPE_USER.getType(), newUser(USER1_LOGIN, asList("user_1", "u1")).setEmail("email1")); | |||
esTester.putDocuments(INDEX_TYPE_USER.getIndex(), INDEX_TYPE_USER.getType(), newUser(USER2_LOGIN, Collections.<String>emptyList()).setEmail("email2")); | |||
assertThat(underTest.search(null, new SearchOptions()).getDocs()).hasSize(2); | |||
assertThat(underTest.search("user", new SearchOptions()).getDocs()).hasSize(2); | |||
assertThat(underTest.search("ser", new SearchOptions()).getDocs()).hasSize(2); | |||
assertThat(underTest.search(USER1_LOGIN, new SearchOptions()).getDocs()).hasSize(1); | |||
assertThat(underTest.search(USER2_LOGIN, new SearchOptions()).getDocs()).hasSize(1); | |||
assertThat(underTest.search("mail", new SearchOptions()).getDocs()).hasSize(2); | |||
assertThat(underTest.search("EMAIL1", new SearchOptions()).getDocs()).hasSize(1); | |||
assertThat(underTest.search(userQuery.build(), new SearchOptions()).getDocs()).hasSize(2); | |||
assertThat(underTest.search(userQuery.setTextQuery("user").build(), new SearchOptions()).getDocs()).hasSize(2); | |||
assertThat(underTest.search(userQuery.setTextQuery("ser").build(), new SearchOptions()).getDocs()).hasSize(2); | |||
assertThat(underTest.search(userQuery.setTextQuery(USER1_LOGIN).build(), new SearchOptions()).getDocs()).hasSize(1); | |||
assertThat(underTest.search(userQuery.setTextQuery(USER2_LOGIN).build(), new SearchOptions()).getDocs()).hasSize(1); | |||
assertThat(underTest.search(userQuery.setTextQuery("mail").build(), new SearchOptions()).getDocs()).hasSize(2); | |||
assertThat(underTest.search(userQuery.setTextQuery("EMAIL1").build(), new SearchOptions()).getDocs()).hasSize(1); | |||
} | |||
@Test | |||
public void search_users_filter_logins() throws Exception { | |||
esTester.putDocuments(INDEX_TYPE_USER.getIndex(), INDEX_TYPE_USER.getType(), newUser(USER1_LOGIN, asList("user_1", "u1")).setEmail("email1")); | |||
esTester.putDocuments(INDEX_TYPE_USER.getIndex(), INDEX_TYPE_USER.getType(), newUser(USER2_LOGIN, emptyList()).setEmail("email2")); | |||
esTester.putDocuments(INDEX_TYPE_USER.getIndex(), INDEX_TYPE_USER.getType(), newUser("user3", emptyList()).setEmail("email2")); | |||
SearchResult<UserDoc> result = underTest.search(userQuery.setLogins(asList(USER1_LOGIN, USER2_LOGIN)).build(), new SearchOptions()); | |||
assertThat(result.getDocs()).hasSize(2) | |||
.extracting(UserDoc::login) | |||
.containsOnly(USER1_LOGIN, USER2_LOGIN); | |||
} | |||
@Test | |||
public void search_users_filter_logins_that_match_exactly() { | |||
esTester.putDocuments(INDEX_TYPE_USER.getIndex(), INDEX_TYPE_USER.getType(), newUser("USER_1", asList("user_1", "u1")).setEmail("email1")); | |||
esTester.putDocuments(INDEX_TYPE_USER.getIndex(), INDEX_TYPE_USER.getType(), newUser("UsEr_1", emptyList()).setEmail("email2")); | |||
assertThat(underTest.search(userQuery.setLogins(singletonList("USER_1")).build(), new SearchOptions()).getDocs()).hasSize(1); | |||
assertThat(underTest.search(userQuery.setLogins(singletonList("USER_")).build(), new SearchOptions()).getDocs()).isEmpty(); | |||
assertThat(underTest.search(userQuery.setLogins(emptyList()).build(), new SearchOptions()).getDocs()).isEmpty(); | |||
} | |||
@Test | |||
public void search_users_exclude_logins() throws Exception { | |||
esTester.putDocuments(INDEX_TYPE_USER.getIndex(), INDEX_TYPE_USER.getType(), newUser(USER1_LOGIN, asList("user_1", "u1")).setEmail("email1")); | |||
esTester.putDocuments(INDEX_TYPE_USER.getIndex(), INDEX_TYPE_USER.getType(), newUser(USER2_LOGIN, emptyList()).setEmail("email2")); | |||
esTester.putDocuments(INDEX_TYPE_USER.getIndex(), INDEX_TYPE_USER.getType(), newUser("user3", emptyList()).setEmail("email2")); | |||
assertThat(underTest.search(userQuery.setExcludedLogins(emptyList()).build(), new SearchOptions()).getDocs()).hasSize(3); | |||
assertThat(underTest.search(userQuery.setExcludedLogins(asList(USER1_LOGIN, USER2_LOGIN, "user3")).build(), new SearchOptions()).getDocs()).isEmpty(); | |||
assertThat(underTest.search(userQuery.setExcludedLogins(asList(USER1_LOGIN, USER2_LOGIN)).build(), new SearchOptions()).getDocs()).hasSize(1); | |||
} | |||
private static UserDoc newUser(String login, List<String> scmAccounts) { |
@@ -0,0 +1,82 @@ | |||
/* | |||
* 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.organization; | |||
import javax.annotation.CheckForNull; | |||
import javax.annotation.Nullable; | |||
public class SearchMembersWsRequest { | |||
private String organization; | |||
private String selected; | |||
private String query; | |||
private Integer page; | |||
private Integer pageSize; | |||
@CheckForNull | |||
public String getOrganization() { | |||
return organization; | |||
} | |||
public SearchMembersWsRequest setOrganization(@Nullable String organization) { | |||
this.organization = organization; | |||
return this; | |||
} | |||
@CheckForNull | |||
public String getSelected() { | |||
return selected; | |||
} | |||
public SearchMembersWsRequest setSelected(@Nullable String selected) { | |||
this.selected = selected; | |||
return this; | |||
} | |||
@CheckForNull | |||
public String getQuery() { | |||
return query; | |||
} | |||
public SearchMembersWsRequest setQuery(@Nullable String query) { | |||
this.query = query; | |||
return this; | |||
} | |||
@CheckForNull | |||
public Integer getPage() { | |||
return page; | |||
} | |||
public SearchMembersWsRequest setPage(@Nullable Integer page) { | |||
this.page = page; | |||
return this; | |||
} | |||
@CheckForNull | |||
public Integer getPageSize() { | |||
return pageSize; | |||
} | |||
public SearchMembersWsRequest setPageSize(@Nullable Integer pageSize) { | |||
this.pageSize = pageSize; | |||
return this; | |||
} | |||
} |
@@ -20,6 +20,8 @@ syntax = "proto2"; | |||
package sonarqube.ws.organizations; | |||
import "ws-commons.proto"; | |||
option java_package = "org.sonarqube.ws"; | |||
option java_outer_classname = "Organizations"; | |||
option optimize_for = SPEED; | |||
@@ -39,6 +41,12 @@ message SearchWsResponse { | |||
repeated Organization organizations = 1; | |||
} | |||
// WS api/organizations/search_members | |||
message SearchMembersWsResponse { | |||
optional sonarqube.ws.commons.Paging paging = 1; | |||
repeated User users = 2; | |||
} | |||
message Organization { | |||
optional string key = 1; | |||
optional string name = 2; | |||
@@ -47,3 +55,10 @@ message Organization { | |||
optional string avatar = 5; | |||
optional bool guarded = 6; | |||
} | |||
message User { | |||
optional string login = 1; | |||
optional string name = 2; | |||
optional string avatar = 3; | |||
optional int32 groupCount = 4; | |||
} |