package org.sonar.server.user.db;
+import java.util.List;
+import javax.annotation.CheckForNull;
import org.sonar.api.utils.System2;
import org.sonar.core.persistence.DaoComponent;
import org.sonar.core.persistence.DbSession;
import org.sonar.core.user.UserMapper;
import org.sonar.server.exceptions.NotFoundException;
-import javax.annotation.CheckForNull;
-
-import java.util.List;
-
public class UserDao extends org.sonar.core.user.UserDao implements DaoComponent {
public UserDao(MyBatis mybatis, System2 system2) {
package org.sonar.server.user.ws;
+import com.google.common.base.Function;
+import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
import org.sonar.api.server.ws.Request;
import org.sonar.api.server.ws.Response;
import org.sonar.api.server.ws.WebService;
import org.sonar.api.server.ws.WebService.Param;
import org.sonar.api.utils.text.JsonWriter;
+import org.sonar.core.persistence.DbSession;
+import org.sonar.server.db.DbClient;
import org.sonar.server.es.SearchOptions;
import org.sonar.server.es.SearchResult;
import org.sonar.server.user.index.UserDoc;
import org.sonar.server.user.index.UserIndex;
-import javax.annotation.Nullable;
-
-import java.util.List;
-import java.util.Set;
-
public class SearchAction implements UsersWsAction {
private static final String FIELD_LOGIN = "login";
private static final String FIELD_NAME = "name";
private static final String FIELD_EMAIL = "email";
private static final String FIELD_SCM_ACCOUNTS = "scmAccounts";
- private static final Set<String> FIELDS = ImmutableSet.of(FIELD_LOGIN, FIELD_NAME, FIELD_EMAIL, FIELD_SCM_ACCOUNTS);
+ private static final String FIELD_GROUPS_COUNT = "groupsCount";
+ private static final Set<String> FIELDS = ImmutableSet.of(FIELD_LOGIN, FIELD_NAME, FIELD_EMAIL, FIELD_SCM_ACCOUNTS, FIELD_GROUPS_COUNT);
private final UserIndex userIndex;
+ private final DbClient dbClient;
- public SearchAction(UserIndex userIndex) {
+ public SearchAction(UserIndex userIndex, DbClient dbClient) {
this.userIndex = userIndex;
+ this.dbClient = dbClient;
}
@Override
List<String> fields = request.paramAsStrings(Param.FIELDS);
SearchResult<UserDoc> result = userIndex.search(request.param(Param.TEXT_QUERY), options);
+ Map<String, Integer> groupsByLogin = Maps.newHashMap();
+ DbSession session = dbClient.openSession(false);
+ try {
+ Collection<String> logins = Collections2.transform(result.getDocs(), new Function<UserDoc, String>() {
+ @Override
+ public String apply(@Nonnull UserDoc input) {
+ return input.login();
+ }
+ });
+ groupsByLogin = dbClient.groupMembershipDao().countGroupsByLogins(session, logins);
+ } finally {
+ session.close();
+ }
+
JsonWriter json = response.newJsonWriter().beginObject();
options.writeJson(json, result.getTotal());
- writeUsers(json, result, fields);
+ writeUsers(json, result, fields, groupsByLogin);
json.endObject().close();
}
- private void writeUsers(JsonWriter json, SearchResult<UserDoc> result, @Nullable List<String> fields) {
+ private void writeUsers(JsonWriter json, SearchResult<UserDoc> result, @Nullable List<String> fields, Map<String, Integer> groupsByLogin) {
json.name("users").beginArray();
for (UserDoc user : result.getDocs()) {
writeIfNeeded(json, user.login(), FIELD_LOGIN, fields);
writeIfNeeded(json, user.name(), FIELD_NAME, fields);
writeIfNeeded(json, user.email(), FIELD_EMAIL, fields);
+ writeIfNeeded(json, groupsByLogin.get(user.login()), FIELD_GROUPS_COUNT, fields);
if (fieldIsWanted(FIELD_SCM_ACCOUNTS, fields)) {
json.name(FIELD_SCM_ACCOUNTS)
.beginArray()
}
}
+ private void writeIfNeeded(JsonWriter json, Integer value, String field, @Nullable List<String> fields) {
+ if (fieldIsWanted(field, fields)) {
+ json.prop(field, value);
+ }
+ }
+
private boolean fieldIsWanted(String field, @Nullable List<String> fields) {
return fields == null || fields.isEmpty() || fields.contains(field);
}
package org.sonar.server.user.ws;
+import com.google.common.collect.Lists;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.sonar.api.config.Settings;
import org.sonar.api.server.ws.WebService;
+import org.sonar.api.utils.System2;
+import org.sonar.core.persistence.DbSession;
+import org.sonar.core.persistence.DbTester;
+import org.sonar.core.user.GroupDto;
+import org.sonar.core.user.GroupMembershipDao;
+import org.sonar.core.user.UserDto;
+import org.sonar.core.user.UserGroupDto;
+import org.sonar.server.db.DbClient;
import org.sonar.server.es.EsTester;
+import org.sonar.server.user.db.GroupDao;
+import org.sonar.server.user.db.UserDao;
+import org.sonar.server.user.db.UserGroupDao;
import org.sonar.server.user.index.UserDoc;
import org.sonar.server.user.index.UserIndex;
import org.sonar.server.user.index.UserIndexDefinition;
import org.sonar.server.ws.WsTester;
-import java.util.Arrays;
-
import static org.assertj.core.api.Assertions.assertThat;
public class SearchActionTest {
+ @ClassRule
+ public static final DbTester dbTester = new DbTester();
+
@ClassRule
public static final EsTester esTester = new EsTester().addDefinitions(new UserIndexDefinition(new Settings()));
UserIndex index;
+ DbClient dbClient;
+
+ DbSession session;
+
@Before
public void setUp() {
+ dbTester.truncateTables();
esTester.truncateIndices();
+ dbClient = new DbClient(dbTester.database(), dbTester.myBatis(),
+ new GroupMembershipDao(dbTester.myBatis()),
+ new UserDao(dbTester.myBatis(), new System2()),
+ new GroupDao(new System2()),
+ new UserGroupDao());
+ session = dbClient.openSession(false);
+
index = new UserIndex(esTester.client());
- tester = new WsTester(new UsersWs(new SearchAction(index)));
+ tester = new WsTester(new UsersWs(new SearchAction(index, dbClient)));
controller = tester.controller("api/users");
+ }
+ @After
+ public void tearDown() {
+ session.close();
}
@Test
.contains("login")
.contains("name")
.contains("email")
- .contains("scmAccounts");
+ .contains("scmAccounts")
+ .contains("groupsCount");
assertThat(tester.newGetRequest("api/users", "search").setParam("f", "").execute().outputAsString())
.contains("login")
.contains("name")
.contains("email")
- .contains("scmAccounts");
+ .contains("scmAccounts")
+ .contains("groupsCount");
assertThat(tester.newGetRequest("api/users", "search").setParam("f", "login").execute().outputAsString())
.contains("login")
.doesNotContain("name")
.doesNotContain("email")
- .doesNotContain("scmAccounts");
+ .doesNotContain("scmAccounts")
+ .doesNotContain("groupsCount");
assertThat(tester.newGetRequest("api/users", "search").setParam("f", "scmAccounts").execute().outputAsString())
.doesNotContain("login")
.doesNotContain("name")
.doesNotContain("email")
- .contains("scmAccounts");
+ .contains("scmAccounts")
+ .doesNotContain("groupsCount");
+
+ assertThat(tester.newGetRequest("api/users", "search").setParam("f", "groupsCount").execute().outputAsString())
+ .doesNotContain("login")
+ .doesNotContain("name")
+ .doesNotContain("email")
+ .doesNotContain("scmAccounts")
+ .contains("groupsCount");
}
- private void injectUsers(int numberOfUsers) throws Exception {
+ @Test
+ public void search_with_groups() throws Exception {
+ List<UserDto> users = injectUsers(1);
+
+ GroupDto group1 = dbClient.groupDao().insert(session, new GroupDto().setName("sonar-users"));
+ GroupDto group2 = dbClient.groupDao().insert(session, new GroupDto().setName("sonar-admins"));
+ dbClient.userGroupDao().insert(session, new UserGroupDto().setGroupId(group1.getId()).setUserId(users.get(0).getId()));
+ dbClient.userGroupDao().insert(session, new UserGroupDto().setGroupId(group2.getId()).setUserId(users.get(0).getId()));
+ session.commit();
+
+ tester.newGetRequest("api/users", "search").execute().assertJson(getClass(), "user_with_groups.json");
+ }
+
+ private List<UserDto> injectUsers(int numberOfUsers) throws Exception {
+ List<UserDto> userDtos = Lists.newArrayList();
long createdAt = System.currentTimeMillis();
UserDoc[] users = new UserDoc[numberOfUsers];
for (int index = 0; index < numberOfUsers; index++) {
+ String email = String.format("user-%d@mail.com", index);
+ String login = String.format("user-%d", index);
+ String name = String.format("User %d", index);
+ List<String> scmAccounts = Arrays.asList(String.format("user-%d", index));
+
+ userDtos.add(dbClient.userDao().insert(session, new UserDto()
+ .setActive(true)
+ .setCreatedAt(createdAt)
+ .setEmail(email)
+ .setLogin(login)
+ .setName(name)
+ .setScmAccounts(scmAccounts)
+ .setUpdatedAt(createdAt)));
+
users[index] = new UserDoc()
.setActive(true)
.setCreatedAt(createdAt)
- .setEmail(String.format("user-%d@mail.com", index))
- .setLogin(String.format("user-%d", index))
- .setName(String.format("User %d", index))
- .setScmAccounts(Arrays.asList(String.format("user-%d", index)))
+ .setEmail(email)
+ .setLogin(login)
+ .setName(name)
+ .setScmAccounts(scmAccounts)
.setUpdatedAt(createdAt);
}
+ session.commit();
esTester.putDocuments(UserIndexDefinition.INDEX, UserIndexDefinition.TYPE_USER, users);
+ return userDtos;
}
}
import org.junit.Test;
import org.sonar.api.i18n.I18n;
import org.sonar.api.server.ws.WebService;
+import org.sonar.server.db.DbClient;
import org.sonar.server.tester.UserSessionRule;
import org.sonar.server.user.UserUpdater;
import org.sonar.server.user.index.UserIndex;
new CurrentAction(userSessionRule),
new DeactivateAction(mock(UserIndex.class), mock(UserUpdater.class), userSessionRule),
new ChangePasswordAction(mock(UserUpdater.class), userSessionRule),
- new SearchAction(mock(UserIndex.class))));
+ new SearchAction(mock(UserIndex.class), mock(DbClient.class))));
controller = tester.controller("api/users");
}
"email": "user-0@mail.com",
"scmAccounts": [
"user-0"
- ]
+ ],
+ "groupsCount": 0
},
{
"login": "user-1",
"email": "user-1@mail.com",
"scmAccounts": [
"user-1"
- ]
+ ],
+ "groupsCount": 0
},
{
"login": "user-2",
"email": "user-2@mail.com",
"scmAccounts": [
"user-2"
- ]
+ ],
+ "groupsCount": 0
},
{
"login": "user-3",
"email": "user-3@mail.com",
"scmAccounts": [
"user-3"
- ]
+ ],
+ "groupsCount": 0
},
{
"login": "user-4",
"email": "user-4@mail.com",
"scmAccounts": [
"user-4"
- ]
+ ],
+ "groupsCount": 0
}
]
}
--- /dev/null
+{
+ "p": 1,
+ "ps": 50,
+ "total": 1,
+ "users": [
+ {
+ "login": "user-0",
+ "name": "User 0",
+ "email": "user-0@mail.com",
+ "scmAccounts": [
+ "user-0"
+ ],
+ "groupsCount": 2
+ }
+ ]
+}
import org.sonar.core.persistence.DaoUtils;
import org.sonar.core.persistence.DbSession;
import org.sonar.core.persistence.MyBatis;
+import org.sonar.core.util.NonNullInputFunction;
public class GroupMembershipDao implements DaoComponent {
return userCounts;
}
});
+
+ return result;
+ }
+
+ public Map<String, Integer> countGroupsByLogins(final DbSession session, Collection<String> logins) {
+ final Map<String, Integer> result = Maps.newHashMap();
+ DaoUtils.executeLargeInputs(logins, new NonNullInputFunction<List<String>, List<UserGroupCount>>() {
+ @Override
+ protected List<UserGroupCount> doApply(List<String> input) {
+ List<UserGroupCount> groupCounts = mapper(session).countGroupsByLogins(input);
+ for (UserGroupCount count : groupCounts) {
+ result.put(count.login(), count.groupCount());
+ }
+ return groupCounts;
+ }
+ });
+
return result;
}
int countGroups(Map<String, Object> params);
List<GroupUserCount> countUsersByGroup(@Param("groupIds") List<Long> groupIds);
+
+ List<UserGroupCount> countGroupsByLogins(@Param("logins") List<String> logins);
}
--- /dev/null
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.core.user;
+
+public class UserGroupCount {
+
+ private String login;
+ private int groupCount;
+
+ public String login() {
+ return login;
+ }
+
+ public int groupCount() {
+ return groupCount;
+ }
+}
\ No newline at end of file
GROUP BY g.name
</select>
+ <select id="countGroupsByLogins" parameterType="string" resultType="org.sonar.core.user.UserGroupCount">
+ SELECT u.login as login, count(gu.user_id) as groupCount
+ FROM users u
+ LEFT JOIN groups_users gu ON gu.user_id=u.id
+ <where>
+ u.login in
+ <foreach collection="logins" open="(" close=")" item="login" separator=",">
+ #{login}
+ </foreach>
+ </where>
+ GROUP BY u.login
+ </select>
+
</mapper>
session.close();
}
}
+
+ @Test
+ public void count_groups_by_login() {
+ dbTester.prepareDbUnit(getClass(), "shared.xml");
+ DbSession session = dbTester.myBatis().openSession(false);
+
+ try {
+ assertThat(dao.countGroupsByLogins(session, Arrays.<String>asList())).isEmpty();
+ assertThat(dao.countGroupsByLogins(session, Arrays.asList("two-hundred")))
+ .containsExactly(entry("two-hundred", 3));
+ assertThat(dao.countGroupsByLogins(session, Arrays.asList("two-hundred", "two-hundred-one")))
+ .containsOnly(entry("two-hundred", 3), entry("two-hundred-one", 1));
+ assertThat(dao.countGroupsByLogins(session, Arrays.asList("two-hundred", "two-hundred-one", "two-hundred-two")))
+ .containsOnly(entry("two-hundred", 3), entry("two-hundred-one", 1), entry("two-hundred-two", 0));
+ assertThat(dao.countGroupsByLogins(session, Arrays.asList("two-hundred-two")))
+ .containsOnly(entry("two-hundred-two", 0));
+ } finally {
+ session.close();
+ }
+ }
}
<!-- user 201 is in users group -->
<groups_users user_id="201" group_id="101"/>
+ <users id="200" login="two-hundred"/>
+ <users id="201" login="two-hundred-one"/>
+ <users id="202" login="two-hundred-two"/>
+
</dataset>