From 478815189f2e78920ed22b5852259d347418246d Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lievremont Date: Thu, 21 May 2015 11:44:12 +0200 Subject: [PATCH] SONAR-6465 Add groupsCount on users search --- .../org/sonar/server/user/db/UserDao.java | 6 +- .../sonar/server/user/ws/SearchAction.java | 48 +++++++-- .../server/user/ws/SearchActionTest.java | 97 ++++++++++++++++--- .../org/sonar/server/user/ws/UsersWsTest.java | 3 +- .../user/ws/SearchActionTest/five_users.json | 15 ++- .../ws/SearchActionTest/user_with_groups.json | 16 +++ .../sonar/core/user/GroupMembershipDao.java | 18 ++++ .../core/user/GroupMembershipMapper.java | 2 + .../org/sonar/core/user/UserGroupCount.java | 34 +++++++ .../sonar/core/user/GroupMembershipMapper.xml | 13 +++ .../core/user/GroupMembershipDaoTest.java | 20 ++++ .../user/GroupMembershipDaoTest/shared.xml | 4 + 12 files changed, 245 insertions(+), 31 deletions(-) create mode 100644 server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/user_with_groups.json create mode 100644 sonar-core/src/main/java/org/sonar/core/user/UserGroupCount.java diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/db/UserDao.java b/server/sonar-server/src/main/java/org/sonar/server/user/db/UserDao.java index 3b813e0ddd0..27ceda98e93 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/user/db/UserDao.java +++ b/server/sonar-server/src/main/java/org/sonar/server/user/db/UserDao.java @@ -20,6 +20,8 @@ package org.sonar.server.user.db; +import java.util.List; +import javax.annotation.CheckForNull; import org.sonar.api.utils.System2; import org.sonar.core.persistence.DaoComponent; import org.sonar.core.persistence.DbSession; @@ -28,10 +30,6 @@ import org.sonar.core.user.UserDto; import org.sonar.core.user.UserMapper; import org.sonar.server.exceptions.NotFoundException; -import javax.annotation.CheckForNull; - -import java.util.List; - public class UserDao extends org.sonar.core.user.UserDao implements DaoComponent { public UserDao(MyBatis mybatis, System2 system2) { 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 7f4cf2439ce..0e6e76c6208 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 @@ -20,34 +20,43 @@ package org.sonar.server.user.ws; +import com.google.common.base.Function; +import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Maps; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.annotation.Nonnull; +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.utils.text.JsonWriter; +import org.sonar.core.persistence.DbSession; +import org.sonar.server.db.DbClient; 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 javax.annotation.Nullable; - -import java.util.List; -import java.util.Set; - public class SearchAction implements UsersWsAction { private static final String FIELD_LOGIN = "login"; private static final String FIELD_NAME = "name"; private static final String FIELD_EMAIL = "email"; private static final String FIELD_SCM_ACCOUNTS = "scmAccounts"; - private static final Set FIELDS = ImmutableSet.of(FIELD_LOGIN, FIELD_NAME, FIELD_EMAIL, FIELD_SCM_ACCOUNTS); + private static final String FIELD_GROUPS_COUNT = "groupsCount"; + private static final Set FIELDS = ImmutableSet.of(FIELD_LOGIN, FIELD_NAME, FIELD_EMAIL, FIELD_SCM_ACCOUNTS, FIELD_GROUPS_COUNT); private final UserIndex userIndex; + private final DbClient dbClient; - public SearchAction(UserIndex userIndex) { + public SearchAction(UserIndex userIndex, DbClient dbClient) { this.userIndex = userIndex; + this.dbClient = dbClient; } @Override @@ -72,13 +81,27 @@ public class SearchAction implements UsersWsAction { List fields = request.paramAsStrings(Param.FIELDS); SearchResult result = userIndex.search(request.param(Param.TEXT_QUERY), options); + Map groupsByLogin = Maps.newHashMap(); + DbSession session = dbClient.openSession(false); + try { + Collection logins = Collections2.transform(result.getDocs(), new Function() { + @Override + public String apply(@Nonnull UserDoc input) { + return input.login(); + } + }); + groupsByLogin = dbClient.groupMembershipDao().countGroupsByLogins(session, logins); + } finally { + session.close(); + } + JsonWriter json = response.newJsonWriter().beginObject(); options.writeJson(json, result.getTotal()); - writeUsers(json, result, fields); + writeUsers(json, result, fields, groupsByLogin); json.endObject().close(); } - private void writeUsers(JsonWriter json, SearchResult result, @Nullable List fields) { + private void writeUsers(JsonWriter json, SearchResult result, @Nullable List fields, Map groupsByLogin) { json.name("users").beginArray(); for (UserDoc user : result.getDocs()) { @@ -86,6 +109,7 @@ public class SearchAction implements UsersWsAction { writeIfNeeded(json, user.login(), FIELD_LOGIN, fields); writeIfNeeded(json, user.name(), FIELD_NAME, fields); writeIfNeeded(json, user.email(), FIELD_EMAIL, fields); + writeIfNeeded(json, groupsByLogin.get(user.login()), FIELD_GROUPS_COUNT, fields); if (fieldIsWanted(FIELD_SCM_ACCOUNTS, fields)) { json.name(FIELD_SCM_ACCOUNTS) .beginArray() @@ -103,6 +127,12 @@ public class SearchAction implements UsersWsAction { } } + private void writeIfNeeded(JsonWriter json, Integer value, String field, @Nullable List fields) { + if (fieldIsWanted(field, fields)) { + json.prop(field, value); + } + } + private boolean fieldIsWanted(String field, @Nullable List fields) { return fields == null || fields.isEmpty() || fields.contains(field); } 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 ab3c60cb8f2..a2456afbac0 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 @@ -20,23 +20,39 @@ package org.sonar.server.user.ws; +import com.google.common.collect.Lists; +import java.util.Arrays; +import java.util.List; +import org.junit.After; import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; import org.sonar.api.config.Settings; import org.sonar.api.server.ws.WebService; +import org.sonar.api.utils.System2; +import org.sonar.core.persistence.DbSession; +import org.sonar.core.persistence.DbTester; +import org.sonar.core.user.GroupDto; +import org.sonar.core.user.GroupMembershipDao; +import org.sonar.core.user.UserDto; +import org.sonar.core.user.UserGroupDto; +import org.sonar.server.db.DbClient; import org.sonar.server.es.EsTester; +import org.sonar.server.user.db.GroupDao; +import org.sonar.server.user.db.UserDao; +import org.sonar.server.user.db.UserGroupDao; import org.sonar.server.user.index.UserDoc; import org.sonar.server.user.index.UserIndex; import org.sonar.server.user.index.UserIndexDefinition; import org.sonar.server.ws.WsTester; -import java.util.Arrays; - import static org.assertj.core.api.Assertions.assertThat; public class SearchActionTest { + @ClassRule + public static final DbTester dbTester = new DbTester(); + @ClassRule public static final EsTester esTester = new EsTester().addDefinitions(new UserIndexDefinition(new Settings())); @@ -46,14 +62,30 @@ public class SearchActionTest { UserIndex index; + DbClient dbClient; + + DbSession session; + @Before public void setUp() { + dbTester.truncateTables(); esTester.truncateIndices(); + dbClient = new DbClient(dbTester.database(), dbTester.myBatis(), + new GroupMembershipDao(dbTester.myBatis()), + new UserDao(dbTester.myBatis(), new System2()), + new GroupDao(new System2()), + new UserGroupDao()); + session = dbClient.openSession(false); + index = new UserIndex(esTester.client()); - tester = new WsTester(new UsersWs(new SearchAction(index))); + tester = new WsTester(new UsersWs(new SearchAction(index, dbClient))); controller = tester.controller("api/users"); + } + @After + public void tearDown() { + session.close(); } @Test @@ -91,40 +123,81 @@ public class SearchActionTest { .contains("login") .contains("name") .contains("email") - .contains("scmAccounts"); + .contains("scmAccounts") + .contains("groupsCount"); assertThat(tester.newGetRequest("api/users", "search").setParam("f", "").execute().outputAsString()) .contains("login") .contains("name") .contains("email") - .contains("scmAccounts"); + .contains("scmAccounts") + .contains("groupsCount"); assertThat(tester.newGetRequest("api/users", "search").setParam("f", "login").execute().outputAsString()) .contains("login") .doesNotContain("name") .doesNotContain("email") - .doesNotContain("scmAccounts"); + .doesNotContain("scmAccounts") + .doesNotContain("groupsCount"); assertThat(tester.newGetRequest("api/users", "search").setParam("f", "scmAccounts").execute().outputAsString()) .doesNotContain("login") .doesNotContain("name") .doesNotContain("email") - .contains("scmAccounts"); + .contains("scmAccounts") + .doesNotContain("groupsCount"); + + assertThat(tester.newGetRequest("api/users", "search").setParam("f", "groupsCount").execute().outputAsString()) + .doesNotContain("login") + .doesNotContain("name") + .doesNotContain("email") + .doesNotContain("scmAccounts") + .contains("groupsCount"); } - private void injectUsers(int numberOfUsers) throws Exception { + @Test + public void search_with_groups() throws Exception { + List users = injectUsers(1); + + GroupDto group1 = dbClient.groupDao().insert(session, new GroupDto().setName("sonar-users")); + GroupDto group2 = dbClient.groupDao().insert(session, new GroupDto().setName("sonar-admins")); + dbClient.userGroupDao().insert(session, new UserGroupDto().setGroupId(group1.getId()).setUserId(users.get(0).getId())); + dbClient.userGroupDao().insert(session, new UserGroupDto().setGroupId(group2.getId()).setUserId(users.get(0).getId())); + session.commit(); + + tester.newGetRequest("api/users", "search").execute().assertJson(getClass(), "user_with_groups.json"); + } + + private List injectUsers(int numberOfUsers) throws Exception { + List userDtos = Lists.newArrayList(); long createdAt = System.currentTimeMillis(); UserDoc[] users = new UserDoc[numberOfUsers]; for (int index = 0; index < numberOfUsers; index++) { + String email = String.format("user-%d@mail.com", index); + String login = String.format("user-%d", index); + String name = String.format("User %d", index); + List scmAccounts = Arrays.asList(String.format("user-%d", index)); + + userDtos.add(dbClient.userDao().insert(session, new UserDto() + .setActive(true) + .setCreatedAt(createdAt) + .setEmail(email) + .setLogin(login) + .setName(name) + .setScmAccounts(scmAccounts) + .setUpdatedAt(createdAt))); + users[index] = new UserDoc() .setActive(true) .setCreatedAt(createdAt) - .setEmail(String.format("user-%d@mail.com", index)) - .setLogin(String.format("user-%d", index)) - .setName(String.format("User %d", index)) - .setScmAccounts(Arrays.asList(String.format("user-%d", index))) + .setEmail(email) + .setLogin(login) + .setName(name) + .setScmAccounts(scmAccounts) .setUpdatedAt(createdAt); } + session.commit(); esTester.putDocuments(UserIndexDefinition.INDEX, UserIndexDefinition.TYPE_USER, users); + return userDtos; } } 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 ccbb54a3830..8a2a5f43c46 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 @@ -25,6 +25,7 @@ import org.junit.Rule; import org.junit.Test; import org.sonar.api.i18n.I18n; import org.sonar.api.server.ws.WebService; +import org.sonar.server.db.DbClient; import org.sonar.server.tester.UserSessionRule; import org.sonar.server.user.UserUpdater; import org.sonar.server.user.index.UserIndex; @@ -46,7 +47,7 @@ public class UsersWsTest { new CurrentAction(userSessionRule), new DeactivateAction(mock(UserIndex.class), mock(UserUpdater.class), userSessionRule), new ChangePasswordAction(mock(UserUpdater.class), userSessionRule), - new SearchAction(mock(UserIndex.class)))); + new SearchAction(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/five_users.json b/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/five_users.json index 88a6fec9ecb..d568e8b0b7e 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 @@ -9,7 +9,8 @@ "email": "user-0@mail.com", "scmAccounts": [ "user-0" - ] + ], + "groupsCount": 0 }, { "login": "user-1", @@ -17,7 +18,8 @@ "email": "user-1@mail.com", "scmAccounts": [ "user-1" - ] + ], + "groupsCount": 0 }, { "login": "user-2", @@ -25,7 +27,8 @@ "email": "user-2@mail.com", "scmAccounts": [ "user-2" - ] + ], + "groupsCount": 0 }, { "login": "user-3", @@ -33,7 +36,8 @@ "email": "user-3@mail.com", "scmAccounts": [ "user-3" - ] + ], + "groupsCount": 0 }, { "login": "user-4", @@ -41,7 +45,8 @@ "email": "user-4@mail.com", "scmAccounts": [ "user-4" - ] + ], + "groupsCount": 0 } ] } 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 new file mode 100644 index 00000000000..a49e8252732 --- /dev/null +++ b/server/sonar-server/src/test/resources/org/sonar/server/user/ws/SearchActionTest/user_with_groups.json @@ -0,0 +1,16 @@ +{ + "p": 1, + "ps": 50, + "total": 1, + "users": [ + { + "login": "user-0", + "name": "User 0", + "email": "user-0@mail.com", + "scmAccounts": [ + "user-0" + ], + "groupsCount": 2 + } + ] +} diff --git a/sonar-core/src/main/java/org/sonar/core/user/GroupMembershipDao.java b/sonar-core/src/main/java/org/sonar/core/user/GroupMembershipDao.java index 8dcba5a7839..31ff32405c2 100644 --- a/sonar-core/src/main/java/org/sonar/core/user/GroupMembershipDao.java +++ b/sonar-core/src/main/java/org/sonar/core/user/GroupMembershipDao.java @@ -34,6 +34,7 @@ import org.sonar.core.persistence.DaoComponent; import org.sonar.core.persistence.DaoUtils; import org.sonar.core.persistence.DbSession; import org.sonar.core.persistence.MyBatis; +import org.sonar.core.util.NonNullInputFunction; public class GroupMembershipDao implements DaoComponent { @@ -75,6 +76,23 @@ public class GroupMembershipDao implements DaoComponent { return userCounts; } }); + + return result; + } + + public Map countGroupsByLogins(final DbSession session, Collection logins) { + final Map result = Maps.newHashMap(); + DaoUtils.executeLargeInputs(logins, new NonNullInputFunction, List>() { + @Override + protected List doApply(List input) { + List groupCounts = mapper(session).countGroupsByLogins(input); + for (UserGroupCount count : groupCounts) { + result.put(count.login(), count.groupCount()); + } + return groupCounts; + } + }); + return result; } diff --git a/sonar-core/src/main/java/org/sonar/core/user/GroupMembershipMapper.java b/sonar-core/src/main/java/org/sonar/core/user/GroupMembershipMapper.java index 4c848759cf2..b4a66a5ba9a 100644 --- a/sonar-core/src/main/java/org/sonar/core/user/GroupMembershipMapper.java +++ b/sonar-core/src/main/java/org/sonar/core/user/GroupMembershipMapper.java @@ -33,4 +33,6 @@ public interface GroupMembershipMapper { int countGroups(Map params); List countUsersByGroup(@Param("groupIds") List groupIds); + + List countGroupsByLogins(@Param("logins") List logins); } diff --git a/sonar-core/src/main/java/org/sonar/core/user/UserGroupCount.java b/sonar-core/src/main/java/org/sonar/core/user/UserGroupCount.java new file mode 100644 index 00000000000..712c1b435f1 --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/user/UserGroupCount.java @@ -0,0 +1,34 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.core.user; + +public class UserGroupCount { + + private String login; + private int groupCount; + + public String login() { + return login; + } + + public int groupCount() { + return groupCount; + } +} \ No newline at end of file diff --git a/sonar-core/src/main/resources/org/sonar/core/user/GroupMembershipMapper.xml b/sonar-core/src/main/resources/org/sonar/core/user/GroupMembershipMapper.xml index 0b9116328d7..7774f9eb4b0 100644 --- a/sonar-core/src/main/resources/org/sonar/core/user/GroupMembershipMapper.xml +++ b/sonar-core/src/main/resources/org/sonar/core/user/GroupMembershipMapper.xml @@ -45,4 +45,17 @@ GROUP BY g.name + + diff --git a/sonar-core/src/test/java/org/sonar/core/user/GroupMembershipDaoTest.java b/sonar-core/src/test/java/org/sonar/core/user/GroupMembershipDaoTest.java index d1a5759d912..570a7921777 100644 --- a/sonar-core/src/test/java/org/sonar/core/user/GroupMembershipDaoTest.java +++ b/sonar-core/src/test/java/org/sonar/core/user/GroupMembershipDaoTest.java @@ -185,4 +185,24 @@ public class GroupMembershipDaoTest { session.close(); } } + + @Test + public void count_groups_by_login() { + dbTester.prepareDbUnit(getClass(), "shared.xml"); + DbSession session = dbTester.myBatis().openSession(false); + + try { + assertThat(dao.countGroupsByLogins(session, Arrays.asList())).isEmpty(); + assertThat(dao.countGroupsByLogins(session, Arrays.asList("two-hundred"))) + .containsExactly(entry("two-hundred", 3)); + assertThat(dao.countGroupsByLogins(session, Arrays.asList("two-hundred", "two-hundred-one"))) + .containsOnly(entry("two-hundred", 3), entry("two-hundred-one", 1)); + assertThat(dao.countGroupsByLogins(session, Arrays.asList("two-hundred", "two-hundred-one", "two-hundred-two"))) + .containsOnly(entry("two-hundred", 3), entry("two-hundred-one", 1), entry("two-hundred-two", 0)); + assertThat(dao.countGroupsByLogins(session, Arrays.asList("two-hundred-two"))) + .containsOnly(entry("two-hundred-two", 0)); + } finally { + session.close(); + } + } } diff --git a/sonar-core/src/test/resources/org/sonar/core/user/GroupMembershipDaoTest/shared.xml b/sonar-core/src/test/resources/org/sonar/core/user/GroupMembershipDaoTest/shared.xml index f8ab9693a66..a882f396f6c 100644 --- a/sonar-core/src/test/resources/org/sonar/core/user/GroupMembershipDaoTest/shared.xml +++ b/sonar-core/src/test/resources/org/sonar/core/user/GroupMembershipDaoTest/shared.xml @@ -12,4 +12,8 @@ + + + + -- 2.39.5