]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6471 Add new WS to search for user groups
authorJean-Baptiste Lievremont <jean-baptiste.lievremont@sonarsource.com>
Mon, 18 May 2015 15:46:33 +0000 (17:46 +0200)
committerJean-Baptiste Lievremont <jean-baptiste.lievremont@sonarsource.com>
Fri, 22 May 2015 13:34:42 +0000 (15:34 +0200)
28 files changed:
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
server/sonar-server/src/main/java/org/sonar/server/user/db/GroupDao.java
server/sonar-server/src/main/java/org/sonar/server/usergroups/package-info.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/usergroups/ws/SearchAction.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/usergroups/ws/UserGroupsWs.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/usergroups/ws/UserGroupsWsAction.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/usergroups/ws/package-info.java [new file with mode: 0644]
server/sonar-server/src/main/resources/org/sonar/server/usergroups/ws/example-search.json [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/user/db/GroupDaoTest.java
server/sonar-server/src/test/java/org/sonar/server/usergroups/ws/SearchActionTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/usergroups/ws/UserGroupsWsTest.java [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/user/db/GroupDaoTest/select_by_query.xml [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/usergroups/ws/SearchActionTest/customers.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/usergroups/ws/SearchActionTest/empty.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/usergroups/ws/SearchActionTest/five_groups.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/usergroups/ws/SearchActionTest/page_1.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/usergroups/ws/SearchActionTest/page_2.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/usergroups/ws/SearchActionTest/page_3.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/usergroups/ws/SearchActionTest/with_members.json [new file with mode: 0644]
sonar-core/src/main/java/org/sonar/core/user/GroupMapper.java
sonar-core/src/main/java/org/sonar/core/user/GroupMembershipDao.java
sonar-core/src/main/java/org/sonar/core/user/GroupMembershipMapper.java
sonar-core/src/main/java/org/sonar/core/user/GroupUserCount.java [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/core/user/GroupMapper.xml
sonar-core/src/main/resources/org/sonar/core/user/GroupMembershipMapper.xml
sonar-core/src/test/java/org/sonar/core/user/GroupMembershipDaoTest.java
sonar-core/src/test/resources/org/sonar/core/user/GroupMembershipDaoTest/shared_plus_empty_group.xml [new file with mode: 0644]
sonar-plugin-api/src/main/java/org/sonar/api/server/ws/Request.java

index f829d7ea3bad5a28475024040f9e84aada5d7195..a09daf2b385c7fc8c2b4fa2047491062b1dfd833 100644 (file)
@@ -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,
index e8ae651cc2f7dd145d5577bd9fb6039e762d4cd4..eda1a1ad2a3bcd8f7e96daf504a53474f6865658 100644 (file)
 
 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<GroupDto> 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 (file)
index 0000000..d8a077d
--- /dev/null
@@ -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 (file)
index 0000000..bcf1c97
--- /dev/null
@@ -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<String> 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<String> fields = neededFields(request);
+
+    DbSession dbSession = dbClient.openSession(false);
+    try {
+      int limit = dbClient.groupDao().countByQuery(dbSession, query);
+      List<GroupDto> groups = dbClient.groupDao().selectByQuery(dbSession, query, options.getOffset(), pageSize);
+      Collection<Long> groupIds = Collections2.transform(groups, new Function<GroupDto, Long>() {
+        @Override
+        public Long apply(@Nonnull GroupDto input) {
+          return input.getId();
+        }
+      });
+      Map<String, Integer> 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<GroupDto> groups, Map<String, Integer> userCountByGroup, Set<String> 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<String> 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<String> neededFields(Request request) {
+    Set<String> fields = Sets.newHashSet();
+    List<String> 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 (file)
index 0000000..8ace684
--- /dev/null
@@ -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 (file)
index 0000000..4e0bbd7
--- /dev/null
@@ -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 (file)
index 0000000..66099e7
--- /dev/null
@@ -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 (file)
index 0000000..e7371e9
--- /dev/null
@@ -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
+    }
+  ]
+}
index ca20818864c75c7a05cf451b49b642f262b1eed2..bb0efb8baf1dc5285d517d8281584027bacf6b73 100644 (file)
@@ -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 (file)
index 0000000..9ec90c7
--- /dev/null
@@ -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 (file)
index 0000000..ad4ff6c
--- /dev/null
@@ -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 (file)
index 0000000..983f6ad
--- /dev/null
@@ -0,0 +1,9 @@
+<dataset>
+
+  <groups id="1" name="sonar-users" description="Sonar Users" created_at="2014-09-07" updated_at="2014-09-08"/>
+  <groups id="2" name="SONAR-ADMINS" description="Sonar Admins" created_at="2014-09-07" updated_at="2014-09-08"/>
+  <groups id="3" name="customers-group1" description="Group 1" created_at="2014-09-07" updated_at="2014-09-08"/>
+  <groups id="4" name="customers-group2" description="Group 2" created_at="2014-09-07" updated_at="2014-09-08"/>
+  <groups id="5" name="customers-group3" description="Group 3" created_at="2014-09-07" updated_at="2014-09-08"/>
+
+</dataset>
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 (file)
index 0000000..b94a007
--- /dev/null
@@ -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 (file)
index 0000000..ef5440a
--- /dev/null
@@ -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 (file)
index 0000000..0bd5050
--- /dev/null
@@ -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 (file)
index 0000000..b9834b6
--- /dev/null
@@ -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 (file)
index 0000000..60e051d
--- /dev/null
@@ -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 (file)
index 0000000..c977557
--- /dev/null
@@ -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 (file)
index 0000000..205c89e
--- /dev/null
@@ -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}
+  ]
+}
index 95d0eef39c88aea31cdd11089971f88f5e38eec8..1dadf30f57229913c8bca2ffec98da7614f44955 100644 (file)
@@ -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<GroupDto> selectByQuery(String query, RowBounds rowBounds);
+
+  int countByQuery(String query);
 }
