]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6521 Add internal WS to list user details for batch
authorJean-Baptiste Lievremont <jean-baptiste.lievremont@sonarsource.com>
Mon, 4 May 2015 16:03:36 +0000 (18:03 +0200)
committerJean-Baptiste Lievremont <jean-baptiste.lievremont@sonarsource.com>
Tue, 5 May 2015 09:56:13 +0000 (11:56 +0200)
12 files changed:
server/sonar-server/src/main/java/org/sonar/server/batch/BatchAction.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/batch/BatchWs.java
server/sonar-server/src/main/java/org/sonar/server/batch/GlobalRepositoryAction.java
server/sonar-server/src/main/java/org/sonar/server/batch/IssuesAction.java
server/sonar-server/src/main/java/org/sonar/server/batch/ProjectRepositoryAction.java
server/sonar-server/src/main/java/org/sonar/server/batch/UsersAction.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java
server/sonar-server/src/main/java/org/sonar/server/user/index/UserIndex.java
server/sonar-server/src/test/java/org/sonar/server/batch/GlobalRepositoryActionTest.java
server/sonar-server/src/test/java/org/sonar/server/batch/IssuesActionTest.java
server/sonar-server/src/test/java/org/sonar/server/batch/ProjectRepositoryActionTest.java
server/sonar-server/src/test/java/org/sonar/server/batch/UsersActionTest.java [new file with mode: 0644]

