]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-18657 Do not use Elastic Search for /api/users/search
authorMathieu Suen <mathieu.suen@sonarsource.com>
Wed, 8 Mar 2023 15:59:09 +0000 (16:59 +0100)
committersonartech <sonartech@sonarsource.com>
Wed, 22 Mar 2023 20:04:07 +0000 (20:04 +0000)
server/sonar-db-dao/src/it/java/org/sonar/db/user/UserDaoIT.java
server/sonar-db-dao/src/main/java/org/sonar/db/user/UserDao.java
server/sonar-db-dao/src/main/java/org/sonar/db/user/UserMapper.java
server/sonar-db-dao/src/main/java/org/sonar/db/user/UserQuery.java [new file with mode: 0644]
server/sonar-db-dao/src/main/resources/org/sonar/db/user/UserMapper.xml
server/sonar-webserver-webapi/src/it/java/org/sonar/server/user/ws/AnonymizeActionIT.java
server/sonar-webserver-webapi/src/it/java/org/sonar/server/user/ws/SearchActionIT.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/SearchAction.java

index ffca27d756bb345e80375e4f9cf09609c0efb67f..bb0be8dfd00ac3085d5ae4946fa51a350d824d39 100644 (file)
  */
 package org.sonar.db.user;
 
+import com.tngtech.java.junit.dataprovider.DataProvider;
 import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.sonar.api.impl.utils.TestSystem2;
-import org.sonar.api.user.UserQuery;
 import org.sonar.api.utils.DateUtils;
 import org.sonar.db.DatabaseUtils;
 import org.sonar.db.DbClient;
@@ -39,7 +45,10 @@ import org.sonar.db.scim.ScimUserDto;
 
 import static java.util.Arrays.asList;
 import static java.util.Collections.emptyList;
+import static java.util.Collections.emptyMap;
+import static java.util.Collections.emptySet;
 import static java.util.Collections.singletonList;
+import static java.util.stream.Collectors.toMap;
 import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.groups.Tuple.tuple;
@@ -146,7 +155,7 @@ public class UserDaoIT {
     db.users().insertUser(user -> user.setLogin("user").setName("User"));
     db.users().insertUser(user -> user.setLogin("inactive_user").setName("Disabled").setActive(false));
 
-    List<UserDto> users = underTest.selectUsers(session, UserQuery.builder().includeDeactivated().build());
+    List<UserDto> users = underTest.selectUsers(session, UserQuery.builder().build());
 
     assertThat(users).hasSize(2);
   }
@@ -156,39 +165,48 @@ public class UserDaoIT {
     db.users().insertUser(user -> user.setLogin("user").setName("User"));
     db.users().insertUser(user -> user.setLogin("inactive_user").setName("Disabled").setActive(false));
 
-    List<UserDto> users = underTest.selectUsers(session, UserQuery.ALL_ACTIVES);
+    List<UserDto> users = underTest.selectUsers(session, UserQuery.builder().isActive(true).build());
 
     assertThat(users).extracting(UserDto::getName).containsExactlyInAnyOrder("User");
   }
 
   @Test