index 8338a3d070b8bdec3adb15fdc8b56cc28ff74273..8dcba5a78390000e64e818f564ebe9ca00856853 100644 (file)
 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<String, Integer> countUsersByGroups(final DbSession session, Collection<Long> groupIds) {
+    final Map<String, Integer> result = Maps.newHashMap();
+    DaoUtils.executeLargeInputs(groupIds, new Function<List<Long>, List<GroupUserCount>>() {
+      @Override
+      public List<GroupUserCount> apply(@Nonnull List<Long> input) {
+        List<GroupUserCount> userCounts = mapper(session).countUsersByGroup(input);
+        for (GroupUserCount count : userCounts) {
+          result.put(count.groupName(), count.userCount());
+        }
+        return userCounts;
+      }
+    });
+    return result;
+  }
+
   @VisibleForTesting
   List<GroupMembershipDto> 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);
   }
-
 }
index 22efebd01acf08e9649009dfabcfbcb9f1d02eae..4c848759cf28156a7fb3f881d44d5a796bc5659e 100644 (file)
  */
 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<GroupMembershipDto> selectGroups(Map<String, Object> params, RowBounds rowBounds);
 
   int countGroups(Map<String, Object> params);
+
+  List<GroupUserCount> countUsersByGroup(@Param("groupIds") List<Long> 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 (file)
index 0000000..e2f4cfa
--- /dev/null
@@ -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;
+  }
+}
index 87a6ea340c240519705a82731346fdfaa44ea29b..b2ffcb622962d0b0be21f49afd1d60edeb74b926 100644 (file)
     VALUES (#{name}, #{description}, #{createdAt}, #{updatedAt})
   </insert>
 
+  <select id="selectByQuery" parameterType="map" resultType="Group">
+    SELECT <include refid="groupColumns" />
+    FROM groups g
+    WHERE UPPER(g.name) LIKE #{query}
+    ORDER BY UPPER(g.name)
+  </select>
+
+  <select id="countByQuery" parameterType="map" resultType="int">
+    SELECT count(g.id)
+    FROM groups g
+    WHERE UPPER(g.name) LIKE #{query}
+  </select>
 </mapper>
index 5c7654c0cb9783395b0493a42d7c69d7326d613c..0b9116328d74f39d2a056f3701f0572fad679a91 100644 (file)
     <include refid="commonClauses" />
   </select>
 
+  <select id="countUsersByGroup" parameterType="long" resultType="org.sonar.core.user.GroupUserCount">
+    SELECT g.name as groupName, count(gu.user_id) as userCount
+    FROM groups g
+    LEFT JOIN groups_users gu ON gu.group_id=g.id
+    <where>
+     g.id in
+     <foreach collection="groupIds" open="(" close=")" item="id" separator=",">
+       #{id}
+     </foreach>
+   </where>
+    GROUP BY g.name
+  </select>
+
 </mapper>
index d6cb76d9dcfe1d9aea124cb0d32cf14c82e644d8..d1a5759d912dee268a4f6ddd20b3eaa6bca2af9e 100644 (file)
@@ -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 (file)
index 0000000..487e11f
--- /dev/null
@@ -0,0 +1,16 @@
+<dataset>
+
+  <groups id="100" name="sonar-administrators" description="System administrators"/>
+  <groups id="101" name="sonar-users" description="Any new users created will automatically join this group"/>
+  <groups id="102" name="sonar-reviewers" description="Reviewers"/>
+  <groups id="103" name="sonar-nobody" description="Nobody in this group"/>
+
+  <!-- user 200 is in all groups -->
+  <groups_users user_id="200" group_id="100"/>
+  <groups_users user_id="200" group_id="101"/>
+  <groups_users user_id="200" group_id="102"/>
+
+  <!-- user 201 is in users group -->
+  <groups_users user_id="201" group_id="101"/>
+
+</dataset>
index 427350b2ea61a0c2efa2ea0faa0209ec099c2d1b..eb25819e0aec21358ebd5f7fa880e3eff778b2d8 100644 (file)
@@ -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