diff --git a/server/sonar-server/src/main/java/org/sonar/server/batch/BatchAction.java b/server/sonar-server/src/main/java/org/sonar/server/batch/BatchAction.java
new file mode 100644 (file)
index 0000000..367935d
--- /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.batch;
+
+import org.sonar.server.ws.WsAction;
+
+public interface BatchAction extends WsAction {
+
+  // Marker interface
+}
index fdddd299e55dcb980f919fbb13548dae2b15b0cd..383c1847cf99a252bb067d94b231668a8f08b05f 100644 (file)
@@ -33,15 +33,11 @@ public class BatchWs implements WebService {
   public static final String API_ENDPOINT = "batch";
 
   private final BatchIndex batchIndex;
-  private final GlobalRepositoryAction globalRepositoryAction;
-  private final ProjectRepositoryAction projectRepositoryAction;
-  private final IssuesAction issuesAction;
+  private final BatchAction[] actions;
 
-  public BatchWs(BatchIndex batchIndex, GlobalRepositoryAction globalRepositoryAction, ProjectRepositoryAction projectRepositoryAction, IssuesAction issuesAction) {
+  public BatchWs(BatchIndex batchIndex, BatchAction... actions) {
     this.batchIndex = batchIndex;
-    this.globalRepositoryAction = globalRepositoryAction;
-    this.projectRepositoryAction = projectRepositoryAction;
-    this.issuesAction = issuesAction;
+    this.actions = actions;
   }
 
   @Override
@@ -52,9 +48,9 @@ public class BatchWs implements WebService {
 
     defineIndexAction(controller);
     defineFileAction(controller);
-    globalRepositoryAction.define(controller);
-    projectRepositoryAction.define(controller);
-    issuesAction.define(controller);
+    for (BatchAction action : actions) {
+      action.define(controller);
+    }
 
     controller.done();
   }
index ae6a9a06d1601c934ad1966512b5b3bbde947f39..0e19a80fce685ec69bf31f2053032193e5ee7c82 100644 (file)
@@ -22,7 +22,6 @@ package org.sonar.server.batch;
 
 import org.apache.commons.io.IOUtils;
 import org.sonar.api.server.ws.Request;
-import org.sonar.api.server.ws.RequestHandler;
 import org.sonar.api.server.ws.Response;
 import org.sonar.api.server.ws.WebService;
 import org.sonar.batch.protocol.input.GlobalRepositories;
@@ -37,7 +36,7 @@ import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.plugins.MimeTypes;
 import org.sonar.server.user.UserSession;
 
-public class GlobalRepositoryAction implements RequestHandler {
+public class GlobalRepositoryAction implements BatchAction {
 
   private final DbClient dbClient;
   private final PropertiesDao propertiesDao;
@@ -47,7 +46,8 @@ public class GlobalRepositoryAction implements RequestHandler {
     this.propertiesDao = propertiesDao;
   }
 
-  void define(WebService.NewController controller) {
+  @Override
+  public void define(WebService.NewController controller) {
     controller.createAction("global")
       .setDescription("Return metrics and global properties")
       .setSince("4.5")
index c19c985787e3d709baa6aaba81abbbdc39ca967d..a0e9bf40c67346d131c25a9ab7d4af288fe1605c 100644 (file)
@@ -22,7 +22,6 @@ package org.sonar.server.batch;
 
 import org.sonar.api.resources.Scopes;
 import org.sonar.api.server.ws.Request;
-import org.sonar.api.server.ws.RequestHandler;
 import org.sonar.api.server.ws.Response;
 import org.sonar.api.server.ws.WebService;
 import org.sonar.batch.protocol.input.BatchInput;
@@ -44,7 +43,7 @@ import java.util.Map;
 
 import static com.google.common.collect.Maps.newHashMap;
 
-public class IssuesAction implements RequestHandler {
+public class IssuesAction implements BatchAction {
 
   private static final String PARAM_KEY = "key";
 
@@ -57,7 +56,8 @@ public class IssuesAction implements RequestHandler {
     this.issueIndex = issueIndex;
   }
 
-  void define(WebService.NewController controller) {
+  @Override
+  public void define(WebService.NewController controller) {
     WebService.NewAction action = controller.createAction("issues")
       .setDescription("Return open issues")
       .setSince("5.1")
index baa4e4a703df6d7ea14bed16074c4b242aaab378..9b7557aafdf773981082218f5ef007c1d3c7db25 100644 (file)
@@ -22,13 +22,12 @@ package org.sonar.server.batch;
 
 import org.apache.commons.io.IOUtils;
 import org.sonar.api.server.ws.Request;
-import org.sonar.api.server.ws.RequestHandler;
 import org.sonar.api.server.ws.Response;
 import org.sonar.api.server.ws.WebService;
 import org.sonar.batch.protocol.input.ProjectRepositories;
 import org.sonar.server.plugins.MimeTypes;
 
-public class ProjectRepositoryAction implements RequestHandler {
+public class ProjectRepositoryAction implements BatchAction {
 
   private static final String PARAM_KEY = "key";
   private static final String PARAM_PROFILE = "profile";
@@ -40,7 +39,8 @@ public class ProjectRepositoryAction implements RequestHandler {
     this.projectReferentialsLoader = projectReferentialsLoader;
   }
 
-  void define(WebService.NewController controller) {
+  @Override
+  public void define(WebService.NewController controller) {
     WebService.NewAction action = controller.createAction("project")
       .setDescription("Return project repository")
       .setSince("4.5")
diff --git a/server/sonar-server/src/main/java/org/sonar/server/batch/UsersAction.java b/server/sonar-server/src/main/java/org/sonar/server/batch/UsersAction.java
new file mode 100644 (file)
index 0000000..520ce7e
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * 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.batch;
+
+import org.sonar.api.server.ws.Request;
+import org.sonar.api.server.ws.Response;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.batch.protocol.input.BatchInput;
+import org.sonar.core.permission.GlobalPermissions;
+import org.sonar.server.plugins.MimeTypes;
+import org.sonar.server.user.UserSession;
+import org.sonar.server.user.index.UserDoc;
+import org.sonar.server.user.index.UserIndex;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Iterator;
+import java.util.List;
+
+public class UsersAction implements BatchAction {
+
+  private static final String PARAM_LOGINS = "logins";
+
+  private final UserIndex userIndex;
+
+  public UsersAction(UserIndex userIndex) {
+    this.userIndex = userIndex;
+  }
+
+  @Override
+  public void define(WebService.NewController controller) {
+    WebService.NewAction action = controller.createAction("users")
+      .setDescription("Return user details.")
+      .setSince("5.2")
+      .setInternal(true)
+      .setHandler(this);
+
+    action
+      .createParam(PARAM_LOGINS)
+      .setRequired(true)
+      .setDescription("A comma separated list of user logins")
+      .setExampleValue("ada.lovelace,grace.hopper");
+  }
+
+  @Override
+  public void handle(Request request, Response response) throws Exception {
+    UserSession.get().checkGlobalPermission(GlobalPermissions.PREVIEW_EXECUTION);
+    List<String> logins = request.mandatoryParamAsStrings(PARAM_LOGINS);
+
+    response.stream().setMediaType(MimeTypes.PROTOBUF);
+    BatchInput.User.Builder userBuilder = BatchInput.User.newBuilder();
+    OutputStream output = response.stream().output();
+    try {
+      for (Iterator<UserDoc> userDocIterator = userIndex.selectUsersForBatch(logins); userDocIterator.hasNext();) {
+        handleUser(userDocIterator.next(), userBuilder, output);
+      }
+    } finally {
+      output.close();
+    }
+  }
+
+  private void handleUser(UserDoc user, BatchInput.User.Builder userBuilder, OutputStream out) {
+    userBuilder.setLogin(user.login())
+      .setName(user.name());
+    try {
+      userBuilder.build().writeDelimitedTo(out);
+    } catch (IOException e) {
+      throw new IllegalStateException("Unable to serialize user", e);
+    }
+    userBuilder.clear();
+  }
+}
index 2ceda78a3d4fd2e30234e5fa6940bfc856cfe346..67679ebd960f166023ae24e90c24a26bd553554e 100644 (file)
@@ -95,6 +95,7 @@ import org.sonar.server.batch.GlobalRepositoryAction;
 import org.sonar.server.batch.IssuesAction;
 import org.sonar.server.batch.ProjectRepositoryAction;
 import org.sonar.server.batch.ProjectRepositoryLoader;
+import org.sonar.server.batch.UsersAction;
 import org.sonar.server.charts.ChartFactory;
 import org.sonar.server.component.ComponentCleanerService;
 import org.sonar.server.component.ComponentService;
@@ -611,6 +612,7 @@ class ServerComponents {
     pico.addSingleton(ProjectRepositoryLoader.class);
     pico.addSingleton(SubmitReportWsAction.class);
     pico.addSingleton(IssuesAction.class);
+    pico.addSingleton(UsersAction.class);
     pico.addSingleton(BatchWs.class);
 
     // Dashboard
index 73a241e3f68a4129ec1d4b23c37a0cfb124e8e7b..791c6642e88b21888b217df9f9faa1708818dd03 100644 (file)
@@ -24,20 +24,34 @@ import org.apache.commons.lang.StringUtils;
 import org.elasticsearch.action.get.GetRequestBuilder;
 import org.elasticsearch.action.get.GetResponse;
 import org.elasticsearch.action.search.SearchRequestBuilder;
+import org.elasticsearch.action.search.SearchResponse;
+import org.elasticsearch.action.search.SearchScrollRequestBuilder;
+import org.elasticsearch.action.search.SearchType;
+import org.elasticsearch.common.unit.TimeValue;
+import org.elasticsearch.index.query.BoolFilterBuilder;
 import org.elasticsearch.index.query.FilterBuilders;
 import org.elasticsearch.index.query.QueryBuilders;
 import org.elasticsearch.search.SearchHit;
+import org.elasticsearch.search.sort.SortBuilders;
+import org.elasticsearch.search.sort.SortOrder;
 import org.sonar.api.ServerComponent;
 import org.sonar.server.es.EsClient;
 import org.sonar.server.exceptions.NotFoundException;
 
 import javax.annotation.CheckForNull;
 
+import java.util.ArrayDeque;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
 import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Queue;
 
 public class UserIndex implements ServerComponent {
 
+  private static final int SCROLL_TIME_IN_MINUTES = 3;
+
   private final EsClient esClient;
 
   public UserIndex(EsClient esClient) {
@@ -100,4 +114,54 @@ public class UserIndex implements ServerComponent {
     return result;
   }
 
+  public Iterator<UserDoc> selectUsersForBatch(List<String> logins) {
+    BoolFilterBuilder filter = FilterBuilders.boolFilter()
+      .must(FilterBuilders.termsFilter(UserIndexDefinition.FIELD_LOGIN, logins));
+
+    SearchRequestBuilder requestBuilder = esClient
+      .prepareSearch(UserIndexDefinition.INDEX)
+      .setTypes(UserIndexDefinition.TYPE_USER)
+      .setSearchType(SearchType.SCAN)
+      .addSort(SortBuilders.fieldSort(UserIndexDefinition.FIELD_LOGIN).order(SortOrder.ASC))
+      .setScroll(TimeValue.timeValueMinutes(SCROLL_TIME_IN_MINUTES))
+      .setSize(10000)
+      .setFetchSource(
+        new String[] {UserIndexDefinition.FIELD_LOGIN, UserIndexDefinition.FIELD_NAME},
+        null)
+      .setQuery(QueryBuilders.filteredQuery(QueryBuilders.matchAllQuery(), filter));
+    SearchResponse response = requestBuilder.get();
+
+    return scroll(response.getScrollId());
+  }
+
+  // Scrolling within the index
+  private Iterator<UserDoc> scroll(final String scrollId) {
+    return new Iterator<UserDoc>() {
+      private final Queue<SearchHit> hits = new ArrayDeque<>();
+
+      @Override
+      public boolean hasNext() {
+        if (hits.isEmpty()) {
+          SearchScrollRequestBuilder esRequest = esClient.prepareSearchScroll(scrollId)
+            .setScroll(TimeValue.timeValueMinutes(SCROLL_TIME_IN_MINUTES));
+          Collections.addAll(hits, esRequest.get().getHits().getHits());
+        }
+        return !hits.isEmpty();
+      }
+
+      @Override
+      public UserDoc next() {
+        if (!hasNext()) {
+          throw new NoSuchElementException();
+        }
+        return new UserDoc(hits.poll().getSource());
+      }
+
+      @Override
+      public void remove() {
+        throw new UnsupportedOperationException("Cannot remove item when scrolling");
+      }
+    };
+  }
+
 }
index 5afe8f54be71c342de545920d10a1524fe752349..6548bfe6bf7d8679acd32ea912d116f0b30dc869 100644 (file)
@@ -65,7 +65,7 @@ public class GlobalRepositoryActionTest {
     when(dbClient.openSession(false)).thenReturn(session);
     when(dbClient.metricDao()).thenReturn(metricDao);
 
-    tester = new WsTester(new BatchWs(mock(BatchIndex.class), new GlobalRepositoryAction(dbClient, propertiesDao), mock(ProjectRepositoryAction.class), mock(IssuesAction.class)));
+    tester = new WsTester(new BatchWs(mock(BatchIndex.class), new GlobalRepositoryAction(dbClient, propertiesDao)));
   }
 
   @Test
index 9186253b320d97de31824f15e70a4e2b11cae89b..efb7729e58315adcdd8aa7218ee24e630df6fe60 100644 (file)
@@ -35,7 +35,6 @@ import org.sonar.core.component.ComponentDto;
 import org.sonar.core.permission.GlobalPermissions;
 import org.sonar.core.persistence.DbSession;
 import org.sonar.core.persistence.DbTester;
-import org.sonar.core.properties.PropertiesDao;
 import org.sonar.server.component.ComponentTesting;
 import org.sonar.server.component.db.ComponentDao;
 import org.sonar.server.db.DbClient;
@@ -43,7 +42,12 @@ import org.sonar.server.es.EsTester;
 import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.issue.IssueTesting;
 import org.sonar.server.issue.db.IssueDao;
-import org.sonar.server.issue.index.*;
+import org.sonar.server.issue.index.IssueAuthorizationDao;
+import org.sonar.server.issue.index.IssueAuthorizationIndexer;
+import org.sonar.server.issue.index.IssueDoc;
+import org.sonar.server.issue.index.IssueIndex;
+import org.sonar.server.issue.index.IssueIndexDefinition;
+import org.sonar.server.issue.index.IssueIndexer;
 import org.sonar.server.user.MockUserSession;
 import org.sonar.server.ws.WsTester;
 import org.sonar.test.DbTests;
@@ -94,12 +98,7 @@ public class IssuesActionTest {
     issuesAction = new IssuesAction(dbClient, issueIndex);
     componentDao = new ComponentDao();
 
-    tester = new WsTester(new BatchWs(
-      new BatchIndex(mock(Server.class)),
-      new GlobalRepositoryAction(mock(DbClient.class), mock(PropertiesDao.class)),
-      new ProjectRepositoryAction(mock(ProjectRepositoryLoader.class)),
-      issuesAction)
-      );
+    tester = new WsTester(new BatchWs(new BatchIndex(mock(Server.class)), issuesAction));
   }
 
   @After
index 7085f4b249981f9d6a3e78f9d73dd31efc3b2ebd..806196fb2f91736b3c39d86b5d343bb275e6b033 100644 (file)
@@ -43,8 +43,7 @@ public class ProjectRepositoryActionTest {
 
   @Before
   public void setUp() throws Exception {
-    tester = new WsTester(new BatchWs(mock(BatchIndex.class), mock(GlobalRepositoryAction.class),
-      new ProjectRepositoryAction(projectRepositoryLoader), mock(IssuesAction.class)));
+    tester = new WsTester(new BatchWs(mock(BatchIndex.class), new ProjectRepositoryAction(projectRepositoryLoader)));
   }
 
   @Test
diff --git a/server/sonar-server/src/test/java/org/sonar/server/batch/UsersActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/batch/UsersActionTest.java
new file mode 100644 (file)
index 0000000..1daa51c
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ * 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.batch;
+
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonar.api.config.Settings;
+import org.sonar.api.platform.Server;
+import org.sonar.batch.protocol.input.BatchInput.User;
+import org.sonar.core.permission.GlobalPermissions;
+import org.sonar.server.es.EsTester;
+import org.sonar.server.user.MockUserSession;
+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.io.ByteArrayInputStream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+public class UsersActionTest {
+
+  @ClassRule
+  public static EsTester es = new EsTester().addDefinitions(new UserIndexDefinition(new Settings()));
+
+  UserIndex userIndex;
+
+  WsTester tester;
+
+  UsersAction usersAction;
+
+  @Before
+  public void before() throws Exception {
+    es.truncateIndices();
+
+    userIndex = new UserIndex(es.client());
+    usersAction = new UsersAction(userIndex);
+
+    tester = new WsTester(new BatchWs(new BatchIndex(mock(Server.class)), usersAction));
+  }
+
+  @Test
+  public void return_minimal_fields() throws Exception {
+    es.putDocuments(UserIndexDefinition.INDEX, UserIndexDefinition.TYPE_USER,
+      new UserDoc().setLogin("ada.lovelace").setName("Ada Lovelace").setActive(false),
+      new UserDoc().setLogin("grace.hopper").setName("Grace Hopper").setActive(true));
+
+    MockUserSession.set().setLogin("sonarqtech").setGlobalPermissions(GlobalPermissions.PREVIEW_EXECUTION);
+
+    WsTester.TestRequest request = tester.newGetRequest("batch", "users").setParam("logins", "ada.lovelace,grace.hopper");
+
+    ByteArrayInputStream input = new ByteArrayInputStream(request.execute().output());
+
+    User user = User.parseDelimitedFrom(input);
+    assertThat(user.getLogin()).isEqualTo("ada.lovelace");
+    assertThat(user.getName()).isEqualTo("Ada Lovelace");
+
+    user = User.parseDelimitedFrom(input);
+    assertThat(user.getLogin()).isEqualTo("grace.hopper");
+    assertThat(user.getName()).isEqualTo("Grace Hopper");
+
+    assertThat(User.parseDelimitedFrom(input)).isNull();
+  }
+}