From 06cafc9839cb231a8f71e1d1c41da43ac1dc0139 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lievremont Date: Mon, 18 May 2015 17:46:33 +0200 Subject: [PATCH] SONAR-6471 Add new WS to search for user groups --- .../platformlevel/PlatformLevel4.java | 3 + .../org/sonar/server/user/db/GroupDao.java | 27 ++- .../sonar/server/usergroups/package-info.java | 25 +++ .../server/usergroups/ws/SearchAction.java | 127 +++++++++++++ .../server/usergroups/ws/UserGroupsWs.java | 45 +++++ .../usergroups/ws/UserGroupsWsAction.java | 27 +++ .../server/usergroups/ws/package-info.java | 25 +++ .../server/usergroups/ws/example-search.json | 19 ++ .../sonar/server/user/db/GroupDaoTest.java | 70 ++++++- .../usergroups/ws/SearchActionTest.java | 175 ++++++++++++++++++ .../usergroups/ws/UserGroupsWsTest.java | 61 ++++++ .../user/db/GroupDaoTest/select_by_query.xml | 9 + .../ws/SearchActionTest/customers.json | 10 + .../usergroups/ws/SearchActionTest/empty.json | 6 + .../ws/SearchActionTest/five_groups.json | 12 ++ .../ws/SearchActionTest/page_1.json | 10 + .../ws/SearchActionTest/page_2.json | 9 + .../ws/SearchActionTest/page_3.json | 6 + .../ws/SearchActionTest/with_members.json | 12 ++ .../java/org/sonar/core/user/GroupMapper.java | 5 + .../sonar/core/user/GroupMembershipDao.java | 27 ++- .../core/user/GroupMembershipMapper.java | 6 +- .../org/sonar/core/user/GroupUserCount.java | 34 ++++ .../org/sonar/core/user/GroupMapper.xml | 12 ++ .../sonar/core/user/GroupMembershipMapper.xml | 13 ++ .../core/user/GroupMembershipDaoTest.java | 20 +- .../shared_plus_empty_group.xml | 16 ++ .../java/org/sonar/api/server/ws/Request.java | 10 +- 28 files changed, 797 insertions(+), 24 deletions(-) create mode 100644 server/sonar-server/src/main/java/org/sonar/server/usergroups/package-info.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/usergroups/ws/SearchAction.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/usergroups/ws/UserGroupsWs.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/usergroups/ws/UserGroupsWsAction.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/usergroups/ws/package-info.java create mode 100644 server/sonar-server/src/main/resources/org/sonar/server/usergroups/ws/example-search.json create mode 100644 server/sonar-server/src/test/java/org/sonar/server/usergroups/ws/SearchActionTest.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/usergroups/ws/UserGroupsWsTest.java create mode 100644 server/sonar-server/src/test/resources/org/sonar/server/user/db/GroupDaoTest/select_by_query.xml create mode 100644 server/sonar-server/src/test/resources/org/sonar/server/usergroups/ws/SearchActionTest/customers.json create mode 100644 server/sonar-server/src/test/resources/org/sonar/server/usergroups/ws/SearchActionTest/empty.json create mode 100644 server/sonar-server/src/test/resources/org/sonar/server/usergroups/ws/SearchActionTest/five_groups.json create mode 100644 server/sonar-server/src/test/resources/org/sonar/server/usergroups/ws/SearchActionTest/page_1.json create mode 100644 server/sonar-server/src/test/resources/org/sonar/server/usergroups/ws/SearchActionTest/page_2.json create mode 100644 server/sonar-server/src/test/resources/org/sonar/server/usergroups/ws/SearchActionTest/page_3.json create mode 100644 server/sonar-server/src/test/resources/org/sonar/server/usergroups/ws/SearchActionTest/with_members.json create mode 100644 sonar-core/src/main/java/org/sonar/core/user/GroupUserCount.java create mode 100644 sonar-core/src/test/resources/org/sonar/core/user/GroupMembershipDaoTest/shared_plus_empty_group.xml diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java index f829d7ea3ba..a09daf2b385 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java @@ -277,6 +277,7 @@ import org.sonar.server.user.ws.CurrentAction; import org.sonar.server.user.ws.FavoritesWs; import org.sonar.server.user.ws.UserPropertiesWs; import org.sonar.server.user.ws.UsersWs; +import org.sonar.server.usergroups.ws.UserGroupsWs; import org.sonar.server.util.BooleanTypeValidation; import org.sonar.server.util.FloatTypeValidation; import org.sonar.server.util.IntegerTypeValidation; @@ -490,6 +491,8 @@ public class PlatformLevel4 extends PlatformLevel { // groups GroupMembershipService.class, GroupMembershipFinder.class, + UserGroupsWs.class, + org.sonar.server.usergroups.ws.SearchAction.class, // permissions PermissionFacade.class, diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/db/GroupDao.java b/server/sonar-server/src/main/java/org/sonar/server/user/db/GroupDao.java index e8ae651cc2f..eda1a1ad2a3 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/user/db/GroupDao.java +++ b/server/sonar-server/src/main/java/org/sonar/server/user/db/GroupDao.java @@ -20,20 +20,23 @@ package org.sonar.server.user.db; +import java.util.Date; +import java.util.List; +import javax.annotation.Nullable; +import org.apache.commons.lang.StringUtils; +import org.apache.ibatis.session.RowBounds; import org.sonar.api.utils.System2; import org.sonar.core.persistence.DaoComponent; import org.sonar.core.persistence.DbSession; import org.sonar.core.user.GroupDto; import org.sonar.core.user.GroupMapper; -import java.util.Date; -import java.util.List; - /** * @since 3.2 */ public class GroupDao implements DaoComponent { + private static final String SQL_WILDCARD = "%"; private System2 system; public GroupDao(System2 system) { @@ -44,6 +47,14 @@ public class GroupDao implements DaoComponent { return mapper(session).selectByKey(key); } + public int countByQuery(DbSession session, @Nullable String query) { + return mapper(session).countByQuery(groupSearchToSql(query)); + } + + public List selectByQuery(DbSession session, @Nullable String query, int offset, int limit) { + return mapper(session).selectByQuery(groupSearchToSql(query), new RowBounds(offset, limit)); + } + public GroupDto insert(DbSession session, GroupDto item) { Date createdAt = new Date(system.now()); item.setCreatedAt(createdAt) @@ -59,4 +70,14 @@ public class GroupDao implements DaoComponent { private GroupMapper mapper(DbSession session) { return session.getMapper(GroupMapper.class); } + + private String groupSearchToSql(@Nullable String query) { + String sql = SQL_WILDCARD; + if (query != null) { + sql = StringUtils.replace(StringUtils.upperCase(query), SQL_WILDCARD, "/%"); + sql = StringUtils.replace(sql, "_", "/_"); + sql = SQL_WILDCARD + sql + SQL_WILDCARD; + } + return sql; + } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/usergroups/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/usergroups/package-info.java new file mode 100644 index 00000000000..d8a077df2fa --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/usergroups/package-info.java @@ -0,0 +1,25 @@ +/* + * 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. + */ + +@ParametersAreNonnullByDefault +package org.sonar.server.usergroups; + +import javax.annotation.ParametersAreNonnullByDefault; + diff --git a/server/sonar-server/src/main/java/org/sonar/server/usergroups/ws/SearchAction.java b/server/sonar-server/src/main/java/org/sonar/server/usergroups/ws/SearchAction.java new file mode 100644 index 00000000000..bcf1c97ca9d --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/usergroups/ws/SearchAction.java @@ -0,0 +1,127 @@ +/* + * 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.server.usergroups.ws; + +import com.google.common.base.Function; +import com.google.common.collect.Collections2; +import com.google.common.collect.Sets; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.annotation.Nonnull; +import org.apache.commons.lang.StringUtils; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService.NewController; +import org.sonar.api.server.ws.WebService.Param; +import org.sonar.api.utils.text.JsonWriter; +import org.sonar.core.persistence.DbSession; +import org.sonar.core.persistence.MyBatis; +import org.sonar.core.user.GroupDto; +import org.sonar.server.db.DbClient; +import org.sonar.server.es.SearchOptions; + +public class SearchAction implements UserGroupsWsAction { + + private static final String FIELD_ID = "id"; + private static final String FIELD_NAME = "name"; + private static final String FIELD_DESCRIPTION = "description"; + private static final String FIELD_MEMBERS_COUNT = "membersCount"; + private static final List ALL_FIELDS = Arrays.asList(FIELD_NAME, FIELD_DESCRIPTION, FIELD_MEMBERS_COUNT); + + private DbClient dbClient; + + public SearchAction(DbClient dbClient) { + this.dbClient = dbClient; + } + + @Override + public void define(NewController context) { + context.createAction("search") + .setDescription("Search for user groups") + .setHandler(this) + .setResponseExample(getClass().getResource("example-search.json")) + .setSince("5.2") + .addFieldsParam(ALL_FIELDS) + .addPagingParams(100) + .addSearchQuery("sonar-users", "names"); + } + + @Override + public void handle(Request request, Response response) throws Exception { + int page = request.mandatoryParamAsInt(Param.PAGE); + int pageSize = request.mandatoryParamAsInt(Param.PAGE_SIZE); + SearchOptions options = new SearchOptions() + .setPage(page, pageSize); + + String query = StringUtils.defaultIfBlank(request.param(Param.TEXT_QUERY), ""); + Set fields = neededFields(request); + + DbSession dbSession = dbClient.openSession(false); + try { + int limit = dbClient.groupDao().countByQuery(dbSession, query); + List groups = dbClient.groupDao().selectByQuery(dbSession, query, options.getOffset(), pageSize); + Collection groupIds = Collections2.transform(groups, new Function() { + @Override + public Long apply(@Nonnull GroupDto input) { + return input.getId(); + } + }); + Map userCountByGroup = dbClient.groupMembershipDao().countUsersByGroups(dbSession, groupIds); + + JsonWriter json = response.newJsonWriter().beginObject(); + options.writeJson(json, limit); + writeGroups(json, groups, userCountByGroup, fields); + json.endObject().close(); + } finally { + MyBatis.closeQuietly(dbSession); + } + } + + private void writeGroups(JsonWriter json, List groups, Map userCountByGroup, Set fields) { + json.name("groups").beginArray(); + for (GroupDto group : groups) { + writeGroup(json, group, userCountByGroup.get(group.getName()), fields); + } + json.endArray(); + } + + private void writeGroup(JsonWriter json, GroupDto group, Integer memberCount, Set fields) { + json.beginObject() + .prop(FIELD_ID, group.getId().toString()) + .prop(FIELD_NAME, fields.contains(FIELD_NAME) ? group.getName() : null) + .prop(FIELD_DESCRIPTION, fields.contains(FIELD_DESCRIPTION) ? group.getDescription() : null) + .prop(FIELD_MEMBERS_COUNT, fields.contains(FIELD_MEMBERS_COUNT) ? memberCount : null) + .endObject(); + } + + private Set neededFields(Request request) { + Set fields = Sets.newHashSet(); + List fieldsFromRequest = request.paramAsStrings(Param.FIELDS); + if (fieldsFromRequest == null || fieldsFromRequest.isEmpty()) { + fields.addAll(ALL_FIELDS); + } else { + fields.addAll(fieldsFromRequest); + } + return fields; + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/usergroups/ws/UserGroupsWs.java b/server/sonar-server/src/main/java/org/sonar/server/usergroups/ws/UserGroupsWs.java new file mode 100644 index 00000000000..8ace6844681 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/usergroups/ws/UserGroupsWs.java @@ -0,0 +1,45 @@ +/* + * 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.server.usergroups.ws; + +import org.sonar.api.server.ws.WebService; + +public class UserGroupsWs implements WebService { + + private UserGroupsWsAction[] actions; + + public UserGroupsWs(UserGroupsWsAction... actions) { + this.actions = actions; + } + + @Override + public void define(Context context) { + NewController controller = context.createController("api/usergroups") + .setDescription("User groups management") + .setSince("5.2"); + + for (UserGroupsWsAction action : actions) { + action.define(controller); + } + + controller.done(); + } + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/usergroups/ws/UserGroupsWsAction.java b/server/sonar-server/src/main/java/org/sonar/server/usergroups/ws/UserGroupsWsAction.java new file mode 100644 index 00000000000..4e0bbd731dc --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/usergroups/ws/UserGroupsWsAction.java @@ -0,0 +1,27 @@ +/* + * 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.server.usergroups.ws; + +import org.sonar.server.ws.WsAction; + +public interface UserGroupsWsAction extends WsAction { + + // Marker interface +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/usergroups/ws/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/usergroups/ws/package-info.java new file mode 100644 index 00000000000..66099e7017c --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/usergroups/ws/package-info.java @@ -0,0 +1,25 @@ +/* + * 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. + */ + +@ParametersAreNonnullByDefault +package org.sonar.server.usergroups.ws; + +import javax.annotation.ParametersAreNonnullByDefault; + diff --git a/server/sonar-server/src/main/resources/org/sonar/server/usergroups/ws/example-search.json b/server/sonar-server/src/main/resources/org/sonar/server/usergroups/ws/example-search.json new file mode 100644 index 00000000000..e7371e910ef --- /dev/null +++ b/server/sonar-server/src/main/resources/org/sonar/server/usergroups/ws/example-search.json @@ -0,0 +1,19 @@ +{ + "p": 1, + "ps": 100, + "total": 2, + "groups": [ + { + "id": "1", + "name": "users", + "description": "Users", + "membersCount": 17 + }, + { + "id": "2", + "name": "administrators", + "description": "Administrators", + "membersCount": 2 + } + ] +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/user/db/GroupDaoTest.java b/server/sonar-server/src/test/java/org/sonar/server/user/db/GroupDaoTest.java index ca20818864c..bb0efb8baf1 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/user/db/GroupDaoTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/user/db/GroupDaoTest.java @@ -22,18 +22,25 @@ package org.sonar.server.user.db; import org.junit.After; import org.junit.Before; +import org.junit.ClassRule; import org.junit.Test; +import org.junit.experimental.categories.Category; import org.sonar.api.utils.DateUtils; import org.sonar.api.utils.System2; -import org.sonar.core.persistence.AbstractDaoTestCase; import org.sonar.core.persistence.DbSession; +import org.sonar.core.persistence.DbTester; import org.sonar.core.user.GroupDto; +import org.sonar.test.DbTests; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -public class GroupDaoTest extends AbstractDaoTestCase { +@Category(DbTests.class) +public class GroupDaoTest { + + @ClassRule + public static final DbTester dbTester = new DbTester(); GroupDao dao; DbSession session; @@ -41,7 +48,8 @@ public class GroupDaoTest extends AbstractDaoTestCase { @Before public void setUp() { - this.session = getMyBatis().openSession(false); + dbTester.truncateTables(); + this.session = dbTester.myBatis().openSession(false); this.system2 = mock(System2.class); this.dao = new GroupDao(system2); } @@ -53,7 +61,7 @@ public class GroupDaoTest extends AbstractDaoTestCase { @Test public void select_by_key() { - setupData("select_by_key"); + dbTester.prepareDbUnit(getClass(), "select_by_key.xml"); GroupDto group = new GroupDao(system2).selectByKey(session, "sonar-users"); assertThat(group).isNotNull(); @@ -66,7 +74,7 @@ public class GroupDaoTest extends AbstractDaoTestCase { @Test public void find_by_user_login() { - setupData("find_by_user_login"); + dbTester.prepareDbUnit(getClass(), "find_by_user_login.xml"); assertThat(dao.findByUserLogin(session, "john")).hasSize(2); assertThat(dao.findByUserLogin(session, "max")).isEmpty(); @@ -76,7 +84,7 @@ public class GroupDaoTest extends AbstractDaoTestCase { public void insert() { when(system2.now()).thenReturn(DateUtils.parseDate("2014-09-08").getTime()); - setupData("empty"); + dbTester.prepareDbUnit(getClass(), "empty.xml"); GroupDto dto = new GroupDto() .setId(1L) @@ -86,6 +94,54 @@ public class GroupDaoTest extends AbstractDaoTestCase { dao.insert(session, dto); session.commit(); - checkTables("insert", "groups"); + dbTester.assertDbUnit(getClass(), "insert-result.xml", "groups"); + } + + @Test + public void select_by_query() { + dbTester.prepareDbUnit(getClass(), "select_by_query.xml"); + + /* + * Ordering and paging are not fully tested, case insensitive sort is broken on MySQL + */ + + // Null query + assertThat(new GroupDao(system2).selectByQuery(session, null, 0, 10)) + .hasSize(5) + .extracting("name").containsOnly("customers-group1", "customers-group2", "customers-group3", "SONAR-ADMINS", "sonar-users"); + + // Empty query + assertThat(new GroupDao(system2).selectByQuery(session, "", 0, 10)) + .hasSize(5) + .extracting("name").containsOnly("customers-group1", "customers-group2", "customers-group3", "SONAR-ADMINS", "sonar-users"); + + // Filter on name + assertThat(new GroupDao(system2).selectByQuery(session, "sonar", 0, 10)) + .hasSize(2) + .extracting("name").containsOnly("SONAR-ADMINS", "sonar-users"); + + // Pagination + assertThat(new GroupDao(system2).selectByQuery(session, null, 0, 3)) + .hasSize(3); + assertThat(new GroupDao(system2).selectByQuery(session, null, 3, 3)) + .hasSize(2); + assertThat(new GroupDao(system2).selectByQuery(session, null, 6, 3)).isEmpty(); + assertThat(new GroupDao(system2).selectByQuery(session, null, 0, 5)) + .hasSize(5); + assertThat(new GroupDao(system2).selectByQuery(session, null, 5, 5)).isEmpty(); + } + + @Test + public void count_by_query() { + dbTester.prepareDbUnit(getClass(), "select_by_query.xml"); + + // Null query + assertThat(new GroupDao(system2).countByQuery(session, null)).isEqualTo(5); + + // Empty query + assertThat(new GroupDao(system2).countByQuery(session, "")).isEqualTo(5); + + // Filter on name + assertThat(new GroupDao(system2).countByQuery(session, "sonar")).isEqualTo(2); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/usergroups/ws/SearchActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/usergroups/ws/SearchActionTest.java new file mode 100644 index 00000000000..9ec90c7554c --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/usergroups/ws/SearchActionTest.java @@ -0,0 +1,175 @@ +/* + * 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.server.usergroups.ws; + +import org.apache.commons.lang.StringUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.sonar.api.server.ws.WebService.Param; +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.UserGroupDto; +import org.sonar.server.db.DbClient; +import org.sonar.server.user.db.GroupDao; +import org.sonar.server.user.db.UserGroupDao; +import org.sonar.server.ws.WsTester; +import org.sonar.test.DbTests; + +import static org.assertj.core.api.Assertions.assertThat; + +@Category(DbTests.class) +public class SearchActionTest { + + @ClassRule + public static final DbTester dbTester = new DbTester(); + + private WsTester tester; + + private GroupDao groupDao; + + private GroupMembershipDao groupMembershipDao; + + private UserGroupDao userGroupDao; + + private DbSession session; + + @Before + public void setUp() { + dbTester.truncateTables(); + + groupDao = new GroupDao(System2.INSTANCE); + groupMembershipDao = new GroupMembershipDao(dbTester.myBatis()); + userGroupDao = new UserGroupDao(); + + DbClient dbClient = new DbClient(dbTester.database(), dbTester.myBatis(), groupDao, groupMembershipDao); + + tester = new WsTester(new UserGroupsWs(new SearchAction(dbClient))); + + session = dbClient.openSession(false); + } + + @After + public void after() { + session.close(); + } + + @Test + public void search_empty() throws Exception { + tester.newGetRequest("api/usergroups", "search").execute().assertJson(getClass(), "empty.json"); + } + + @Test + public void search_without_parameters() throws Exception { + insertGroups("users", "admins", "customer1", "customer2", "customer3"); + session.commit(); + + tester.newGetRequest("api/usergroups", "search").execute().assertJson(getClass(), "five_groups.json"); + } + + @Test + public void search_with_members() throws Exception { + insertGroups("users", "admins", "customer1", "customer2", "customer3"); + insertMembers("users", 5); + insertMembers("admins", 1); + insertMembers("customer2", 4); + session.commit(); + + tester.newGetRequest("api/usergroups", "search").execute().assertJson(getClass(), "with_members.json"); + } + + @Test + public void search_with_query() throws Exception { + insertGroups("users", "admins", "customer1", "customer2", "customer3"); + session.commit(); + + tester.newGetRequest("api/usergroups", "search").setParam(Param.TEXT_QUERY, "custom").execute().assertJson(getClass(), "customers.json"); + } + + @Test + public void search_with_paging() throws Exception { + insertGroups("users", "admins", "customer1", "customer2", "customer3"); + session.commit(); + + tester.newGetRequest("api/usergroups", "search") + .setParam(Param.PAGE_SIZE, "3").execute().assertJson(getClass(), "page_1.json"); + tester.newGetRequest("api/usergroups", "search") + .setParam(Param.PAGE_SIZE, "3").setParam(Param.PAGE, "2").execute().assertJson(getClass(), "page_2.json"); + tester.newGetRequest("api/usergroups", "search") + .setParam(Param.PAGE_SIZE, "3").setParam(Param.PAGE, "3").execute().assertJson(getClass(), "page_3.json"); + } + + @Test + public void search_with_fields() throws Exception { + insertGroups("sonar-users"); + session.commit(); + + assertThat(tester.newGetRequest("api/usergroups", "search").execute().outputAsString()) + .contains("id") + .contains("name") + .contains("description") + .contains("membersCount"); + + assertThat(tester.newGetRequest("api/usergroups", "search").setParam(Param.FIELDS, "").execute().outputAsString()) + .contains("id") + .contains("name") + .contains("description") + .contains("membersCount"); + + assertThat(tester.newGetRequest("api/usergroups", "search").setParam(Param.FIELDS, "name").execute().outputAsString()) + .contains("id") + .contains("name") + .doesNotContain("description") + .doesNotContain("membersCount"); + + assertThat(tester.newGetRequest("api/usergroups", "search").setParam(Param.FIELDS, "description").execute().outputAsString()) + .contains("id") + .doesNotContain("name") + .contains("description") + .doesNotContain("membersCount"); + + assertThat(tester.newGetRequest("api/usergroups", "search").setParam(Param.FIELDS, "membersCount").execute().outputAsString()) + .contains("id") + .doesNotContain("name") + .doesNotContain("description") + .contains("membersCount"); + } + + private void insertGroups(String... groupNames) { + for (String groupName : groupNames) { + groupDao.insert(session, new GroupDto() + .setName(groupName) + .setDescription(StringUtils.capitalize(groupName))); + } + } + + private void insertMembers(String groupName, int count) { + long groupId = groupDao.selectByKey(session, groupName).getId(); + for (int i = 0; i < count; i++) { + userGroupDao.insert(session, new UserGroupDto().setGroupId(groupId).setUserId((long) i + 1)); + } + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/usergroups/ws/UserGroupsWsTest.java b/server/sonar-server/src/test/java/org/sonar/server/usergroups/ws/UserGroupsWsTest.java new file mode 100644 index 00000000000..ad4ff6c6bfc --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/usergroups/ws/UserGroupsWsTest.java @@ -0,0 +1,61 @@ +/* + * 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.server.usergroups.ws; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.server.ws.WebService; +import org.sonar.server.db.DbClient; +import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.ws.WsTester; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +public class UserGroupsWsTest { + @Rule + public UserSessionRule userSessionRule = UserSessionRule.standalone(); + WebService.Controller controller; + + @Before + public void setUp() { + WsTester tester = new WsTester(new UserGroupsWs(new SearchAction(mock(DbClient.class)))); + controller = tester.controller("api/usergroups"); + } + + @Test + public void define_controller() { + assertThat(controller).isNotNull(); + assertThat(controller.description()).isNotEmpty(); + assertThat(controller.since()).isEqualTo("5.2"); + assertThat(controller.actions()).hasSize(1); + } + + @Test + public void define_search_action() { + WebService.Action action = controller.action("search"); + assertThat(action).isNotNull(); + assertThat(action.isPost()).isFalse(); + assertThat(action.responseExampleAsString()).isNotEmpty(); + assertThat(action.params()).hasSize(4); + } +} diff --git a/server/sonar-server/src/test/resources/org/sonar/server/user/db/GroupDaoTest/select_by_query.xml b/server/sonar-server/src/test/resources/org/sonar/server/user/db/GroupDaoTest/select_by_query.xml new file mode 100644 index 00000000000..983f6ad9980 --- /dev/null +++ b/server/sonar-server/src/test/resources/org/sonar/server/user/db/GroupDaoTest/select_by_query.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/server/sonar-server/src/test/resources/org/sonar/server/usergroups/ws/SearchActionTest/customers.json b/server/sonar-server/src/test/resources/org/sonar/server/usergroups/ws/SearchActionTest/customers.json new file mode 100644 index 00000000000..b94a0072af9 --- /dev/null +++ b/server/sonar-server/src/test/resources/org/sonar/server/usergroups/ws/SearchActionTest/customers.json @@ -0,0 +1,10 @@ +{ + "p": 1, + "ps": 100, + "total": 3, + "groups": [ + {"name": "customer1", "description": "Customer1", "membersCount": 0}, + {"name": "customer2", "description": "Customer2", "membersCount": 0}, + {"name": "customer3", "description": "Customer3", "membersCount": 0} + ] +} diff --git a/server/sonar-server/src/test/resources/org/sonar/server/usergroups/ws/SearchActionTest/empty.json b/server/sonar-server/src/test/resources/org/sonar/server/usergroups/ws/SearchActionTest/empty.json new file mode 100644 index 00000000000..ef5440a6563 --- /dev/null +++ b/server/sonar-server/src/test/resources/org/sonar/server/usergroups/ws/SearchActionTest/empty.json @@ -0,0 +1,6 @@ +{ + "p": 1, + "ps": 100, + "total": 0, + "groups": [] +} diff --git a/server/sonar-server/src/test/resources/org/sonar/server/usergroups/ws/SearchActionTest/five_groups.json b/server/sonar-server/src/test/resources/org/sonar/server/usergroups/ws/SearchActionTest/five_groups.json new file mode 100644 index 00000000000..0bd50502fb5 --- /dev/null +++ b/server/sonar-server/src/test/resources/org/sonar/server/usergroups/ws/SearchActionTest/five_groups.json @@ -0,0 +1,12 @@ +{ + "p": 1, + "ps": 100, + "total": 5, + "groups": [ + {"name": "admins", "description": "Admins", "membersCount": 0}, + {"name": "customer1", "description": "Customer1", "membersCount": 0}, + {"name": "customer2", "description": "Customer2", "membersCount": 0}, + {"name": "customer3", "description": "Customer3", "membersCount": 0}, + {"name": "users", "description": "Users", "membersCount": 0} + ] +} diff --git a/server/sonar-server/src/test/resources/org/sonar/server/usergroups/ws/SearchActionTest/page_1.json b/server/sonar-server/src/test/resources/org/sonar/server/usergroups/ws/SearchActionTest/page_1.json new file mode 100644 index 00000000000..b9834b6865f --- /dev/null +++ b/server/sonar-server/src/test/resources/org/sonar/server/usergroups/ws/SearchActionTest/page_1.json @@ -0,0 +1,10 @@ +{ + "p": 1, + "ps": 3, + "total": 5, + "groups": [ + {"name": "admins", "description": "Admins", "membersCount": 0}, + {"name": "customer1", "description": "Customer1", "membersCount": 0}, + {"name": "customer2", "description": "Customer2", "membersCount": 0} + ] +} diff --git a/server/sonar-server/src/test/resources/org/sonar/server/usergroups/ws/SearchActionTest/page_2.json b/server/sonar-server/src/test/resources/org/sonar/server/usergroups/ws/SearchActionTest/page_2.json new file mode 100644 index 00000000000..60e051d1278 --- /dev/null +++ b/server/sonar-server/src/test/resources/org/sonar/server/usergroups/ws/SearchActionTest/page_2.json @@ -0,0 +1,9 @@ +{ + "p": 2, + "ps": 3, + "total": 5, + "groups": [ + {"name": "customer3", "description": "Customer3", "membersCount": 0}, + {"name": "users", "description": "Users", "membersCount": 0} + ] +} diff --git a/server/sonar-server/src/test/resources/org/sonar/server/usergroups/ws/SearchActionTest/page_3.json b/server/sonar-server/src/test/resources/org/sonar/server/usergroups/ws/SearchActionTest/page_3.json new file mode 100644 index 00000000000..c9775574e61 --- /dev/null +++ b/server/sonar-server/src/test/resources/org/sonar/server/usergroups/ws/SearchActionTest/page_3.json @@ -0,0 +1,6 @@ +{ + "p": 3, + "ps": 3, + "total": 5, + "groups": [] +} diff --git a/server/sonar-server/src/test/resources/org/sonar/server/usergroups/ws/SearchActionTest/with_members.json b/server/sonar-server/src/test/resources/org/sonar/server/usergroups/ws/SearchActionTest/with_members.json new file mode 100644 index 00000000000..205c89e8603 --- /dev/null +++ b/server/sonar-server/src/test/resources/org/sonar/server/usergroups/ws/SearchActionTest/with_members.json @@ -0,0 +1,12 @@ +{ + "p": 1, + "ps": 100, + "total": 5, + "groups": [ + {"name": "admins", "description": "Admins", "membersCount": 1}, + {"name": "customer1", "description": "Customer1", "membersCount": 0}, + {"name": "customer2", "description": "Customer2", "membersCount": 4}, + {"name": "customer3", "description": "Customer3", "membersCount": 0}, + {"name": "users", "description": "Users", "membersCount": 5} + ] +} diff --git a/sonar-core/src/main/java/org/sonar/core/user/GroupMapper.java b/sonar-core/src/main/java/org/sonar/core/user/GroupMapper.java index 95d0eef39c8..1dadf30f572 100644 --- a/sonar-core/src/main/java/org/sonar/core/user/GroupMapper.java +++ b/sonar-core/src/main/java/org/sonar/core/user/GroupMapper.java @@ -20,6 +20,8 @@ package org.sonar.core.user; +import org.apache.ibatis.session.RowBounds; + import javax.annotation.CheckForNull; import java.util.List; @@ -33,4 +35,7 @@ public interface GroupMapper { void insert(GroupDto groupDto); + List selectByQuery(String query, RowBounds rowBounds); + + int countByQuery(String query); } 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 8338a3d070b..8dcba5a7839 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 @@ -21,15 +21,20 @@ package org.sonar.core.user; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Function; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import javax.annotation.Nonnull; import org.apache.ibatis.session.RowBounds; import org.apache.ibatis.session.SqlSession; 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 java.util.List; -import java.util.Map; - public class GroupMembershipDao implements DaoComponent { private final MyBatis mybatis; @@ -58,6 +63,21 @@ public class GroupMembershipDao implements DaoComponent { return mapper(session).countGroups(params); } + public Map countUsersByGroups(final DbSession session, Collection groupIds) { + final Map result = Maps.newHashMap(); + DaoUtils.executeLargeInputs(groupIds, new Function, List>() { + @Override + public List apply(@Nonnull List input) { + List userCounts = mapper(session).countUsersByGroup(input); + for (GroupUserCount count : userCounts) { + result.put(count.groupName(), count.userCount()); + } + return userCounts; + } + }); + return result; + } + @VisibleForTesting List selectGroups(GroupMembershipQuery query, Long userId) { return selectGroups(query, userId, 0, Integer.MAX_VALUE); @@ -66,5 +86,4 @@ public class GroupMembershipDao implements DaoComponent { private GroupMembershipMapper mapper(SqlSession session) { return session.getMapper(GroupMembershipMapper.class); } - } 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 22efebd01ac..4c848759cf2 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 @@ -19,10 +19,10 @@ */ package org.sonar.core.user; -import org.apache.ibatis.session.RowBounds; - import java.util.List; import java.util.Map; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.session.RowBounds; public interface GroupMembershipMapper { @@ -31,4 +31,6 @@ public interface GroupMembershipMapper { List selectGroups(Map params, RowBounds rowBounds); int countGroups(Map params); + + List countUsersByGroup(@Param("groupIds") List groupIds); } diff --git a/sonar-core/src/main/java/org/sonar/core/user/GroupUserCount.java b/sonar-core/src/main/java/org/sonar/core/user/GroupUserCount.java new file mode 100644 index 00000000000..e2f4cfa1840 --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/user/GroupUserCount.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 GroupUserCount { + + private String groupName; + private int userCount; + + public String groupName() { + return groupName; + } + + public int userCount() { + return userCount; + } +} diff --git a/sonar-core/src/main/resources/org/sonar/core/user/GroupMapper.xml b/sonar-core/src/main/resources/org/sonar/core/user/GroupMapper.xml index 87a6ea340c2..b2ffcb62296 100644 --- a/sonar-core/src/main/resources/org/sonar/core/user/GroupMapper.xml +++ b/sonar-core/src/main/resources/org/sonar/core/user/GroupMapper.xml @@ -35,4 +35,16 @@ VALUES (#{name}, #{description}, #{createdAt}, #{updatedAt}) + + + 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 5c7654c0cb9..0b9116328d7 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 @@ -32,4 +32,17 @@ + + 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 d6cb76d9dcf..d1a5759d912 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 @@ -20,6 +20,8 @@ package org.sonar.core.user; +import java.util.Arrays; +import java.util.List; import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; @@ -28,9 +30,8 @@ import org.sonar.core.persistence.DbSession; import org.sonar.core.persistence.DbTester; import org.sonar.test.DbTests; -import java.util.List; - import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.data.MapEntry.entry; @Category(DbTests.class) public class GroupMembershipDaoTest { @@ -169,4 +170,19 @@ public class GroupMembershipDaoTest { session.close(); } } + + @Test + public void count_users_by_group() { + dbTester.prepareDbUnit(getClass(), "shared_plus_empty_group.xml"); + DbSession session = dbTester.myBatis().openSession(false); + + try { + assertThat(dao.countUsersByGroups(session, Arrays.asList(100L, 101L, 102L, 103L))).containsOnly( + entry("sonar-users", 2), entry("sonar-reviewers", 1), entry("sonar-administrators", 1), entry("sonar-nobody", 0)); + assertThat(dao.countUsersByGroups(session, Arrays.asList(100L, 103L))).containsOnly( + entry("sonar-administrators", 1), entry("sonar-nobody", 0)); + } finally { + session.close(); + } + } } diff --git a/sonar-core/src/test/resources/org/sonar/core/user/GroupMembershipDaoTest/shared_plus_empty_group.xml b/sonar-core/src/test/resources/org/sonar/core/user/GroupMembershipDaoTest/shared_plus_empty_group.xml new file mode 100644 index 00000000000..487e11ff71f --- /dev/null +++ b/sonar-core/src/test/resources/org/sonar/core/user/GroupMembershipDaoTest/shared_plus_empty_group.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/server/ws/Request.java b/sonar-plugin-api/src/main/java/org/sonar/api/server/ws/Request.java index 427350b2ea6..eb25819e0ae 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/server/ws/Request.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/server/ws/Request.java @@ -21,16 +21,14 @@ package org.sonar.api.server.ws; import com.google.common.base.Splitter; import com.google.common.collect.Lists; -import org.apache.commons.lang.StringUtils; -import org.sonar.api.utils.DateUtils; -import org.sonar.api.utils.SonarException; - -import javax.annotation.CheckForNull; - import java.io.InputStream; import java.util.ArrayList; import java.util.Date; import java.util.List; +import javax.annotation.CheckForNull; +import org.apache.commons.lang.StringUtils; +import org.sonar.api.utils.DateUtils; +import org.sonar.api.utils.SonarException; /** * @since 4.2 -- 2.39.5