-  public void selectUsersByQuery_filter_by_login() {
-    db.users().insertUser(user -> user.setLogin("user").setName("User"));
-    db.users().insertUser(user -> user.setLogin("inactive_user").setName("Disabled").setActive(false));
+  public void selectUsersByQuery_whenSearchTextMatchPartOfTheLoginCaseInsensitively_findsTheRightResults() {
+    db.users().insertUser(user -> user.setLogin("tata"));
+    UserDto userToFind = db.users().insertUser(user -> user.setLogin("simon"));
+    UserDto userToFind2 = db.users().insertUser(user -> user.setLogin("ToSimonTo"));
 
-    List<UserDto> users = underTest.selectUsers(session, UserQuery.builder().logins("user", "john").build());
+    UserQuery query = UserQuery.builder().searchText("Simon").build();
+    List<UserDto> users = underTest.selectUsers(session, query);
 
-    assertThat(users).extracting(UserDto::getName).containsExactlyInAnyOrder("User");
+    assertThat(users).usingRecursiveFieldByFieldElementComparator().containsOnly(userToFind, userToFind2);
+    assertThat(underTest.countUsers(session, query)).isEqualTo(2);
   }
 
   @Test
-  public void selectUsersByQuery_search_by_login_text() {
-    db.users().insertUser(user -> user.setLogin("user").setName("User"));
-    db.users().insertUser(user -> user.setLogin("sbrandhof").setName("Simon Brandhof"));
+  public void selectUsersByQuery_whenSearchTextMatchPartOfTheNameCaseInsensitively_findsTheRightResults() {
+    db.users().insertUser(user -> user.setName("tata"));
+    UserDto userToFind = db.users().insertUser(user -> user.setName("simon"));
+    UserDto userToFind2 = db.users().insertUser(user -> user.setName("ToSimonTo"));
 
-    List<UserDto> users = underTest.selectUsers(session, UserQuery.builder().searchText("sbr").build());
+    UserQuery query = UserQuery.builder().searchText("Simon").build();
+    List<UserDto> users = underTest.selectUsers(session, query);
 
-    assertThat(users).extracting(UserDto::getLogin).containsExactlyInAnyOrder("sbrandhof");
+    assertThat(users).usingRecursiveFieldByFieldElementComparator().containsOnly(userToFind, userToFind2);
+    assertThat(underTest.countUsers(session, query)).isEqualTo(2);
   }
 
   @Test
-  public void selectUsersByQuery_search_by_name_text() {
-    db.users().insertUser(user -> user.setLogin("user").setName("User"));
-    db.users().insertUser(user -> user.setLogin("sbrandhof").setName("Simon Brandhof"));
+  public void selectUsersByQuery_whenSearchTextMatchPartOfTheEmailCaseInsensitively_findsTheRightResults() {
+    db.users().insertUser(user -> user.setEmail("user@user.com"));
+    UserDto userToFind = db.users().insertUser(user -> user.setEmail("simon@brandhof.com"));
+    UserDto userToFind2 = db.users().insertUser(user -> user.setEmail("tagadasimon2@brandhof.com"));
 
-    List<UserDto> users = underTest.selectUsers(session, UserQuery.builder().searchText("Simon").build());
+    UserQuery query = UserQuery.builder().searchText("Simon").build();
+    List<UserDto> users = underTest.selectUsers(session, query);
 
-    assertThat(users).extracting(UserDto::getLogin).containsExactlyInAnyOrder("sbrandhof");
+    assertThat(users).usingRecursiveFieldByFieldElementComparator().containsOnly(userToFind, userToFind2);
+    assertThat(underTest.countUsers(session, query)).isEqualTo(2);
   }
 
   @Test
@@ -203,6 +221,47 @@ public class UserDaoIT {
     assertThat(users).isEmpty();
   }
 
+  @DataProvider
+  public static Object[][] paginationTestCases() {
+    return new Object[][] {
+      {100, 1, 5},
+      {100, 3, 18},
+      {2075, 41, 50},
+      {0, 2, 5},
+    };
+  }
+
+  @Test
+  @UseDataProvider("paginationTestCases")
+  public void selectUsers_whenUsingPagination_findsTheRightResults(int numberOfUsersToGenerate, int offset, int limit) {
+    Map<String, UserDto> allUsers = generateUsers(numberOfUsersToGenerate);
+
+    UserQuery query = UserQuery.builder().build();
+    List<UserDto> users = underTest.selectUsers(session, query, offset, limit);
+
+    Set<UserDto> expectedUsers = getExpectedUsers(offset, limit, allUsers);
+
+    assertThat(users).usingRecursiveFieldByFieldElementComparator().containsExactlyInAnyOrderElementsOf(expectedUsers);
+    assertThat(underTest.countUsers(session, query)).isEqualTo(numberOfUsersToGenerate);
+  }
+
+  private Map<String, UserDto> generateUsers(int numberOfUsersToGenerate) {
+    if (numberOfUsersToGenerate == 0) {
+      return emptyMap();
+    }
+    return IntStream.range(1000, 1000 + numberOfUsersToGenerate)
+      .mapToObj(i -> db.users().insertUser(user -> user.setLogin(i + "_user").setName(i + "_name")))
+      .collect(toMap(UserDto::getName, Function.identity()));
+  }
+
+  private static Set<UserDto> getExpectedUsers(int offset, int limit, Map<String, UserDto> allUsers) {
+    if (allUsers.isEmpty()) {
+      return emptySet();
+    }
+    return IntStream.range(1000 + (offset - 1) * limit, 1000 + offset * limit)
+      .mapToObj(i -> allUsers.get(i + "_name"))
+      .collect(Collectors.toSet());
+  }
 
   @Test
   public void insert_user_with_default_values() {
index 1949fff807108009904c20ad12530961302cd9f7..82b9a080e55aee22e2119cf35457a556dbe5f3ee 100644 (file)
@@ -28,11 +28,11 @@ import java.util.function.Consumer;
 import java.util.function.Function;
 import javax.annotation.CheckForNull;
 import javax.annotation.Nonnull;
-import org.sonar.api.user.UserQuery;
 import org.sonar.api.utils.System2;
 import org.sonar.core.util.UuidFactory;
 import org.sonar.db.Dao;
 import org.sonar.db.DbSession;
+import org.sonar.db.Pagination;
 import org.sonar.db.audit.AuditPersister;
 import org.sonar.db.audit.model.UserNewValue;
 import org.sonar.db.component.ComponentDto;
@@ -99,7 +99,15 @@ public class UserDao implements Dao {
   }
 
   public List<UserDto> selectUsers(DbSession dbSession, UserQuery query) {
-    return mapper(dbSession).selectUsers(query);
+    return mapper(dbSession).selectUsers(query, Pagination.all());
+  }
+
+  public List<UserDto> selectUsers(DbSession dbSession, UserQuery query, int offset, int limit) {
+    return mapper(dbSession).selectUsers(query, Pagination.forPage(offset).andSize(limit));
+  }
+
+  public int countUsers(DbSession dbSession, UserQuery userQuery) {
+    return mapper(dbSession).countByQuery(userQuery);
   }
 
   public List<UserTelemetryDto> selectUsersForTelemetry(DbSession dbSession) {
index 8676a8a04a2d77a0c0c03a38bdb05cbbb2b484e1..58815a63abcd128b04011ada67d3b887f16019cd 100644 (file)
@@ -23,7 +23,7 @@ import java.util.List;
 import javax.annotation.CheckForNull;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.session.ResultHandler;
-import org.sonar.api.user.UserQuery;
+import org.sonar.db.Pagination;
 
 public interface UserMapper {
 
@@ -46,7 +46,9 @@ public interface UserMapper {
   @CheckForNull
   UserDto selectUserByLogin(String login);
 
-  List<UserDto> selectUsers(UserQuery query);
+  List<UserDto> selectUsers(@Param("query") UserQuery query, @Param("pagination") Pagination pagination);
+
+  int countByQuery(@Param("query") UserQuery query);
 
   List<UserTelemetryDto> selectUsersForTelemetry();
 
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/user/UserQuery.java b/server/sonar-db-dao/src/main/java/org/sonar/db/user/UserQuery.java
new file mode 100644 (file)
index 0000000..b230372
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.db.user;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.apache.commons.lang.StringUtils;
+
+public class UserQuery {
+  private final String searchText;
+  private final Boolean isActive;
+
+  public UserQuery(@Nullable String searchText, @Nullable Boolean isActive) {
+    this.searchText = searchTextToSearchTextSql(searchText);
+    this.isActive = isActive;
+  }
+
+  private static String searchTextToSearchTextSql(@Nullable String text) {
+    String sql = null;
+    if (text != null) {
+      sql = StringUtils.replace(text, "%", "/%");
+      sql = StringUtils.replace(sql, "_", "/_");
+      sql = "%" + sql + "%";
+    }
+    return sql;
+  }
+
+  @CheckForNull
+  private String getSearchText() {
+    return searchText;
+  }
+
+  @CheckForNull
+  private Boolean isActive() {
+    return isActive;
+  }
+
+  public static UserQueryBuilder builder() {
+    return new UserQueryBuilder();
+  }
+
+  public static final class UserQueryBuilder {
+    private String searchText;
+    private Boolean isActive;
+
+    private UserQueryBuilder() {
+    }
+
+    public UserQueryBuilder searchText(@Nullable String searchText) {
+      this.searchText = searchText;
+      return this;
+    }
+
+    public UserQueryBuilder isActive(@Nullable Boolean isActive) {
+      this.isActive = isActive;
+      return this;
+    }
+
+    public UserQuery build() {
+      return new UserQuery(searchText, isActive);
+    }
+  }
+}
index bc92cc5234ac0675897abc171f176e07235c3986..bf5145c8791e100b58a3f7187ab6508c6ec8ee57 100644 (file)
         SELECT
         <include refid="userColumns"/>
         FROM users u
-        <where>
-            <if test="logins != null and logins.size() > 0">
-                u.login IN
-                <foreach item="login" index="index" collection="logins" open="(" separator="," close=")">
-                    #{login, jdbcType=VARCHAR}
-                </foreach>
-            </if>
-            <if test="includeDeactivated==false">
-                AND u.active=${_true}
+        <include refid="searchByQueryWhereClause"/>
+        ORDER BY u.login
+        limit #{pagination.pageSize,jdbcType=INTEGER} offset #{pagination.offset,jdbcType=INTEGER}
+    </select>
+
+    <select id="selectUsers" parameterType="map" resultType="User" databaseId="mssql">
+        SELECT
+            <include refid="userColumns"/>
+        FROM
+            (SELECT
+                    u.uuid,
+                    u.login,
+                    u.name,
+                    u.email,
+                    u.active,
+                    u.scm_accounts,
+                    u.salt,
+                    u.crypted_password,
+                    u.hash_method,
+                    u.external_id,
+                    u.external_login,
+                    u.external_identity_provider,
+                    u.user_local,
+                    u.reset_password,
+                    u.homepage_type,
+                    u.homepage_parameter,
+                    u.last_connection_date,
+                    u.last_sonarlint_connection,
+                    u.created_at,
+                    u.updated_at
+            FROM users u
+            <include refid="searchByQueryWhereClause"/>
+            ORDER BY u.login
+            offset #{pagination.offset} rows
+            fetch next #{pagination.pageSize,jdbcType=INTEGER} rows only
+            ) u
+    </select>
+
+    <select id="selectUsers" parameterType="map" resultType="User" databaseId="oracle">
+        SELECT
+            <include refid="userColumns"/>
+        FROM
+            (SELECT rownum as rn, t.* from (
+                SELECT
+                    u.uuid,
+                    u.login,
+                    u.name,
+                    u.email,
+                    u.active,
+                    u.scm_accounts,
+                    u.salt,
+                    u.crypted_password,
+                    u.hash_method,
+                    u.external_id,
+                    u.external_login,
+                    u.external_identity_provider,
+                    u.user_local,
+                    u.reset_password,
+                    u.homepage_type,
+                    u.homepage_parameter,
+                    u.last_connection_date,
+                    u.last_sonarlint_connection,
+                    u.created_at,
+                    u.updated_at
+                FROM users u
+                <include refid="searchByQueryWhereClause"/>
+                ORDER BY u.login ASC
+                ) t
+            ) u
+        WHERE
+        u.rn BETWEEN #{pagination.startRowNumber,jdbcType=INTEGER} and #{pagination.endRowNumber,jdbcType=INTEGER}
+             ORDER BY u.rn ASC
+    </select>
+
+    <select id="countByQuery" parameterType="map" resultType="int">
+        SELECT count(1)
+            FROM users u
+        <include refid="searchByQueryWhereClause"/>
+    </select>
+
+    <sql id="searchByQueryWhereClause">
+             <where>
+            <if test="query.isActive != null">
+                u.active=#{query.isActive, jdbcType=BOOLEAN}
             </if>
-            <if test="searchText != null">
-                AND (u.login LIKE #{searchTextSql, jdbcType=VARCHAR} ESCAPE '/' OR u.name LIKE #{searchTextSql, jdbcType=VARCHAR} ESCAPE '/')
+            <if test="query.searchText != null">
+                AND (
+                    (lower(u.login) LIKE lower(#{query.searchText, jdbcType=VARCHAR}) ESCAPE '/')
+                    OR (lower(u.name) LIKE lower(#{query.searchText, jdbcType=VARCHAR}) ESCAPE '/')
+                    OR (lower(u.email) LIKE lower(#{query.searchText, jdbcType=VARCHAR}) ESCAPE '/')
+                )
             </if>
         </where>
-        ORDER BY u.name
-    </select>
+
+    </sql>
 
     <select id="selectUsersForTelemetry" parameterType="map" resultType="UserTelemetry">
         SELECT
index e2114c603d33326d9b2b819ba1c13bcb8efd7917..940dfb7c1568e476cb7dea02db47d01ef062baa0 100644 (file)
@@ -27,11 +27,11 @@ import org.elasticsearch.search.builder.SearchSourceBuilder;
 import org.junit.Rule;
 import org.junit.Test;
 import org.sonar.api.impl.utils.AlwaysIncreasingSystem2;
-import org.sonar.api.user.UserQuery;
 import org.sonar.api.utils.System2;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbTester;
 import org.sonar.db.user.UserDto;
+import org.sonar.db.user.UserQuery;
 import org.sonar.server.es.EsClient;
 import org.sonar.server.es.EsTester;
 import org.sonar.server.es.EsUtils;
@@ -187,7 +187,7 @@ public class AnonymizeActionIT {
   }
 
   private void verifyThatUserIsAnonymized(String uuid) {
-    List<UserDto> users = dbClient.userDao().selectUsers(db.getSession(), UserQuery.builder().includeDeactivated().build());
+    List<UserDto> users = dbClient.userDao().selectUsers(db.getSession(), UserQuery.builder().isActive(false).build());
     assertThat(users).hasSize(1);
 
     UserDto anonymized = dbClient.userDao().selectByUuid(db.getSession(), uuid);
index c932f86ce340039f538807882b0770def57e3c0e..5fe1512d720a8b369132fbcb28fd5e46e3a22a24 100644 (file)
@@ -25,16 +25,12 @@ import org.junit.Rule;
 import org.junit.Test;
 import org.sonar.api.server.ws.WebService;
 import org.sonar.api.server.ws.WebService.Param;
-import org.sonar.api.utils.System2;
 import org.sonar.db.DbTester;
 import org.sonar.db.user.GroupDto;
 import org.sonar.db.user.UserDto;
-import org.sonar.server.es.EsTester;
 import org.sonar.server.issue.AvatarResolverImpl;
 import org.sonar.server.management.ManagedInstanceService;
 import org.sonar.server.tester.UserSessionRule;
-import org.sonar.server.user.index.UserIndex;
-import org.sonar.server.user.index.UserIndexer;
 import org.sonar.server.ws.WsActionTester;
 import org.sonarqube.ws.Common.Paging;
 import org.sonarqube.ws.Users.SearchWsResponse;
@@ -55,9 +51,6 @@ import static org.sonar.test.JsonAssert.assertJson;
 
 public class SearchActionIT {
 
-  @Rule
-  public EsTester es = EsTester.create();
-
   @Rule
   public UserSessionRule userSession = UserSessionRule.standalone();
 
@@ -65,9 +58,7 @@ public class SearchActionIT {
   public DbTester db = DbTester.create();
 
   private ManagedInstanceService managedInstanceService = mock(ManagedInstanceService.class);
-  private UserIndex index = new UserIndex(es.client(), System2.INSTANCE);
-  private UserIndexer userIndexer = new UserIndexer(db.getDbClient(), es.client());
-  private WsActionTester ws = new WsActionTester(new SearchAction(userSession, index, db.getDbClient(), new AvatarResolverImpl(), managedInstanceService));
+  private WsActionTester ws = new WsActionTester(new SearchAction(userSession, db.getDbClient(), new AvatarResolverImpl(), managedInstanceService));
 
   @Test
   public void search_for_all_active_users() {
@@ -75,7 +66,6 @@ public class SearchActionIT {
     UserDto user2 = db.users().insertUser();
     UserDto user3 = db.users().insertUser(u -> u.setActive(false));
 
-    userIndexer.indexAll();
     userSession.logIn();
 
     SearchWsResponse response = ws.newRequest()
@@ -92,7 +82,6 @@ public class SearchActionIT {
   public void search_deactivated_users() {
     UserDto user1 = db.users().insertUser(u -> u.setActive(false));
     UserDto user2 = db.users().insertUser(u -> u.setActive(true));
-    userIndexer.indexAll();
     userSession.logIn();
 
     SearchWsResponse response = ws.newRequest()
@@ -114,7 +103,6 @@ public class SearchActionIT {
       .setEmail("user@mail.com")
       .setLocal(true)
       .setScmAccounts(singletonList("user1")));
-    userIndexer.indexAll();
 
     assertThat(ws.newRequest()
       .setParam("q", "user-%_%-")
@@ -136,7 +124,6 @@ public class SearchActionIT {
   @Test
   public void return_avatar() {
     UserDto user = db.users().insertUser(u -> u.setEmail("john@doe.com"));
-    userIndexer.indexAll();
     userSession.logIn();
 
     SearchWsResponse response = ws.newRequest()
@@ -152,7 +139,6 @@ public class SearchActionIT {
     UserDto nonManagedUser = db.users().insertUser(u -> u.setEmail("john@doe.com"));
     UserDto managedUser = db.users().insertUser(u -> u.setEmail("externalUser@doe.com"));
     mockUsersAsManaged(managedUser.getUuid());
-    userIndexer.indexAll();
     userSession.logIn().setSystemAdministrator();
 
     SearchWsResponse response = ws.newRequest()
@@ -169,7 +155,7 @@ public class SearchActionIT {
   @Test
   public void return_scm_accounts() {
     UserDto user = db.users().insertUser(u -> u.setScmAccounts(asList("john1", "john2")));
-    userIndexer.indexAll();
+
     userSession.logIn();
 
     SearchWsResponse response = ws.newRequest()
@@ -185,7 +171,6 @@ public class SearchActionIT {
     UserDto user = db.users().insertUser();
     db.users().insertToken(user);
     db.users().insertToken(user);
-    userIndexer.indexAll();
 
     userSession.logIn().setSystemAdministrator();
     assertThat(ws.newRequest()
@@ -203,7 +188,6 @@ public class SearchActionIT {
   @Test
   public void return_email_only_when_system_administer() {
     UserDto user = db.users().insertUser();
-    userIndexer.indexAll();
 
     userSession.logIn().setSystemAdministrator();
     assertThat(ws.newRequest()
@@ -221,7 +205,6 @@ public class SearchActionIT {
   @Test
   public void return_user_not_having_email() {
     UserDto user = db.users().insertUser(u -> u.setEmail(null));
-    userIndexer.indexAll();
     userSession.logIn().setSystemAdministrator();
 
     SearchWsResponse response = ws.newRequest()
@@ -240,7 +223,6 @@ public class SearchActionIT {
     GroupDto group3 = db.users().insertGroup("group3");
     db.users().insertMember(group1, user);
     db.users().insertMember(group2, user);
-    userIndexer.indexAll();
 
     userSession.logIn().setSystemAdministrator();
     assertThat(ws.newRequest()
@@ -258,7 +240,6 @@ public class SearchActionIT {
   @Test
   public void return_external_information() {
     UserDto user = db.users().insertUser();
-    userIndexer.indexAll();
     userSession.logIn().setSystemAdministrator();
 
     SearchWsResponse response = ws.newRequest()
@@ -272,7 +253,6 @@ public class SearchActionIT {
   @Test
   public void return_external_identity_only_when_system_administer() {
     UserDto user = db.users().insertUser();
-    userIndexer.indexAll();
 
     userSession.logIn().setSystemAdministrator();
     assertThat(ws.newRequest()
@@ -293,7 +273,6 @@ public class SearchActionIT {
     db.users().insertToken(user);
     GroupDto group = db.users().insertGroup();
     db.users().insertMember(group, user);
-    userIndexer.indexAll();
     userSession.anonymous();
 
     SearchWsResponse response = ws.newRequest()
@@ -309,7 +288,6 @@ public class SearchActionIT {
     UserDto userWithLastConnectionDate = db.users().insertUser();
     db.users().updateLastConnectionDate(userWithLastConnectionDate, 10_000_000_000L);
     UserDto userWithoutLastConnectionDate = db.users().insertUser();
-    userIndexer.indexAll();
     userSession.logIn().setSystemAdministrator();
 
     SearchWsResponse response = ws.newRequest()
@@ -331,7 +309,6 @@ public class SearchActionIT {
     GroupDto group = db.users().insertGroup();
     db.users().insertMember(group, user);
     UserDto otherUser = db.users().insertUser();
-    userIndexer.indexAll();
 
     userSession.logIn(user);
     assertThat(ws.newRequest().setParam("q", user.getLogin())
@@ -350,11 +327,23 @@ public class SearchActionIT {
         tuple(user.getLogin(), user.getName(), false, false, true, true, true, false, false, false));
   }
 
+  @Test
+  public void search_whenNoPagingInformationProvided_setsDefaultValues() {
+    userSession.logIn();
+    IntStream.rangeClosed(0, 9).forEach(i -> db.users().insertUser(u -> u.setLogin("user-" + i).setName("User " + i)));
+
+    SearchWsResponse response = ws.newRequest()
+      .executeProtobuf(SearchWsResponse.class);
+
+    assertThat(response.getPaging().getTotal()).isEqualTo(10);
+    assertThat(response.getPaging().getPageIndex()).isEqualTo(1);
+    assertThat(response.getPaging().getPageSize()).isEqualTo(50);
+  }
+
   @Test
   public void search_with_paging() {
     userSession.logIn();
     IntStream.rangeClosed(0, 9).forEach(i -> db.users().insertUser(u -> u.setLogin("user-" + i).setName("User " + i)));
-    userIndexer.indexAll();
 
     SearchWsResponse response = ws.newRequest()
       .setParam(Param.PAGE_SIZE, "5")
@@ -412,7 +401,6 @@ public class SearchActionIT {
     db.users().insertToken(simon);
     db.users().insertToken(simon);
     db.users().insertToken(fmallet);
-    userIndexer.indexAll();
     userSession.logIn().setSystemAdministrator();
 
     String response = ws.newRequest().execute().getInput();
index 571e794f1f919a1cdd4b23143e8b6fdd7813056c..dc463de3d68a0c4c103344c90c48067fd00c8d1f 100644 (file)
@@ -36,20 +36,18 @@ import org.sonar.api.utils.Paging;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.user.UserDto;
+import org.sonar.db.user.UserQuery;
 import org.sonar.server.es.SearchOptions;
-import org.sonar.server.es.SearchResult;
 import org.sonar.server.issue.AvatarResolver;
 import org.sonar.server.management.ManagedInstanceService;
 import org.sonar.server.user.UserSession;
-import org.sonar.server.user.index.UserDoc;
-import org.sonar.server.user.index.UserIndex;
-import org.sonar.server.user.index.UserQuery;
 import org.sonarqube.ws.Users;
 import org.sonarqube.ws.Users.SearchWsResponse;
 
 import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Strings.emptyToNull;
+import static java.util.Comparator.comparing;
 import static java.lang.Boolean.TRUE;
 import static java.util.Optional.ofNullable;
 import static org.sonar.api.server.ws.WebService.Param.PAGE;
@@ -69,15 +67,13 @@ public class SearchAction implements UsersWsAction {
   private static final int MAX_PAGE_SIZE = 500;
 
   private final UserSession userSession;
-  private final UserIndex userIndex;
   private final DbClient dbClient;
   private final AvatarResolver avatarResolver;
   private final ManagedInstanceService managedInstanceService;
 
-  public SearchAction(UserSession userSession, UserIndex userIndex, DbClient dbClient, AvatarResolver avatarResolver,
+  public SearchAction(UserSession userSession, DbClient dbClient, AvatarResolver avatarResolver,
     ManagedInstanceService managedInstanceService) {
     this.userSession = userSession;
-    this.userIndex = userIndex;
     this.dbClient = dbClient;
     this.avatarResolver = avatarResolver;
     this.managedInstanceService = managedInstanceService;
@@ -99,6 +95,7 @@ public class SearchAction implements UsersWsAction {
         "Field 'lastConnectionDate' is only updated every hour, so it may not be accurate, for instance when a user authenticates many times in less than one hour.")
       .setSince("3.6")
       .setChangelog(
+        new Change("10.0", "'q' parameter values is now always performing a case insensitive match"),
         new Change("10.0", "Response includes 'managed' field."),
         new Change("9.7", "New parameter 'deactivated' to optionally search for deactivated users"),
         new Change("7.7", "New field 'lastConnectionDate' is added to response"),
@@ -114,17 +111,7 @@ public class SearchAction implements UsersWsAction {
     action.createParam(TEXT_QUERY)
       .setMinimumLength(2)
       .setDescription("Filter on login, name and email.<br />" +
-        "This parameter can either be case sensitive and perform an exact match, or case insensitive and perform a partial match (contains), depending on the scenario:<br />" +
-        "<ul>" +
-        "  <li>" +
-        "    If the search query is <em>less or equal to 15 characters</em>, then the query is <em>case insensitive</em>, and will match any login, name, or email, that " +
-        "    <em>contains</em> the search query." +
-        "  </li>" +
-        "  <li>" +
-        "    If the search query is <em>greater than 15 characters</em>, then the query becomes <em>case sensitive</em>, and will match any login, name, or email, that " +
-        "    <em>exactly matches</em> the search query." +
-        "  </li>" +
-        "</ul>");
+        "This parameter can either perform an exact match, or a partial match (contains), it is case insensitive.");
     action.createParam(DEACTIVATED_PARAM)
       .setSince("9.7")
       .setDescription("Return deactivated users instead of active users")
@@ -140,15 +127,16 @@ public class SearchAction implements UsersWsAction {
   }
 
   private Users.SearchWsResponse doHandle(SearchRequest request) {
-    SearchOptions options = new SearchOptions().setPage(request.getPage(), request.getPageSize());
-    SearchResult<UserDoc> result = userIndex.search(UserQuery.builder().setActive(!request.isDeactivated()).setTextQuery(request.getQuery()).build(), options);
+    UserQuery userQuery = buildUserQuery(request);
     try (DbSession dbSession = dbClient.openSession(false)) {
-      List<String> logins = result.getDocs().stream().map(UserDoc::login).collect(toList());
+      List<UserDto> users = fetchUsersAndSortByLogin(request, dbSession, userQuery);
+      int totalUsers = dbClient.userDao().countUsers(dbSession, userQuery);
+
+      List<String> logins = users.stream().map(UserDto::getLogin).collect(toList());
       Multimap<String, String> groupsByLogin = dbClient.groupMembershipDao().selectGroupsByLogins(dbSession, logins);
-      List<UserDto> users = dbClient.userDao().selectByOrderedLogins(dbSession, logins);
       Map<String, Integer> tokenCountsByLogin = dbClient.userTokenDao().countTokensByUsers(dbSession, users);
       Map<String, Boolean> userUuidToIsManaged = managedInstanceService.getUserUuidToManaged(dbSession, getUserUuids(users));
-      Paging paging = forPageIndex(request.getPage()).withPageSize(request.getPageSize()).andTotal((int) result.getTotal());
+      Paging paging = forPageIndex(request.getPage()).withPageSize(request.getPageSize()).andTotal(totalUsers);
       return buildResponse(users, groupsByLogin, tokenCountsByLogin, userUuidToIsManaged, paging);
     }
   }
@@ -157,6 +145,20 @@ public class SearchAction implements UsersWsAction {
     return users.stream().map(UserDto::getUuid).collect(Collectors.toSet());
   }
 
+  private static UserQuery buildUserQuery(SearchRequest request) {
+    return UserQuery.builder()
+      .isActive(!request.isDeactivated())
+      .searchText(request.getQuery())
+      .build();
+  }
+
+  private List<UserDto> fetchUsersAndSortByLogin(SearchRequest request, DbSession dbSession, UserQuery userQuery) {
+    return dbClient.userDao().selectUsers(dbSession, userQuery, request.getPage(), request.getPageSize())
+      .stream()
+      .sorted(comparing(UserDto::getLogin))
+      .toList();
+  }
+
   private SearchWsResponse buildResponse(List<UserDto> users, Multimap<String, String> groupsByLogin, Map<String, Integer> tokenCountsByLogin,
     Map<String, Boolean> userUuidToIsManaged, Paging paging) {
     SearchWsResponse.Builder responseBuilder = newBuilder();
@@ -220,12 +222,10 @@ public class SearchAction implements UsersWsAction {
       this.deactivated = builder.deactivated;
     }
 
-    @CheckForNull
     public Integer getPage() {
       return page;
     }
 
-    @CheckForNull
     public Integer getPageSize() {
       return pageSize;
     }
@@ -254,12 +254,12 @@ public class SearchAction implements UsersWsAction {
       // enforce factory method use
     }
 
-    public Builder setPage(@Nullable Integer page) {
+    public Builder setPage(Integer page) {
       this.page = page;
       return this;
     }
 
-    public Builder setPageSize(@Nullable Integer pageSize) {
+    public Builder setPageSize(Integer pageSize) {
       this.pageSize = pageSize;
       return this;
     }