]> source.dussan.org Git - sonarqube.git/commitdiff
Refactor issue search stack in order to remove dependency with
authorSimon Brandhof <simon.brandhof@sonarsource.com>
Thu, 5 Feb 2015 12:37:25 +0000 (13:37 +0100)
committerSimon Brandhof <simon.brandhof@sonarsource.com>
Mon, 9 Feb 2015 10:41:05 +0000 (11:41 +0100)
SearchRequestHandler

60 files changed:
server/sonar-server-benchmarks/src/test/java/org/sonar/server/benchmark/IssueIndexBenchmarkTest.java
server/sonar-server/src/main/java/org/sonar/core/computation/dbcleaner/ProjectCleaner.java
server/sonar-server/src/main/java/org/sonar/server/db/DbClient.java
server/sonar-server/src/main/java/org/sonar/server/es/BaseIndex.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/es/EsUtils.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/es/Facets.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/es/SearchOptions.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/es/SearchResult.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/issue/AbstractChangeTagsAction.java
server/sonar-server/src/main/java/org/sonar/server/issue/Action.java
server/sonar-server/src/main/java/org/sonar/server/issue/AssignAction.java
server/sonar-server/src/main/java/org/sonar/server/issue/CommentAction.java
server/sonar-server/src/main/java/org/sonar/server/issue/InternalRubyIssueService.java
server/sonar-server/src/main/java/org/sonar/server/issue/IssueBulkChangeService.java
server/sonar-server/src/main/java/org/sonar/server/issue/IssueQuery.java
server/sonar-server/src/main/java/org/sonar/server/issue/IssueService.java
server/sonar-server/src/main/java/org/sonar/server/issue/PlanAction.java
server/sonar-server/src/main/java/org/sonar/server/issue/SetSeverityAction.java
server/sonar-server/src/main/java/org/sonar/server/issue/TransitionAction.java
server/sonar-server/src/main/java/org/sonar/server/issue/db/IssueDao.java
server/sonar-server/src/main/java/org/sonar/server/issue/filter/IssueFilterService.java
server/sonar-server/src/main/java/org/sonar/server/issue/index/FakeIssueDto.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndex.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/AuthorsAction.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/BaseIssuesWsAction.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/issue/ws/ComponentTagsAction.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueShowAction.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssuesWs.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchAction.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/SetTagsAction.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/TagsAction.java
server/sonar-server/src/main/java/org/sonar/server/search/StickyFacetBuilder.java
server/sonar-server/src/main/java/org/sonar/server/source/index/SourceLineIndex.java
server/sonar-server/src/main/resources/org/sonar/server/issue/ws/example-search.json
server/sonar-server/src/test/java/org/sonar/core/computation/dbcleaner/ProjectCleanerTest.java
server/sonar-server/src/test/java/org/sonar/server/es/EsUtilsTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/es/SearchOptionsTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/issue/ActionTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/InternalRubyIssueServiceTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/IssueBulkChangeServiceTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/IssueServiceMediumTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/filter/IssueFilterServiceTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexMediumTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/AuthorsActionTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/ComponentTagsActionTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/IssueShowActionTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/IssueTagsActionTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/IssuesWsTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionComponentsMediumTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionMediumTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetTagsActionTest.java
server/sonar-server/src/test/java/org/sonar/server/view/index/ViewIndexerMediumTest.java
server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionMediumTest/default_page_size_is_100.json
server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionMediumTest/deprecated_paging.json
server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionMediumTest/empty_result.json
server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionMediumTest/issue.json
sonar-core/src/main/java/org/sonar/core/issue/db/IssueChangeDao.java
sonar-core/src/main/java/org/sonar/core/issue/db/IssueMapper.java
sonar-plugin-api/src/main/java/org/sonar/api/server/ws/WebService.java
sonar-plugin-api/src/test/java/org/sonar/api/server/ws/WebServiceTest.java

index 64950c865d714bc1d3c2ba60c05b4d49157e3049..4d2a9de9f5bf3efcef9a523a659a721cd1a44d9a 100644 (file)
@@ -34,14 +34,14 @@ import org.sonar.api.issue.Issue;
 import org.sonar.api.rule.Severity;
 import org.sonar.api.utils.internal.Uuids;
 import org.sonar.server.es.EsClient;
+import org.sonar.server.es.SearchOptions;
+import org.sonar.server.es.SearchResult;
 import org.sonar.server.issue.IssueQuery;
 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.IssueIndexer;
-import org.sonar.server.search.QueryContext;
-import org.sonar.server.search.Result;
 import org.sonar.server.tester.ServerTester;
 import org.sonar.server.user.MockUserSession;
 
@@ -138,7 +138,7 @@ public class IssueIndexBenchmarkTest {
     IssueIndex index = tester.get(IssueIndex.class);
     for (int i = 0; i < 10; i++) {
       long start = System.currentTimeMillis();
-      Result<Issue> result = index.search(query, new QueryContext());
+      SearchResult<IssueDoc> result = index.search(query, new SearchOptions());
       long end = System.currentTimeMillis();
       LOGGER.info("Request (" + label + "): {} docs in {} ms", result.getTotal(), end - start);
     }
index 021f88ebdaf2ad4633ad6ff1e8c6d57206c996ee..dd9a2d955ecb4675cefc1c84641bcfa569ae855d 100644 (file)
@@ -28,10 +28,13 @@ import org.sonar.api.config.Settings;
 import org.sonar.api.utils.TimeUtils;
 import org.sonar.core.computation.dbcleaner.period.DefaultPeriodCleaner;
 import org.sonar.core.persistence.DbSession;
-import org.sonar.core.purge.*;
+import org.sonar.core.purge.IdUuidPair;
+import org.sonar.core.purge.PurgeConfiguration;
+import org.sonar.core.purge.PurgeDao;
+import org.sonar.core.purge.PurgeListener;
+import org.sonar.core.purge.PurgeProfiler;
 import org.sonar.server.issue.index.IssueIndex;
 import org.sonar.server.properties.ProjectSettingsFactory;
-import org.sonar.server.search.IndexClient;
 
 import javax.annotation.Nullable;
 import java.util.Date;
@@ -46,16 +49,16 @@ public class ProjectCleaner implements ServerComponent {
   private final PurgeDao purgeDao;
   private final DefaultPeriodCleaner periodCleaner;
   private final ProjectSettingsFactory projectSettingsFactory;
-  private final IndexClient indexClient;
+  private final IssueIndex issueIndex;
 
   public ProjectCleaner(PurgeDao purgeDao, DefaultPeriodCleaner periodCleaner, PurgeProfiler profiler, PurgeListener purgeListener,
-                        ProjectSettingsFactory projectSettingsFactory, IndexClient indexClient) {
+                        ProjectSettingsFactory projectSettingsFactory, IssueIndex issueIndex) {
     this.purgeDao = purgeDao;
     this.periodCleaner = periodCleaner;
     this.profiler = profiler;
     this.purgeListener = purgeListener;
     this.projectSettingsFactory = projectSettingsFactory;
-    this.indexClient = indexClient;
+    this.issueIndex = issueIndex;
   }
 
   public ProjectCleaner purge(DbSession session, IdUuidPair idUuidPair) {
@@ -77,7 +80,7 @@ public class ProjectCleaner implements ServerComponent {
 
   private void deleteIndexedIssuesBefore(String uuid, @Nullable Date lastDateWithClosedIssues) {
     if (lastDateWithClosedIssues != null) {
-      indexClient.get(IssueIndex.class).deleteClosedIssuesOfProjectBefore(uuid, lastDateWithClosedIssues);
+      issueIndex.deleteClosedIssuesOfProjectBefore(uuid, lastDateWithClosedIssues);
     }
   }
 
index 22cbf6fe36e9731a2f39455185621439e9142b7e..ea6ac8f2980afffc2415e1436e6b3ee66a19806d 100644 (file)
  */
 package org.sonar.server.db;
 
-import org.sonar.core.user.AuthorDao;
-
 import org.sonar.api.ServerComponent;
 import org.sonar.core.issue.db.ActionPlanDao;
+import org.sonar.core.issue.db.IssueChangeDao;
 import org.sonar.core.persistence.DaoComponent;
 import org.sonar.core.persistence.Database;
 import org.sonar.core.persistence.DbSession;
@@ -33,6 +32,7 @@ import org.sonar.core.resource.ResourceDao;
 import org.sonar.core.source.db.FileSourceDao;
 import org.sonar.core.technicaldebt.db.CharacteristicDao;
 import org.sonar.core.template.LoadedTemplateDao;
+import org.sonar.core.user.AuthorDao;
 import org.sonar.core.user.AuthorizationDao;
 import org.sonar.server.activity.db.ActivityDao;
 import org.sonar.server.component.db.ComponentDao;
@@ -79,6 +79,7 @@ public class DbClient implements ServerComponent {
   private final UserDao userDao;
   private final GroupDao groupDao;
   private final IssueDao issueDao;
+  private final IssueChangeDao issueChangeDao;
   private final ActionPlanDao actionPlanDao;
   private final AnalysisReportDao analysisReportDao;
   private final DashboardDao dashboardDao;
@@ -111,6 +112,7 @@ public class DbClient implements ServerComponent {
     userDao = getDao(map, UserDao.class);
     groupDao = getDao(map, GroupDao.class);
     issueDao = getDao(map, IssueDao.class);
+    issueChangeDao = getDao(map, IssueChangeDao.class);
     actionPlanDao = getDao(map, ActionPlanDao.class);
     analysisReportDao = getDao(map, AnalysisReportDao.class);
     dashboardDao = getDao(map, DashboardDao.class);
@@ -140,6 +142,10 @@ public class DbClient implements ServerComponent {
     return issueDao;
   }
 
+  public IssueChangeDao issueChangeDao() {
+    return issueChangeDao;
+  }
+
   public QualityProfileDao qualityProfileDao() {
     return qualityProfileDao;
   }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/BaseIndex.java b/server/sonar-server/src/main/java/org/sonar/server/es/BaseIndex.java
new file mode 100644 (file)
index 0000000..45d2535
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * 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.es;
+
+import org.sonar.api.ServerComponent;
+
+public abstract class BaseIndex implements ServerComponent {
+  private final EsClient client;
+
+  public BaseIndex(EsClient client) {
+    this.client = client;
+  }
+
+  public EsClient getClient() {
+    return client;
+  }
+
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/EsUtils.java b/server/sonar-server/src/main/java/org/sonar/server/es/EsUtils.java
new file mode 100644 (file)
index 0000000..837754c
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * 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.es;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Lists;
+import org.elasticsearch.search.SearchHit;
+import org.elasticsearch.search.SearchHits;
+import org.elasticsearch.search.aggregations.bucket.terms.Terms;
+import org.sonar.server.search.BaseDoc;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class EsUtils {
+
+  private EsUtils() {
+    // only static methods
+  }
+
+  public static <D extends BaseDoc> List<D> convertToDocs(SearchHits hits, Function<Map<String, Object>, D> converter) {
+    List<D> docs = new ArrayList<>();
+    for (SearchHit hit : hits.getHits()) {
+      docs.add(converter.apply(hit.getSource()));
+    }
+    return docs;
+  }
+
+  public static LinkedHashMap<String, Long> termsToMap(Terms terms) {
+    LinkedHashMap<String, Long> map = new LinkedHashMap<>();
+    List<Terms.Bucket> buckets = terms.getBuckets();
+    for (Terms.Bucket bucket : buckets) {
+      map.put(bucket.getKey(), bucket.getDocCount());
+    }
+    return map;
+  }
+
+  public static List<String> termsKeys(Terms terms) {
+    return Lists.transform(terms.getBuckets(), new Function<Terms.Bucket, String>() {
+      @Override
+      public String apply(Terms.Bucket bucket) {
+        return bucket.getKey();
+      }
+    });
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/Facets.java b/server/sonar-server/src/main/java/org/sonar/server/es/Facets.java
new file mode 100644 (file)
index 0000000..a22131e
--- /dev/null
@@ -0,0 +1,153 @@
+/*
+ * 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.es;
+
+import org.apache.commons.lang.builder.ReflectionToStringBuilder;
+import org.apache.commons.lang.builder.ToStringStyle;
+import org.elasticsearch.action.search.SearchResponse;
+import org.elasticsearch.search.aggregations.Aggregation;
+import org.elasticsearch.search.aggregations.HasAggregations;
+import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogram;
+import org.elasticsearch.search.aggregations.bucket.missing.Missing;
+import org.elasticsearch.search.aggregations.bucket.terms.Terms;
+
+import javax.annotation.CheckForNull;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+public class Facets {
+
+  private final Map<String, LinkedHashMap<String, Long>> facetsByName = new LinkedHashMap<>();
+
+  public Facets(SearchResponse response) {
+    if (response.getAggregations() != null) {
+      for (Aggregation facet : response.getAggregations()) {
+        processAggregation(facet);
+      }
+    }
+  }
+
+  public Facets(Terms terms) {
+    processTermsAggregation(terms);
+  }
+
+  private void processAggregation(Aggregation aggregation) {
+    if (Missing.class.isAssignableFrom(aggregation.getClass())) {
+      processMissingAggregation((Missing) aggregation);
+    } else if (Terms.class.isAssignableFrom(aggregation.getClass())) {
+      processTermsAggregation((Terms) aggregation);
+    } else if (HasAggregations.class.isAssignableFrom(aggregation.getClass())) {
+      processSubAggregations((HasAggregations) aggregation);
+    } else if (DateHistogram.class.isAssignableFrom(aggregation.getClass())) {
+      processDateHistogram((DateHistogram) aggregation);
+    } else {
+      throw new IllegalArgumentException("Aggregation type not supported yet: " + aggregation.getClass());
+    }
+  }
+
+  private void processMissingAggregation(Missing aggregation) {
+    long docCount = aggregation.getDocCount();
+    if (docCount > 0L) {
+      getOrCreateFacet(aggregation.getName().replace("_missing", "")).put("", docCount);
+    }
+  }
+
+  private void processTermsAggregation(Terms aggregation) {
+    String facetName = aggregation.getName();
+    // TODO document this naming convention
+    if (facetName.contains("__") && !facetName.startsWith("__")) {
+      facetName = facetName.substring(0, facetName.indexOf("__"));
+    }
+    facetName = facetName.replace("_selected", "");
+    LinkedHashMap<String, Long> facet = getOrCreateFacet(facetName);
+    for (Terms.Bucket value : aggregation.getBuckets()) {
+      facet.put(value.getKey(), value.getDocCount());
+    }
+  }
+
+  private void processSubAggregations(HasAggregations aggregation) {
+    for (Aggregation sub : aggregation.getAggregations()) {
+      processAggregation(sub);
+    }
+  }
+
+  private void processDateHistogram(DateHistogram aggregation) {
+    LinkedHashMap<String, Long> facet = getOrCreateFacet(aggregation.getName());
+    for (DateHistogram.Bucket value : aggregation.getBuckets()) {
+      facet.put(value.getKeyAsText().toString(), value.getDocCount());
+    }
+  }
+
+  public boolean contains(String facetName) {
+    return facetsByName.containsKey(facetName);
+  }
+
+  /**
+   * The buckets of the given facet. Null if the facet does not exist
+   */
+  @CheckForNull
+  public LinkedHashMap<String, Long> get(String facetName) {
+    return facetsByName.get(facetName);
+  }
+
+  public Map<String, LinkedHashMap<String, Long>> getAll() {
+    return facetsByName;
+  }
+
+  /**
+   * Value of the facet bucket. Null if the facet or the bucket do not exist.
+   */
+  @CheckForNull
+  public Long getBucketValue(String facetName, String bucketKey) {
+    LinkedHashMap<String, Long> facet = facetsByName.get(facetName);
+    if (facet != null) {
+      return facet.get(bucketKey);
+    }
+    return null;
+  }
+
+  public Set<String> getBucketKeys(String facetName) {
+    LinkedHashMap<String, Long> facet = facetsByName.get(facetName);
+    if (facet != null) {
+      return facet.keySet();
+    }
+    return Collections.emptySet();
+  }
+
+  public Set<String> getNames() {
+    return facetsByName.keySet();
+  }
+
+  @Override
+  public String toString() {
+    return ReflectionToStringBuilder.toString(this, ToStringStyle.SIMPLE_STYLE);
+  }
+
+  private LinkedHashMap<String, Long> getOrCreateFacet(String facetName) {
+    LinkedHashMap<String, Long> facet = facetsByName.get(facetName);
+    if (facet == null) {
+      facet = new LinkedHashMap<>();
+      facetsByName.put(facetName, facet);
+    }
+    return facet;
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/SearchOptions.java b/server/sonar-server/src/main/java/org/sonar/server/es/SearchOptions.java
new file mode 100644 (file)
index 0000000..57cf490
--- /dev/null
@@ -0,0 +1,179 @@
+/*
+ * 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.es;
+
+import com.google.common.base.Preconditions;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.api.utils.text.JsonWriter;
+
+import javax.annotation.Nullable;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+/**
+ * Various Elasticsearch request options: paging, fields and facets
+ */
+public class SearchOptions {
+
+  public static final int DEFAULT_OFFSET = 0;
+  public static final int DEFAULT_LIMIT = 10;
+  public static final int MAX_LIMIT = 500;
+
+  private int offset = DEFAULT_OFFSET;
+  private int limit = DEFAULT_LIMIT;
+  private final Set<String> facets = new LinkedHashSet<>();
+  private final Set<String> fieldsToReturn = new HashSet<>();
+
+  /**
+   * Offset of the first result to return. Defaults to {@link #DEFAULT_OFFSET}
+   */
+  public int getOffset() {
+    return offset;
+  }
+
+  /**
+   * Sets the offset of the first result to return (zero-based).
+   */
+  public SearchOptions setOffset(int offset) {
+    Preconditions.checkArgument(offset >= 0, "Offset must be positive");
+    this.offset = offset;
+    return this;
+  }
+
+  /**
+   * Set offset and limit according to page approach. If pageSize is negative, then
+   * {@link #MAX_LIMIT} is used.
+   */
+  public SearchOptions setPage(int page, int pageSize) {
+    Preconditions.checkArgument(page >= 1, "Page must be greater or equal to 1 (got " + page + ")");
+    setLimit(pageSize);
+    setOffset((page * this.limit) - this.limit);
+    return this;
+  }
+
+  public int getPage() {
+    return limit > 0 ? (int) Math.ceil((double) (offset + 1) / (double) limit) : 0;
+  }
+
+  /**
+   * Limit on the number of results to return. Defaults to {@link #DEFAULT_LIMIT}.
+   */
+  public int getLimit() {
+    return limit;
+  }
+
+  /**
+   * Sets the limit on the number of results to return.
+   */
+  public SearchOptions setLimit(int limit) {
+    if (limit < 0) {
+      this.limit = MAX_LIMIT;
+    } else {
+      this.limit = Math.min(limit, MAX_LIMIT);
+    }
+    return this;
+  }
+
+  /**
+   * WARNING - dangerous
+   */
+  @Deprecated
+  public SearchOptions disableLimit() {
+    this.limit = 999999;
+    return this;
+  }
+
+  /**
+   * Lists selected facets.
+   */
+  public Collection<String> getFacets() {
+    return facets;
+  }
+
+  /**
+   * Selects facets to return for the domain.
+   */
+  public SearchOptions addFacets(@Nullable Collection<String> f) {
+    if (f != null) {
+      this.facets.addAll(f);
+    }
+    return this;
+  }
+
+  public SearchOptions addFacets(String... array) {
+    Collections.addAll(facets, array);
+    return this;
+  }
+
+  public Set<String> getFields() {
+    return fieldsToReturn;
+  }
+
+  public boolean hasField(String key) {
+    return fieldsToReturn.isEmpty() || fieldsToReturn.contains(key);
+  }
+
+  public SearchOptions addFields(@Nullable Collection<String> c) {
+    if (c != null) {
+      for (String s : c) {
+        if (StringUtils.isNotBlank(s)) {
+          fieldsToReturn.add(s);
+        }
+      }
+    }
+    return this;
+  }
+
+  public SearchOptions addFields(String... array) {
+    return addFields(Arrays.asList(array));
+  }
+
+  public SearchOptions writeJson(JsonWriter json, long totalHits) {
+    json.prop("total", totalHits);
+    json.prop(WebService.Param.PAGE, getPage());
+    json.prop(WebService.Param.PAGE_SIZE, getLimit());
+    return this;
+  }
+
+  @Deprecated
+  public SearchOptions writeDeprecatedJson(JsonWriter json, long totalHits) {
+    int pages = 0;
+    if (limit > 0) {
+      pages = (int) (totalHits / limit);
+      if (totalHits % limit > 0) {
+        pages++;
+      }
+    }
+    json.name("paging").beginObject()
+      .prop("pageIndex", getPage())
+      .prop("pageSize", getLimit())
+      .prop("total", totalHits)
+      .prop("fTotal", String.valueOf(totalHits))
+      .prop("pages", pages)
+      .endObject();
+    return this;
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/SearchResult.java b/server/sonar-server/src/main/java/org/sonar/server/es/SearchResult.java
new file mode 100644 (file)
index 0000000..731f487
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * 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.es;
+
+import com.google.common.base.Function;
+import org.apache.commons.lang.builder.ReflectionToStringBuilder;
+import org.elasticsearch.action.search.SearchResponse;
+import org.sonar.server.search.BaseDoc;
+
+import java.util.List;
+import java.util.Map;
+
+public class SearchResult<DOC extends BaseDoc> {
+
+  private final List<DOC> docs;
+  private final Facets facets;
+  private final long total;
+
+  public SearchResult(SearchResponse response, Function<Map<String, Object>, DOC> converter) {
+    this.facets = new Facets(response);
+    this.total = response.getHits().totalHits();
+    this.docs = EsUtils.convertToDocs(response.getHits(), converter);
+  }
+
+  public List<DOC> getDocs() {
+    return docs;
+  }
+
+  public long getTotal() {
+    return total;
+  }
+
+  public Facets getFacets() {
+    return this.facets;
+  }
+
+  @Override
+  public String toString() {
+    return ReflectionToStringBuilder.toString(this);
+  }
+}
index d5a0c6293415ddee1d6d08fab97c10fe941a6b16..7aa68b28e11ab2b4ccb29a24ff5be8138165fa3f 100644 (file)
@@ -32,11 +32,9 @@ import org.sonar.core.issue.IssueUpdater;
 import org.sonar.server.user.UserSession;
 
 import java.util.Collection;
-import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
-
 public abstract class AbstractChangeTagsAction extends Action implements ServerComponent {
 
   private static final Splitter TAGS_SPLITTER = Splitter.on(',').trimResults().omitEmptyStrings();
@@ -50,7 +48,7 @@ public abstract class AbstractChangeTagsAction extends Action implements ServerC
   }
 
   @Override
-  public boolean verify(Map<String, Object> properties, List<Issue> issues, UserSession userSession){
+  public boolean verify(Map<String, Object> properties, Collection<Issue> issues, UserSession userSession) {
     parseTags(properties);
     return true;
   }
@@ -67,7 +65,7 @@ public abstract class AbstractChangeTagsAction extends Action implements ServerC
     Set<String> result = Sets.newHashSet();
     String tagsString = (String) properties.get("tags");
     if (!Strings.isNullOrEmpty(tagsString)) {
-      for(String tag: TAGS_SPLITTER.split(tagsString)) {
+      for (String tag : TAGS_SPLITTER.split(tagsString)) {
         RuleTagFormat.validate(tag);
         result.add(tag);
       }
index e7224a79129e48ce6cb268795e7932ea8df5ac15..715e725df6eab79ede6d59d74e28212920ae6203 100644 (file)
@@ -29,6 +29,7 @@ import org.sonar.api.issue.condition.Condition;
 import org.sonar.api.issue.internal.IssueChangeContext;
 import org.sonar.server.user.UserSession;
 
+import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 
@@ -70,7 +71,7 @@ public abstract class Action implements ServerComponent {
     return true;
   }
 
-  abstract boolean verify(Map<String, Object> properties, List<Issue> issues, UserSession userSession);
+  abstract boolean verify(Map<String, Object> properties, Collection<Issue> issues, UserSession userSession);
 
   abstract boolean execute(Map<String, Object> properties, Context context);
 
index d6ead7dd524ad593514ff9788b6bacc21fce82a4..8beab29d430d47ffd22bee1b918008763b2730f0 100644 (file)
@@ -30,7 +30,7 @@ import org.sonar.api.user.UserFinder;
 import org.sonar.core.issue.IssueUpdater;
 import org.sonar.server.user.UserSession;
 
-import java.util.List;
+import java.util.Collection;
 import java.util.Map;
 
 
@@ -50,7 +50,7 @@ public class AssignAction extends Action implements ServerComponent {
   }
 
   @Override
-  public boolean verify(Map<String, Object> properties, List<Issue> issues, UserSession userSession){
+  public boolean verify(Map<String, Object> properties, Collection<Issue> issues, UserSession userSession){
     String assignee = assigneeValue(properties);
     if(!Strings.isNullOrEmpty(assignee)) {
       User user = selectUser(assignee);
index e50eba29d089cab2e62671c5b5ebe08835bea721..d8700f556003cc113da4d1f8319e17a927f49120 100644 (file)
@@ -27,10 +27,9 @@ import org.sonar.api.issue.internal.DefaultIssue;
 import org.sonar.core.issue.IssueUpdater;
 import org.sonar.server.user.UserSession;
 
-import java.util.List;
+import java.util.Collection;
 import java.util.Map;
 
-
 public class CommentAction extends Action implements ServerComponent {
 
   public static final String KEY = "comment";
@@ -44,7 +43,7 @@ public class CommentAction extends Action implements ServerComponent {
   }
 
   @Override
-  public boolean verify(Map<String, Object> properties, List<Issue> issues, UserSession userSession) {
+  public boolean verify(Map<String, Object> properties, Collection<Issue> issues, UserSession userSession) {
     comment(properties);
     return true;
   }
@@ -58,7 +57,7 @@ public class CommentAction extends Action implements ServerComponent {
   private String comment(Map<String, Object> properties) {
     String param = (String) properties.get(COMMENT_PROPERTY);
     if (Strings.isNullOrEmpty(param)) {
-      throw new IllegalArgumentException("Missing parameter : '"+ COMMENT_PROPERTY +"'");
+      throw new IllegalArgumentException("Missing parameter : '" + COMMENT_PROPERTY + "'");
     }
     return param;
   }
index e60b31e6323b9e5dc794431582857228f6d2dc9a..d20fc825c446e1fec47e52f70b0a33ecad5ed14f 100644 (file)
@@ -43,6 +43,7 @@ import org.sonar.core.issue.workflow.Transition;
 import org.sonar.core.resource.ResourceDao;
 import org.sonar.core.resource.ResourceDto;
 import org.sonar.core.resource.ResourceQuery;
+import org.sonar.server.es.SearchOptions;
 import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.issue.actionplan.ActionPlanService;
 import org.sonar.server.issue.filter.IssueFilterParameters;
@@ -477,7 +478,7 @@ public class InternalRubyIssueService implements ServerComponent {
    * Execute issue filter from parameters
    */
   public IssueFilterService.IssueFilterResult execute(Map<String, Object> props) {
-    return issueFilterService.execute(issueQueryService.createFromMap(props), toContext(props));
+    return issueFilterService.execute(issueQueryService.createFromMap(props), toSearchOptions(props));
   }
 
   /**
@@ -636,16 +637,16 @@ public class InternalRubyIssueService implements ServerComponent {
   }
 
   @VisibleForTesting
-  static QueryContext toContext(Map<String, Object> props) {
-    QueryContext context = new QueryContext();
+  static SearchOptions toSearchOptions(Map<String, Object> props) {
+    SearchOptions options = new SearchOptions();
     Integer pageIndex = RubyUtils.toInteger(props.get(IssueFilterParameters.PAGE_INDEX));
     Integer pageSize = RubyUtils.toInteger(props.get(IssueFilterParameters.PAGE_SIZE));
     if (pageSize != null && pageSize < 0) {
-      context.setMaxLimit();
+      options.setLimit(SearchOptions.MAX_LIMIT);
     } else {
-      context.setPage(pageIndex != null ? pageIndex : 1, pageSize != null ? pageSize : 100);
+      options.setPage(pageIndex != null ? pageIndex : 1, pageSize != null ? pageSize : 100);
     }
-    return context;
+    return options;
   }
 
   public Collection<String> listTags() {
index f235a36e71d298bcb1474fd1d1e30ef8c6a22c75..d29e4a4eabee5c2c780b45f2193b86f58f0ef49f 100644 (file)
@@ -22,7 +22,9 @@ package org.sonar.server.issue;
 
 import com.google.common.base.Function;
 import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
 import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.sonar.api.issue.Issue;
@@ -35,16 +37,20 @@ import org.sonar.core.component.ComponentDto;
 import org.sonar.core.issue.db.IssueDto;
 import org.sonar.core.issue.db.IssueStorage;
 import org.sonar.core.persistence.DbSession;
+import org.sonar.core.persistence.MyBatis;
 import org.sonar.server.db.DbClient;
+import org.sonar.server.es.SearchOptions;
 import org.sonar.server.exceptions.BadRequestException;
+import org.sonar.server.issue.index.IssueDoc;
 import org.sonar.server.issue.notification.IssueChangeNotification;
 import org.sonar.server.rule.DefaultRuleFinder;
-import org.sonar.server.search.QueryContext;
 import org.sonar.server.user.UserSession;
 
 import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
 
+import java.util.Collection;
+import java.util.Collections;
 import java.util.Date;
 import java.util.HashSet;
 import java.util.List;
@@ -83,12 +89,12 @@ public class IssueBulkChangeService {
 
     IssueBulkChangeResult result = new IssueBulkChangeResult();
 
-    List<Issue> issues = getByKeysForUpdate(issueBulkChangeQuery.issues());
+    Collection<Issue> issues = getByKeysForUpdate(issueBulkChangeQuery.issues());
     Repository repository = new Repository(issues);
 
     List<Action> bulkActions = getActionsToApply(issueBulkChangeQuery, issues, userSession);
     IssueChangeContext issueChangeContext = IssueChangeContext.createUser(new Date(), userSession.login());
-    Set<String> concernedProjects = new HashSet<String>();
+    Set<String> concernedProjects = new HashSet<>();
     for (Issue issue : issues) {
       ActionContext actionContext = new ActionContext(issue, issueChangeContext);
       for (Action action : bulkActions) {
@@ -119,31 +125,36 @@ public class IssueBulkChangeService {
     return result;
   }
 
-  private List<Issue> getByKeysForUpdate(List<String> issueKeys) {
+  private Collection<Issue> getByKeysForUpdate(List<String> issueKeys) {
     // Load from index to check permission
-    List<Issue> authorizedIndexIssues = issueService.search(IssueQuery.builder().issueKeys(issueKeys).build(), new QueryContext().setMaxLimit()).getHits();
-    List<String> authorizedIssueKeys = newArrayList(Iterables.transform(authorizedIndexIssues, new Function<Issue, String>() {
+    SearchOptions options = new SearchOptions().setLimit(SearchOptions.MAX_LIMIT);
+    // TODO restrict fields to issue key, in order to not load all other fields;
+    List<IssueDoc> authorizedIssues = issueService.search(IssueQuery.builder().issueKeys(issueKeys).build(), options).getDocs();
+    Collection<String> authorizedKeys = Collections2.transform(authorizedIssues, new Function<IssueDoc, String>() {
       @Override
-      public String apply(@Nullable Issue input) {
-        return input != null ? input.key() : null;
+      public String apply(IssueDoc input) {
+        return input.key();
       }
-    }));
+    });
 
-    DbSession session = dbClient.openSession(false);
-    try {
-      List<IssueDto> issueDtos = dbClient.issueDao().selectByKeys(session, authorizedIssueKeys);
-      return newArrayList(Iterables.transform(issueDtos, new Function<IssueDto, Issue>() {
-        @Override
-        public Issue apply(@Nullable IssueDto input) {
-          return input != null ? input.toDefaultIssue() : null;
-        }
-      }));
-    } finally {
-      session.close();
+    if (!authorizedKeys.isEmpty()) {
+      DbSession session = dbClient.openSession(false);
+      try {
+        List<IssueDto> dtos = dbClient.issueDao().selectByKeys(session, Lists.newArrayList(authorizedKeys));
+        return Collections2.transform(dtos, new Function<IssueDto, Issue>() {
+          @Override
+          public Issue apply(@Nullable IssueDto input) {
+            return input != null ? input.toDefaultIssue() : null;
+          }
+        });
+      } finally {
+        MyBatis.closeQuietly(session);
+      }
     }
+    return Collections.emptyList();
   }
 
-  private List<Action> getActionsToApply(IssueBulkChangeQuery issueBulkChangeQuery, List<Issue> issues, UserSession userSession) {
+  private List<Action> getActionsToApply(IssueBulkChangeQuery issueBulkChangeQuery, Collection<Issue> issues, UserSession userSession) {
     List<Action> bulkActions = newArrayList();
     for (String actionKey : issueBulkChangeQuery.actions()) {
       Action action = getAction(actionKey);
@@ -207,7 +218,7 @@ public class IssueBulkChangeService {
     private final Map<String, ComponentDto> components = newHashMap();
     private final Map<String, ComponentDto> projects = newHashMap();
 
-    public Repository(List<Issue> issues) {
+    public Repository(Collection<Issue> issues) {
       Set<RuleKey> ruleKeys = newHashSet();
       Set<String> componentKeys = newHashSet();
       Set<String> projectKeys = newHashSet();
index 8067ca3702134b54cc53e362f3504b6e328b5839..94ff60ae58f91dad2d625a59d7e7e699c2ac6a70 100644 (file)
@@ -24,15 +24,17 @@ import com.google.common.collect.ImmutableSet;
 import org.apache.commons.lang.builder.ReflectionToStringBuilder;
 import org.sonar.api.rule.RuleKey;
 import org.sonar.server.search.QueryContext;
+import org.sonar.server.user.UserSession;
 
 import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
-
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
 import java.util.Set;
 
+import static com.google.common.collect.Sets.newHashSet;
+
 /**
  * @since 3.6
  */
@@ -84,6 +86,10 @@ public class IssueQuery {
   private final Boolean ignorePaging;
   private final Boolean contextualized;
 
+  private final String userLogin;
+  private final Set<String> userGroups;
+  private final boolean checkAuthorization;
+
   private IssueQuery(Builder builder) {
     this.issueKeys = defaultCollection(builder.issueKeys);
     this.severities = defaultCollection(builder.severities);
@@ -114,6 +120,9 @@ public class IssueQuery {
     this.sort = builder.sort;
     this.asc = builder.asc;
     this.ignorePaging = builder.ignorePaging;
+    this.userLogin = builder.userLogin;
+    this.userGroups = builder.userGroups;
+    this.checkAuthorization = builder.checkAuthorization;
     this.contextualized = builder.contextualized;
   }
 
@@ -247,6 +256,19 @@ public class IssueQuery {
     return ignorePaging;
   }
 
+  @CheckForNull
+  public String userLogin() {
+    return userLogin;
+  }
+
+  public Set<String> userGroups() {
+    return newHashSet(defaultCollection(userGroups));
+  }
+
+  public boolean checkAuthorization() {
+    return checkAuthorization;
+  }
+
   @CheckForNull
   public Boolean isContextualized() {
     return contextualized;
@@ -292,6 +314,9 @@ public class IssueQuery {
     private Boolean asc = false;
     private Boolean ignorePaging = false;
     private boolean contextualized;
+    private String userLogin = UserSession.get().login();
+    private Set<String> userGroups = UserSession.get().userGroups();
+    private boolean checkAuthorization = true;
 
     private Builder() {
     }
@@ -475,6 +500,21 @@ public class IssueQuery {
       return this;
     }
 
+    public Builder userLogin(@Nullable String userLogin) {
+      this.userLogin = userLogin;
+      return this;
+    }
+
+    public Builder userGroups(@Nullable Set<String> userGroups) {
+      this.userGroups = userGroups;
+      return this;
+    }
+
+    public Builder checkAuthorization(boolean checkAuthorization) {
+      this.checkAuthorization = checkAuthorization;
+      return this;
+    }
+
     public Builder setContextualized(boolean b) {
       this.contextualized = b;
       return this;
index 0c893e2fe79c11ff046cd0a106bdff27fcb63c73..6379acc899f9b7b47cbbe222826ba60ebdb27c9d 100644 (file)
@@ -48,13 +48,13 @@ import org.sonar.core.issue.workflow.Transition;
 import org.sonar.core.persistence.DbSession;
 import org.sonar.core.rule.RuleDto;
 import org.sonar.server.db.DbClient;
+import org.sonar.server.es.SearchOptions;
+import org.sonar.server.es.SearchResult;
 import org.sonar.server.exceptions.NotFoundException;
 import org.sonar.server.issue.actionplan.ActionPlanService;
+import org.sonar.server.issue.index.IssueDoc;
 import org.sonar.server.issue.index.IssueIndex;
 import org.sonar.server.issue.notification.IssueChangeNotification;
-import org.sonar.server.search.FacetValue;
-import org.sonar.server.search.IndexClient;
-import org.sonar.server.search.QueryContext;
 import org.sonar.server.source.index.SourceLineDoc;
 import org.sonar.server.source.index.SourceLineIndex;
 import org.sonar.server.user.UserSession;
@@ -64,14 +64,20 @@ import org.sonar.server.user.index.UserIndex;
 import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
 
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
 
 import static com.google.common.collect.Maps.newLinkedHashMap;
 
 public class IssueService implements ServerComponent {
 
   private final DbClient dbClient;
-  private final IndexClient indexClient;
+  private final IssueIndex issueIndex;
 
   private final IssueWorkflow workflow;
   private final IssueUpdater issueUpdater;
@@ -84,7 +90,7 @@ public class IssueService implements ServerComponent {
   private final UserIndex userIndex;
   private final SourceLineIndex sourceLineIndex;
 
-  public IssueService(DbClient dbClient, IndexClient indexClient,
+  public IssueService(DbClient dbClient, IssueIndex issueIndex,
     IssueWorkflow workflow,
     IssueStorage issueStorage,
     IssueUpdater issueUpdater,
@@ -95,7 +101,7 @@ public class IssueService implements ServerComponent {
     UserFinder userFinder,
     UserIndex userIndex, SourceLineIndex sourceLineIndex) {
     this.dbClient = dbClient;
-    this.indexClient = indexClient;
+    this.issueIndex = issueIndex;
     this.workflow = workflow;
     this.issueStorage = issueStorage;
     this.issueUpdater = issueUpdater;
@@ -301,19 +307,20 @@ public class IssueService implements ServerComponent {
 
   public Map<String, Long> findIssueAssignees(IssueQuery query) {
     Map<String, Long> result = newLinkedHashMap();
-    List<FacetValue> facetValues = indexClient.get(IssueIndex.class).listAssignees(query);
-    for (FacetValue facetValue : facetValues) {
-      if ("_notAssigned_".equals(facetValue.getKey())) {
-        result.put(null, facetValue.getValue());
+    Map<String,Long> buckets = issueIndex.searchForAssignees(query);
+    for (Map.Entry<String, Long> bucket : buckets.entrySet()) {
+      if ("_notAssigned_".equals(bucket.getKey())) {
+        // TODO null key ?
+        result.put(null, bucket.getValue());
       } else {
-        result.put(facetValue.getKey(), facetValue.getValue());
+        result.put(bucket.getKey(), bucket.getValue());
       }
     }
     return result;
   }
 
   public Issue getByKey(String key) {
-    return indexClient.get(IssueIndex.class).getByKey(key);
+    return issueIndex.getByKey(key);
   }
 
   IssueDto getByKeyForUpdate(DbSession session, String key) {
@@ -346,20 +353,29 @@ public class IssueService implements ServerComponent {
     return ruleFinder.findByKey(ruleKey);
   }
 
-  public org.sonar.server.search.Result<Issue> search(IssueQuery query, QueryContext options) {
-    return indexClient.get(IssueIndex.class).search(query, options);
+  public SearchResult<IssueDoc> search(IssueQuery query, SearchOptions options) {
+    return issueIndex.search(query, options);
   }
 
   private void verifyLoggedIn() {
     UserSession.get().checkLoggedIn();
   }
 
-  public Collection<String> listTags(@Nullable String query, int pageSize) {
-    return indexClient.get(IssueIndex.class).listTagsMatching(query, pageSize);
+  /**
+   * Search for all tags, whatever issue resolution or user access rights
+   */
+  public List<String> listTags(@Nullable String textQuery, int pageSize) {
+    IssueQuery query = IssueQuery.builder()
+      .checkAuthorization(false)
+      .build();
+    return issueIndex.listTags(query, textQuery, pageSize);
   }
 
-  public Collection<String> listAuthors(@Nullable String query, int pageSize) {
-    return indexClient.get(IssueIndex.class).listAuthorsMatching(query, pageSize);
+  public List<String> listAuthors(@Nullable String textQuery, int pageSize) {
+    IssueQuery query = IssueQuery.builder()
+      .checkAuthorization(false)
+      .build();
+    return issueIndex.listAuthors(query, textQuery, pageSize);
   }
 
   public Collection<String> setTags(String issueKey, Collection<String> tags) {
@@ -379,8 +395,17 @@ public class IssueService implements ServerComponent {
     }
   }
 
+  // TODO check compatibility with Views, projects, etc.
   public Map<String, Long> listTagsForComponent(String componentUuid, int pageSize) {
-    return indexClient.get(IssueIndex.class).listTagsForComponent(componentUuid, pageSize);
+    IssueQuery query = IssueQuery.builder()
+      .userLogin(UserSession.get().login())
+      .userGroups(UserSession.get().userGroups())
+      .moduleRootUuids(Arrays.asList(componentUuid))
+      .onComponentOnly(false)
+      .setContextualized(true)
+      .resolved(false)
+      .build();
+    return issueIndex.countTags(query, pageSize);
   }
 
   @CheckForNull
index 49aa95a09ac9dfd5d2db3a9ac0a46babc8c591c7..1bf7d3ee49b143b559cf35a742a64b69629a4f22 100644 (file)
@@ -30,10 +30,9 @@ import org.sonar.core.issue.IssueUpdater;
 import org.sonar.server.issue.actionplan.ActionPlanService;
 import org.sonar.server.user.UserSession;
 
-import java.util.List;
+import java.util.Collection;
 import java.util.Map;
 
-
 public class PlanAction extends Action implements ServerComponent {
 
   public static final String KEY = "plan";
@@ -50,7 +49,7 @@ public class PlanAction extends Action implements ServerComponent {
   }
 
   @Override
-  public boolean verify(Map<String, Object> properties, List<Issue> issues, UserSession userSession) {
+  public boolean verify(Map<String, Object> properties, Collection<Issue> issues, UserSession userSession) {
     String actionPlanValue = planValue(properties);
     if (!Strings.isNullOrEmpty(actionPlanValue)) {
       ActionPlan actionPlan = selectActionPlan(actionPlanValue, userSession);
@@ -67,7 +66,7 @@ public class PlanAction extends Action implements ServerComponent {
 
   @Override
   public boolean execute(Map<String, Object> properties, Context context) {
-    if(!properties.containsKey(VERIFIED_ACTION_PLAN)) {
+    if (!properties.containsKey(VERIFIED_ACTION_PLAN)) {
       throw new IllegalArgumentException("Action plan is missing from the execution parameters");
     }
     ActionPlan actionPlan = (ActionPlan) properties.get(VERIFIED_ACTION_PLAN);
@@ -78,7 +77,7 @@ public class PlanAction extends Action implements ServerComponent {
     return (String) properties.get("plan");
   }
 
-  private void verifyIssuesAreAllRelatedOnActionPlanProject(List<Issue> issues, ActionPlan actionPlan) {
+  private void verifyIssuesAreAllRelatedOnActionPlanProject(Collection<Issue> issues, ActionPlan actionPlan) {
     String projectKey = actionPlan.projectKey();
     for (Issue issue : issues) {
       DefaultIssue defaultIssue = (DefaultIssue) issue;
index a48f22c4e2a89ea63e230f6f8df134733ec53250..7a8efdc62b4ae5b18c093b56383276bfde45c876 100644 (file)
@@ -30,7 +30,7 @@ import org.sonar.api.web.UserRole;
 import org.sonar.core.issue.IssueUpdater;
 import org.sonar.server.user.UserSession;
 
-import java.util.List;
+import java.util.Collection;
 import java.util.Map;
 
 public class SetSeverityAction extends Action implements ServerComponent {
@@ -45,7 +45,7 @@ public class SetSeverityAction extends Action implements ServerComponent {
     super.setConditions(new IsUnResolved(), new Condition() {
       @Override
       public boolean matches(Issue issue) {
-        return isCurrentUserIssueAdmin(((DefaultIssue) issue).projectKey());
+        return isCurrentUserIssueAdmin(issue.projectKey());
       }
     });
   }
@@ -55,7 +55,7 @@ public class SetSeverityAction extends Action implements ServerComponent {
   }
 
   @Override
-  public boolean verify(Map<String, Object> properties, List<Issue> issues, UserSession userSession) {
+  public boolean verify(Map<String, Object> properties, Collection<Issue> issues, UserSession userSession) {
     severity(properties);
     return true;
   }
index b2c39ccaa9baf88ba479eded085921ffc8d00c11..bb18afbfdda7181b79a32067ac6f361afcfe8ecb 100644 (file)
@@ -31,7 +31,7 @@ import org.sonar.core.issue.workflow.IssueWorkflow;
 import org.sonar.core.issue.workflow.Transition;
 import org.sonar.server.user.UserSession;
 
-import java.util.List;
+import java.util.Collection;
 import java.util.Map;
 
 public class TransitionAction extends Action implements ServerComponent {
@@ -46,7 +46,7 @@ public class TransitionAction extends Action implements ServerComponent {
   }
 
   @Override
-  public boolean verify(Map<String, Object> properties, List<Issue> issues, UserSession userSession) {
+  public boolean verify(Map<String, Object> properties, Collection<Issue> issues, UserSession userSession) {
     transition(properties);
     return true;
   }
index f8bd5f91d5312cd173386fbf53db5234917c764c..958135fb50d91894a78db4e550d15dcd5a25655d 100644 (file)
@@ -28,8 +28,6 @@ import org.sonar.core.persistence.MyBatis;
 import org.sonar.server.exceptions.NotFoundException;
 
 import javax.annotation.CheckForNull;
-
-import java.util.Collection;
 import java.util.List;
 
 public class IssueDao extends org.sonar.core.issue.db.IssueDao implements DaoComponent {
@@ -55,7 +53,7 @@ public class IssueDao extends org.sonar.core.issue.db.IssueDao implements DaoCom
     return mapper(session).selectByActionPlan(actionPlan);
   }
 
-  public List<IssueDto> selectByKeys(DbSession session, Collection<String> keys) {
+  public List<IssueDto> selectByKeys(DbSession session, List<String> keys) {
     return mapper(session).selectByKeys(keys);
   }
 
index bf53ae3f6c3eb45d2282cdae68e7ce61be82c5b4..df6025a9b34ffe54793dc515c07f90ab69296167 100644 (file)
@@ -25,7 +25,6 @@ import com.google.common.base.Predicate;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Maps;
 import org.sonar.api.ServerComponent;
-import org.sonar.api.issue.Issue;
 import org.sonar.api.utils.Paging;
 import org.sonar.core.issue.DefaultIssueFilter;
 import org.sonar.core.issue.IssueFilterSerializer;
@@ -35,14 +34,15 @@ import org.sonar.core.issue.db.IssueFilterFavouriteDao;
 import org.sonar.core.issue.db.IssueFilterFavouriteDto;
 import org.sonar.core.permission.GlobalPermissions;
 import org.sonar.core.user.AuthorizationDao;
+import org.sonar.server.es.SearchOptions;
+import org.sonar.server.es.SearchResult;
 import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.exceptions.NotFoundException;
 import org.sonar.server.exceptions.UnauthorizedException;
 import org.sonar.server.issue.IssueQuery;
+import org.sonar.server.issue.index.IssueDoc;
 import org.sonar.server.issue.index.IssueIndex;
-import org.sonar.server.search.QueryContext;
-import org.sonar.server.search.Result;
 import org.sonar.server.user.UserSession;
 
 import javax.annotation.CheckForNull;
@@ -61,8 +61,8 @@ public class IssueFilterService implements ServerComponent {
   private final IssueFilterSerializer serializer;
 
   public IssueFilterService(IssueFilterDao filterDao, IssueFilterFavouriteDao favouriteDao,
-                            IssueIndex issueIndex, AuthorizationDao authorizationDao,
-                            IssueFilterSerializer serializer) {
+    IssueIndex issueIndex, AuthorizationDao authorizationDao,
+    IssueFilterSerializer serializer) {
     this.filterDao = filterDao;
     this.favouriteDao = favouriteDao;
     this.issueIndex = issueIndex;
@@ -70,8 +70,8 @@ public class IssueFilterService implements ServerComponent {
     this.serializer = serializer;
   }
 
-  public IssueFilterResult execute(IssueQuery issueQuery, QueryContext context) {
-    return createIssueFilterResult(issueIndex.search(issueQuery, context), context);
+  public IssueFilterResult execute(IssueQuery issueQuery, SearchOptions options) {
+    return createIssueFilterResult(issueIndex.search(issueQuery, options), options);
   }
 
   public DefaultIssueFilter find(Long id, UserSession userSession) {
@@ -335,8 +335,8 @@ public class IssueFilterService implements ServerComponent {
     return authorizationDao.selectGlobalPermissions(user).contains(GlobalPermissions.SYSTEM_ADMIN);
   }
 
-  private IssueFilterResult createIssueFilterResult(Result<Issue> issues, QueryContext context) {
-    return new IssueFilterResult(issues.getHits(), Paging.create(context.getLimit(), context.getPage(), ((Long) issues.getTotal()).intValue()));
+  private IssueFilterResult createIssueFilterResult(SearchResult<IssueDoc> issues, SearchOptions options) {
+    return new IssueFilterResult(issues.getDocs(), Paging.create(options.getLimit(), options.getPage(), (int) issues.getTotal()));
   }
 
   private boolean hasUserSharingPermission(String user) {
@@ -345,21 +345,21 @@ public class IssueFilterService implements ServerComponent {
 
   public static class IssueFilterResult {
 
-    private final List<Issue> issues;
+    private final List<IssueDoc> issues;
     private final Paging paging;
 
-    public IssueFilterResult(List<Issue> issues, Paging paging) {
+    public IssueFilterResult(List<IssueDoc> issues, Paging paging) {
       this.issues = issues;
       this.paging = paging;
     }
 
-    public List<Issue> issues(){
+    public List<IssueDoc> issues() {
       return issues;
     }
 
-    public Paging paging(){
+    public Paging paging() {
       return paging;
     }
-  }  
-  
+  }
+
 }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/index/FakeIssueDto.java b/server/sonar-server/src/main/java/org/sonar/server/issue/index/FakeIssueDto.java
deleted file mode 100644 (file)
index 0db9d5d..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * 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.issue.index;
-
-import org.sonar.core.persistence.Dto;
-
-public class FakeIssueDto extends Dto<String> {
-  @Override
-  public String getKey() {
-    throw new UnsupportedOperationException();
-  }
-}
index 42064038b101b375ed4eddaa70592a13ea444b22..6e42d8c5b21635baa75dfe97f9facdaaf92f65c0 100644 (file)
 package org.sonar.server.issue.index;
 
 import com.google.common.base.Function;
-import com.google.common.base.Preconditions;
-import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import org.apache.commons.lang.BooleanUtils;
 import org.elasticsearch.action.search.SearchRequestBuilder;
 import org.elasticsearch.action.search.SearchResponse;
 import org.elasticsearch.action.search.SearchType;
-import org.elasticsearch.common.unit.TimeValue;
-import org.elasticsearch.index.query.*;
+import org.elasticsearch.index.query.BoolFilterBuilder;
+import org.elasticsearch.index.query.FilterBuilder;
+import org.elasticsearch.index.query.FilterBuilders;
+import org.elasticsearch.index.query.OrFilterBuilder;
+import org.elasticsearch.index.query.QueryBuilder;
+import org.elasticsearch.index.query.QueryBuilders;
 import org.elasticsearch.search.aggregations.AggregationBuilder;
 import org.elasticsearch.search.aggregations.AggregationBuilders;
 import org.elasticsearch.search.aggregations.bucket.filter.FilterAggregationBuilder;
-import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogram.Interval;
+import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogram;
 import org.elasticsearch.search.aggregations.bucket.missing.InternalMissing;
 import org.elasticsearch.search.aggregations.bucket.terms.Terms;
-import org.elasticsearch.search.aggregations.bucket.terms.Terms.Bucket;
-import org.elasticsearch.search.aggregations.bucket.terms.Terms.Order;
 import org.elasticsearch.search.aggregations.bucket.terms.TermsBuilder;
 import org.elasticsearch.search.aggregations.metrics.min.Min;
 import org.joda.time.Duration;
@@ -45,10 +46,16 @@ import org.sonar.api.issue.Issue;
 import org.sonar.api.rule.Severity;
 import org.sonar.api.utils.DateUtils;
 import org.sonar.api.utils.System2;
+import org.sonar.server.es.BaseIndex;
+import org.sonar.server.es.EsClient;
+import org.sonar.server.es.EsUtils;
+import org.sonar.server.es.SearchOptions;
+import org.sonar.server.es.SearchResult;
 import org.sonar.server.es.Sorting;
+import org.sonar.server.exceptions.NotFoundException;
 import org.sonar.server.issue.IssueQuery;
 import org.sonar.server.issue.filter.IssueFilterParameters;
-import org.sonar.server.search.*;
+import org.sonar.server.search.StickyFacetBuilder;
 import org.sonar.server.user.UserSession;
 import org.sonar.server.view.index.ViewIndexDefinition;
 
@@ -56,179 +63,155 @@ import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
 
 import java.text.SimpleDateFormat;
-import java.util.*;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TimeZone;
 
 import static com.google.common.collect.Lists.newArrayList;
 
-public class IssueIndex extends BaseIndex<Issue, FakeIssueDto, String> {
-
+/**
+ * The unique entry-point to interact with Elasticsearch index "issues".
+ * All the requests are listed here.
+ */
+public class IssueIndex extends BaseIndex {
+
+  public static final List<String> SUPPORTED_FACETS = ImmutableList.of(
+    IssueFilterParameters.SEVERITIES,
+    IssueFilterParameters.STATUSES,
+    IssueFilterParameters.RESOLUTIONS,
+    IssueFilterParameters.ACTION_PLANS,
+    IssueFilterParameters.PROJECT_UUIDS,
+    IssueFilterParameters.RULES,
+    IssueFilterParameters.ASSIGNEES,
+    IssueFilterParameters.REPORTERS,
+    IssueFilterParameters.AUTHORS,
+    IssueFilterParameters.MODULE_UUIDS,
+    IssueFilterParameters.FILE_UUIDS,
+    IssueFilterParameters.DIRECTORIES,
+    IssueFilterParameters.LANGUAGES,
+    IssueFilterParameters.TAGS,
+    IssueFilterParameters.CREATED_AT);
+
+  // TODO to be documented
   private static final String FILTER_COMPONENT_ROOT = "__componentRoot";
 
+  // TODO to be documented
+  // TODO move to Facets ?
   private static final String FACET_SUFFIX_MISSING = "_missing";
 
-  private static final int DEFAULT_ISSUE_FACET_SIZE = 15;
-
+  private static final int DEFAULT_FACET_SIZE = 15;
   private static final Duration TWENTY_DAYS = Duration.standardDays(20L);
   private static final Duration TWENTY_WEEKS = Duration.standardDays(20L * 7L);
   private static final Duration TWENTY_MONTHS = Duration.standardDays(20L * 30L);
 
-  private final Sorting sorting;
+  /**
+   * Convert an Elasticsearch result (a map) to an {@link org.sonar.server.issue.index.IssueDoc}. It's
+   * used for {@link org.sonar.server.es.SearchResult}.
+   */
+  private static final Function<Map<String, Object>, IssueDoc> DOC_CONVERTER = new Function<Map<String, Object>, IssueDoc>() {
+    @Override
+    public IssueDoc apply(Map<String, Object> input) {
+      return new IssueDoc(input);
+    }
+  };
 
+  private final Sorting sorting;
   private final System2 system;
 
-  public IssueIndex(SearchClient client, System2 system) {
-    super(IndexDefinition.ISSUES, null, client);
-    this.system = system;
+  public IssueIndex(EsClient client, System2 system) {
+    super(client);
 
-    sorting = new Sorting();
-    sorting.add(IssueQuery.SORT_BY_ASSIGNEE, IssueIndexDefinition.FIELD_ISSUE_ASSIGNEE);
-    sorting.add(IssueQuery.SORT_BY_STATUS, IssueIndexDefinition.FIELD_ISSUE_STATUS);
-    sorting.add(IssueQuery.SORT_BY_SEVERITY, IssueIndexDefinition.FIELD_ISSUE_SEVERITY_VALUE);
-    sorting.add(IssueQuery.SORT_BY_CREATION_DATE, IssueIndexDefinition.FIELD_ISSUE_FUNC_CREATED_AT);
-    sorting.add(IssueQuery.SORT_BY_UPDATE_DATE, IssueIndexDefinition.FIELD_ISSUE_FUNC_UPDATED_AT);
-    sorting.add(IssueQuery.SORT_BY_CLOSE_DATE, IssueIndexDefinition.FIELD_ISSUE_FUNC_CLOSED_AT);
-    sorting.add(IssueQuery.SORT_BY_FILE_LINE, IssueIndexDefinition.FIELD_ISSUE_PROJECT_UUID);
-    sorting.add(IssueQuery.SORT_BY_FILE_LINE, IssueIndexDefinition.FIELD_ISSUE_FILE_PATH);
-    sorting.add(IssueQuery.SORT_BY_FILE_LINE, IssueIndexDefinition.FIELD_ISSUE_LINE);
-    sorting.add(IssueQuery.SORT_BY_FILE_LINE, IssueIndexDefinition.FIELD_ISSUE_SEVERITY_VALUE).reverse();
-    sorting.add(IssueQuery.SORT_BY_FILE_LINE, IssueIndexDefinition.FIELD_ISSUE_KEY);
+    this.system = system;
+    this.sorting = new Sorting();
+    this.sorting.add(IssueQuery.SORT_BY_ASSIGNEE, IssueIndexDefinition.FIELD_ISSUE_ASSIGNEE);
+    this.sorting.add(IssueQuery.SORT_BY_STATUS, IssueIndexDefinition.FIELD_ISSUE_STATUS);
+    this.sorting.add(IssueQuery.SORT_BY_SEVERITY, IssueIndexDefinition.FIELD_ISSUE_SEVERITY_VALUE);
+    this.sorting.add(IssueQuery.SORT_BY_CREATION_DATE, IssueIndexDefinition.FIELD_ISSUE_FUNC_CREATED_AT);
+    this.sorting.add(IssueQuery.SORT_BY_UPDATE_DATE, IssueIndexDefinition.FIELD_ISSUE_FUNC_UPDATED_AT);
+    this.sorting.add(IssueQuery.SORT_BY_CLOSE_DATE, IssueIndexDefinition.FIELD_ISSUE_FUNC_CLOSED_AT);
+    this.sorting.add(IssueQuery.SORT_BY_FILE_LINE, IssueIndexDefinition.FIELD_ISSUE_PROJECT_UUID);
+    this.sorting.add(IssueQuery.SORT_BY_FILE_LINE, IssueIndexDefinition.FIELD_ISSUE_FILE_PATH);
+    this.sorting.add(IssueQuery.SORT_BY_FILE_LINE, IssueIndexDefinition.FIELD_ISSUE_LINE);
+    this.sorting.add(IssueQuery.SORT_BY_FILE_LINE, IssueIndexDefinition.FIELD_ISSUE_SEVERITY_VALUE).reverse();
+    this.sorting.add(IssueQuery.SORT_BY_FILE_LINE, IssueIndexDefinition.FIELD_ISSUE_KEY);
 
     // by default order by updated date and issue key (in order to be deterministic when same ms)
-    sorting.addDefault(IssueIndexDefinition.FIELD_ISSUE_FUNC_UPDATED_AT).reverse();
-    sorting.addDefault(IssueIndexDefinition.FIELD_ISSUE_KEY);
-  }
-
-  @Override
-  protected void initializeIndex() {
-    // replaced by IssueIndexDefinition
-  }
-
-  @Override
-  protected String getKeyValue(String keyString) {
-    return keyString;
-  }
-
-  @Override
-  protected Map mapProperties() {
-    throw new UnsupportedOperationException("Being refactored");
+    this.sorting.addDefault(IssueIndexDefinition.FIELD_ISSUE_FUNC_UPDATED_AT).reverse();
+    this.sorting.addDefault(IssueIndexDefinition.FIELD_ISSUE_KEY);
   }
 
-  @Override
-  protected Map mapKey() {
-    throw new UnsupportedOperationException("Being refactored");
-  }
-
-  @Override
-  protected IssueDoc toDoc(Map<String, Object> fields) {
-    Preconditions.checkNotNull(fields, "Cannot construct Issue with null response");
-    return new IssueDoc(fields);
-  }
-
-  @Override
-  public Issue getNullableByKey(String key) {
-    Result<Issue> result = search(IssueQuery.builder().issueKeys(newArrayList(key)).build(), new QueryContext());
+  /**
+   * Warning, this method is not efficient as routing (the project uuid) is not known.
+   * All the ES cluster nodes are involved.
+   */
+  @CheckForNull
+  public IssueDoc getNullableByKey(String key) {
+    SearchResult<IssueDoc> result = search(IssueQuery.builder().issueKeys(newArrayList(key)).build(), new SearchOptions());
     if (result.getTotal() == 1) {
-      return result.getHits().get(0);
+      return result.getDocs().get(0);
     }
     return null;
   }
 
-  public List<FacetValue> listAssignees(IssueQuery query) {
-    QueryContext queryContext = new QueryContext().setPage(1, 0);
-
-    SearchRequestBuilder esSearch = getClient()
-      .prepareSearch(IssueIndexDefinition.INDEX)
-      .setTypes(IssueIndexDefinition.TYPE_ISSUE);
-
-    QueryBuilder esQuery = QueryBuilders.matchAllQuery();
-    BoolFilterBuilder esFilter = getFilter(query, queryContext);
-    if (esFilter.hasClauses()) {
-      esSearch.setQuery(QueryBuilders.filteredQuery(esQuery, esFilter));
-    } else {
-      esSearch.setQuery(esQuery);
+  /**
+   * Warning, see {@link #getNullableByKey(String)}.
+   * A {@link org.sonar.server.exceptions.NotFoundException} is thrown if key does not exist.
+   */
+  public IssueDoc getByKey(String key) {
+    IssueDoc value = getNullableByKey(key);
+    if (value == null) {
+      throw new NotFoundException(String.format("Issue with key '%s' does not exist", key));
     }
-    esSearch.addAggregation(AggregationBuilders.terms(IssueIndexDefinition.FIELD_ISSUE_ASSIGNEE)
-      .size(Integer.MAX_VALUE)
-      .field(IssueIndexDefinition.FIELD_ISSUE_ASSIGNEE));
-    esSearch.addAggregation(AggregationBuilders.missing("notAssigned")
-      .field(IssueIndexDefinition.FIELD_ISSUE_ASSIGNEE));
-
-    SearchResponse response = esSearch.get();
-    Terms aggregation = (Terms) response.getAggregations().getAsMap().get(IssueIndexDefinition.FIELD_ISSUE_ASSIGNEE);
-    List<FacetValue> facetValues = newArrayList();
-    for (Terms.Bucket value : aggregation.getBuckets()) {
-      facetValues.add(new FacetValue(value.getKey(), value.getDocCount()));
-    }
-    facetValues.add(new FacetValue("_notAssigned_", ((InternalMissing) response.getAggregations().get("notAssigned")).getDocCount()));
-
-    return facetValues;
+    return value;
   }
 
-  public Result<Issue> search(IssueQuery query, QueryContext options) {
-    SearchRequestBuilder esSearch = getClient()
+  public SearchResult<IssueDoc> search(IssueQuery query, SearchOptions options) {
+    SearchRequestBuilder requestBuilder = getClient()
       .prepareSearch(IssueIndexDefinition.INDEX)
       .setTypes(IssueIndexDefinition.TYPE_ISSUE);
 
-    if (options.isScroll()) {
-      esSearch.setSearchType(SearchType.SCAN);
-      esSearch.setScroll(TimeValue.timeValueMinutes(3));
-    }
-
-    setSorting(query, esSearch);
-    setPagination(options, esSearch);
+    configureSorting(query, requestBuilder);
+    configurePagination(options, requestBuilder);
 
     QueryBuilder esQuery = QueryBuilders.matchAllQuery();
-    Map<String, FilterBuilder> filters = getFilters(query, options);
-    setQueryFilter(esSearch, esQuery, filters);
-
-    setFacets(query, options, filters, esQuery, esSearch);
-
-    SearchResponse response = esSearch.get();
-    return new Result<>(this, response);
-  }
-
-  protected void setQueryFilter(SearchRequestBuilder esSearch, QueryBuilder esQuery, Map<String, FilterBuilder> filters) {
     BoolFilterBuilder esFilter = FilterBuilders.boolFilter();
+    Map<String, FilterBuilder> filters = createFilters(query);
     for (FilterBuilder filter : filters.values()) {
       if (filter != null) {
         esFilter.must(filter);
       }
     }
-
     if (esFilter.hasClauses()) {
-      esSearch.setQuery(QueryBuilders.filteredQuery(esQuery, esFilter));
+      requestBuilder.setQuery(QueryBuilders.filteredQuery(esQuery, esFilter));
     } else {
-      esSearch.setQuery(esQuery);
+      requestBuilder.setQuery(esQuery);
     }
+
+    configureStickyFacets(query, options, filters, esQuery, requestBuilder);
+    return new SearchResult<>(requestBuilder.get(), DOC_CONVERTER);
   }
 
-  private BoolFilterBuilder getFilter(IssueQuery query, QueryContext options) {
-    BoolFilterBuilder esFilter = FilterBuilders.boolFilter();
-    for (FilterBuilder filter : getFilters(query, options).values()) {
-      if (filter != null) {
-        esFilter.must(filter);
-      }
+  private void configureSorting(IssueQuery query, SearchRequestBuilder esRequest) {
+    String sortField = query.sort();
+    if (sortField != null) {
+      boolean asc = BooleanUtils.isTrue(query.asc());
+      sorting.fill(esRequest, sortField, asc);
+    } else {
+      sorting.fillDefault(esRequest);
     }
-    return esFilter;
   }
 
-  public void deleteClosedIssuesOfProjectBefore(String uuid, Date beforeDate) {
-    FilterBuilder projectFilter = FilterBuilders.boolFilter().must(FilterBuilders.termsFilter(IssueIndexDefinition.FIELD_ISSUE_PROJECT_UUID, uuid));
-    FilterBuilder dateFilter = FilterBuilders.rangeFilter(IssueIndexDefinition.FIELD_ISSUE_FUNC_CLOSED_AT).lt(beforeDate.getTime());
-    QueryBuilder queryBuilder = QueryBuilders.filteredQuery(
-      QueryBuilders.matchAllQuery(),
-      FilterBuilders.andFilter(projectFilter, dateFilter)
-      );
-
-    getClient().prepareDeleteByQuery(IssueIndexDefinition.INDEX).setQuery(queryBuilder).get();
+  protected void configurePagination(SearchOptions options, SearchRequestBuilder esSearch) {
+    esSearch.setFrom(options.getOffset()).setSize(options.getLimit());
   }
 
-  /* Build main filter (match based) */
-  protected Map<String, FilterBuilder> getFilters(IssueQuery query, QueryContext options) {
-
-    Map<String, FilterBuilder> filters = Maps.newHashMap();
-
-    filters.put("__authorization", getAuthorizationFilter(options));
+  private Map<String, FilterBuilder> createFilters(IssueQuery query) {
+    Map<String, FilterBuilder> filters = new HashMap<>();
+    filters.put("__authorization", createAuthorizationFilter(query));
 
     // Issue is assigned Filter
     String isAssigned = "__isAssigned";
@@ -255,20 +238,20 @@ public class IssueIndex extends BaseIndex<Issue, FakeIssueDto, String> {
     }
 
     // Field Filters
-    filters.put(IssueIndexDefinition.FIELD_ISSUE_KEY, matchFilter(IssueIndexDefinition.FIELD_ISSUE_KEY, query.issueKeys()));
-    filters.put(IssueIndexDefinition.FIELD_ISSUE_ACTION_PLAN, matchFilter(IssueIndexDefinition.FIELD_ISSUE_ACTION_PLAN, query.actionPlans()));
-    filters.put(IssueIndexDefinition.FIELD_ISSUE_ASSIGNEE, matchFilter(IssueIndexDefinition.FIELD_ISSUE_ASSIGNEE, query.assignees()));
+    filters.put(IssueIndexDefinition.FIELD_ISSUE_KEY, createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_KEY, query.issueKeys()));
+    filters.put(IssueIndexDefinition.FIELD_ISSUE_ACTION_PLAN, createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_ACTION_PLAN, query.actionPlans()));
+    filters.put(IssueIndexDefinition.FIELD_ISSUE_ASSIGNEE, createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_ASSIGNEE, query.assignees()));
 
     addComponentRelatedFilters(query, filters);
 
-    filters.put(IssueIndexDefinition.FIELD_ISSUE_LANGUAGE, matchFilter(IssueIndexDefinition.FIELD_ISSUE_LANGUAGE, query.languages()));
-    filters.put(IssueIndexDefinition.FIELD_ISSUE_TAGS, matchFilter(IssueIndexDefinition.FIELD_ISSUE_TAGS, query.tags()));
-    filters.put(IssueIndexDefinition.FIELD_ISSUE_RESOLUTION, matchFilter(IssueIndexDefinition.FIELD_ISSUE_RESOLUTION, query.resolutions()));
-    filters.put(IssueIndexDefinition.FIELD_ISSUE_REPORTER, matchFilter(IssueIndexDefinition.FIELD_ISSUE_REPORTER, query.reporters()));
-    filters.put(IssueIndexDefinition.FIELD_ISSUE_AUTHOR_LOGIN, matchFilter(IssueIndexDefinition.FIELD_ISSUE_AUTHOR_LOGIN, query.authors()));
-    filters.put(IssueIndexDefinition.FIELD_ISSUE_RULE_KEY, matchFilter(IssueIndexDefinition.FIELD_ISSUE_RULE_KEY, query.rules()));
-    filters.put(IssueIndexDefinition.FIELD_ISSUE_SEVERITY, matchFilter(IssueIndexDefinition.FIELD_ISSUE_SEVERITY, query.severities()));
-    filters.put(IssueIndexDefinition.FIELD_ISSUE_STATUS, matchFilter(IssueIndexDefinition.FIELD_ISSUE_STATUS, query.statuses()));
+    filters.put(IssueIndexDefinition.FIELD_ISSUE_LANGUAGE, createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_LANGUAGE, query.languages()));
+    filters.put(IssueIndexDefinition.FIELD_ISSUE_TAGS, createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_TAGS, query.tags()));
+    filters.put(IssueIndexDefinition.FIELD_ISSUE_RESOLUTION, createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_RESOLUTION, query.resolutions()));
+    filters.put(IssueIndexDefinition.FIELD_ISSUE_REPORTER, createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_REPORTER, query.reporters()));
+    filters.put(IssueIndexDefinition.FIELD_ISSUE_AUTHOR_LOGIN, createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_AUTHOR_LOGIN, query.authors()));
+    filters.put(IssueIndexDefinition.FIELD_ISSUE_RULE_KEY, createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_RULE_KEY, query.rules()));
+    filters.put(IssueIndexDefinition.FIELD_ISSUE_SEVERITY, createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_SEVERITY, query.severities()));
+    filters.put(IssueIndexDefinition.FIELD_ISSUE_STATUS, createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_STATUS, query.statuses()));
 
     addDatesFilter(filters, query);
 
@@ -276,14 +259,14 @@ public class IssueIndex extends BaseIndex<Issue, FakeIssueDto, String> {
   }
 
   private void addComponentRelatedFilters(IssueQuery query, Map<String, FilterBuilder> filters) {
-    FilterBuilder viewFilter = viewFilter(query.viewUuids());
-    FilterBuilder componentFilter = matchFilter(IssueIndexDefinition.FIELD_ISSUE_COMPONENT_UUID, query.componentUuids());
-    FilterBuilder projectFilter = matchFilter(IssueIndexDefinition.FIELD_ISSUE_PROJECT_UUID, query.projectUuids());
-    FilterBuilder moduleRootFilter = moduleRootFilter(query.moduleRootUuids());
-    FilterBuilder moduleFilter = matchFilter(IssueIndexDefinition.FIELD_ISSUE_MODULE_UUID, query.moduleUuids());
-    FilterBuilder directoryRootFilter = directoryRootFilter(query.moduleUuids(), query.directories());
-    FilterBuilder directoryFilter = matchFilter(IssueIndexDefinition.FIELD_ISSUE_DIRECTORY_PATH, query.directories());
-    FilterBuilder fileFilter = matchFilter(IssueIndexDefinition.FIELD_ISSUE_COMPONENT_UUID, query.fileUuids());
+    FilterBuilder viewFilter = createViewFilter(query.viewUuids());
+    FilterBuilder componentFilter = createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_COMPONENT_UUID, query.componentUuids());
+    FilterBuilder projectFilter = createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_PROJECT_UUID, query.projectUuids());
+    FilterBuilder moduleRootFilter = createModuleRootFilter(query.moduleRootUuids());
+    FilterBuilder moduleFilter = createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_MODULE_UUID, query.moduleUuids());
+    FilterBuilder directoryRootFilter = createDirectoryRootFilter(query.moduleUuids(), query.directories());
+    FilterBuilder directoryFilter = createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_DIRECTORY_PATH, query.directories());
+    FilterBuilder fileFilter = createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_COMPONENT_UUID, query.fileUuids());
 
     if (BooleanUtils.isTrue(query.isContextualized())) {
       if (viewFilter != null) {
@@ -325,12 +308,12 @@ public class IssueIndex extends BaseIndex<Issue, FakeIssueDto, String> {
   }
 
   @CheckForNull
-  private FilterBuilder moduleRootFilter(Collection<String> componentUuids) {
+  private FilterBuilder createModuleRootFilter(Collection<String> componentUuids) {
     if (componentUuids.isEmpty()) {
       return null;
     }
-    FilterBuilder componentFilter = matchFilter(IssueIndexDefinition.FIELD_ISSUE_COMPONENT_UUID, componentUuids);
-    FilterBuilder modulePathFilter = matchFilter(IssueIndexDefinition.FIELD_ISSUE_MODULE_PATH, componentUuids);
+    FilterBuilder componentFilter = createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_COMPONENT_UUID, componentUuids);
+    FilterBuilder modulePathFilter = createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_MODULE_PATH, componentUuids);
     FilterBuilder compositeFilter = null;
     if (componentFilter != null) {
       if (modulePathFilter != null) {
@@ -341,14 +324,15 @@ public class IssueIndex extends BaseIndex<Issue, FakeIssueDto, String> {
     } else if (modulePathFilter != null) {
       compositeFilter = modulePathFilter;
     }
+    System.out.println("compositeFilter:" + compositeFilter);
     return compositeFilter;
   }
 
   @CheckForNull
-  private FilterBuilder directoryRootFilter(Collection<String> moduleUuids, Collection<String> directoryPaths) {
+  private FilterBuilder createDirectoryRootFilter(Collection<String> moduleUuids, Collection<String> directoryPaths) {
     BoolFilterBuilder directoryTop = null;
-    FilterBuilder moduleFilter = matchFilter(IssueIndexDefinition.FIELD_ISSUE_MODULE_UUID, moduleUuids);
-    FilterBuilder directoryFilter = matchFilter(IssueIndexDefinition.FIELD_ISSUE_DIRECTORY_PATH, directoryPaths);
+    FilterBuilder moduleFilter = createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_MODULE_UUID, moduleUuids);
+    FilterBuilder directoryFilter = createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_DIRECTORY_PATH, directoryPaths);
     if (moduleFilter != null) {
       directoryTop = FilterBuilders.boolFilter();
       directoryTop.must(moduleFilter);
@@ -363,7 +347,7 @@ public class IssueIndex extends BaseIndex<Issue, FakeIssueDto, String> {
   }
 
   @CheckForNull
-  private FilterBuilder viewFilter(Collection<String> viewUuids) {
+  private FilterBuilder createViewFilter(Collection<String> viewUuids) {
     if (viewUuids.isEmpty()) {
       return null;
     }
@@ -381,26 +365,29 @@ public class IssueIndex extends BaseIndex<Issue, FakeIssueDto, String> {
   }
 
   public static String viewsLookupCacheKey(String viewUuid) {
-    return IssueIndexDefinition.TYPE_ISSUE + viewUuid + ViewIndexDefinition.TYPE_VIEW;
+    return String.format("%s%s%s", IssueIndexDefinition.TYPE_ISSUE, viewUuid, ViewIndexDefinition.TYPE_VIEW);
   }
 
-  private FilterBuilder getAuthorizationFilter(QueryContext options) {
-    String user = options.getUserLogin();
-    Set<String> groups = options.getUserGroups();
-    OrFilterBuilder groupsAndUser = FilterBuilders.orFilter();
-    if (user != null) {
-      groupsAndUser.add(FilterBuilders.termFilter(IssueIndexDefinition.FIELD_AUTHORIZATION_USERS, user));
-    }
-    for (String group : groups) {
-      groupsAndUser.add(FilterBuilders.termFilter(IssueIndexDefinition.FIELD_AUTHORIZATION_GROUPS, group));
+  private FilterBuilder createAuthorizationFilter(IssueQuery query) {
+    if (query.checkAuthorization()) {
+      String user = query.userLogin();
+      OrFilterBuilder groupsAndUser = FilterBuilders.orFilter();
+      if (user != null) {
+        groupsAndUser.add(FilterBuilders.termFilter(IssueIndexDefinition.FIELD_AUTHORIZATION_USERS, user));
+      }
+      for (String group : query.userGroups()) {
+        groupsAndUser.add(FilterBuilders.termFilter(IssueIndexDefinition.FIELD_AUTHORIZATION_GROUPS, group));
+      }
+      return FilterBuilders.hasParentFilter(IssueIndexDefinition.TYPE_AUTHORIZATION,
+        QueryBuilders.filteredQuery(
+          QueryBuilders.matchAllQuery(),
+          FilterBuilders.boolFilter()
+            .must(groupsAndUser)
+            .cache(true))
+        );
+    } else {
+      return FilterBuilders.matchAllFilter();
     }
-    return FilterBuilders.hasParentFilter(IssueIndexDefinition.TYPE_AUTHORIZATION,
-      QueryBuilders.filteredQuery(
-        QueryBuilders.matchAllQuery(),
-        FilterBuilders.boolFilter()
-          .must(groupsAndUser)
-          .cache(true))
-      );
   }
 
   private void addDatesFilter(Map<String, FilterBuilder> filters, IssueQuery query) {
@@ -424,9 +411,9 @@ public class IssueIndex extends BaseIndex<Issue, FakeIssueDto, String> {
     }
   }
 
-  private void setFacets(IssueQuery query, QueryContext options, Map<String, FilterBuilder> filters, QueryBuilder esQuery, SearchRequestBuilder esSearch) {
-    if (options.isFacet()) {
-      StickyFacetBuilder stickyFacetBuilder = stickyFacetBuilder(esQuery, filters);
+  private void configureStickyFacets(IssueQuery query, SearchOptions options, Map<String, FilterBuilder> filters, QueryBuilder esQuery, SearchRequestBuilder esSearch) {
+    if (!options.getFacets().isEmpty()) {
+      StickyFacetBuilder stickyFacetBuilder = new StickyFacetBuilder(esQuery, filters);
       // Execute Term aggregations
       addSimpleStickyFacetIfNeeded(options, stickyFacetBuilder, esSearch,
         IssueFilterParameters.SEVERITIES, IssueIndexDefinition.FIELD_ISSUE_SEVERITY, Severity.ALL.toArray());
@@ -450,33 +437,85 @@ public class IssueIndex extends BaseIndex<Issue, FakeIssueDto, String> {
       addSimpleStickyFacetIfNeeded(options, stickyFacetBuilder, esSearch,
         IssueFilterParameters.AUTHORS, IssueIndexDefinition.FIELD_ISSUE_AUTHOR_LOGIN, query.authors().toArray());
 
-      if (options.facets().contains(IssueFilterParameters.TAGS)) {
+      if (options.getFacets().contains(IssueFilterParameters.TAGS)) {
         esSearch.addAggregation(stickyFacetBuilder.buildStickyFacet(IssueIndexDefinition.FIELD_ISSUE_TAGS, IssueFilterParameters.TAGS, query.tags().toArray()));
       }
 
-      if (options.facets().contains(IssueFilterParameters.RESOLUTIONS)) {
-        esSearch.addAggregation(getResolutionFacet(filters, esQuery));
+      if (options.getFacets().contains(IssueFilterParameters.RESOLUTIONS)) {
+        esSearch.addAggregation(createResolutionFacet(filters, esQuery));
       }
-      if (options.facets().contains(IssueFilterParameters.ASSIGNEES)) {
-        esSearch.addAggregation(getAssigneesFacet(query, filters, esQuery));
+      if (options.getFacets().contains(IssueFilterParameters.ASSIGNEES)) {
+        esSearch.addAggregation(createAssigneesFacet(query, filters, esQuery));
       }
-      if (options.facets().contains(IssueFilterParameters.ACTION_PLANS)) {
-        esSearch.addAggregation(getActionPlansFacet(query, filters, esQuery));
+      if (options.getFacets().contains(IssueFilterParameters.ACTION_PLANS)) {
+        esSearch.addAggregation(createActionPlansFacet(query, filters, esQuery));
       }
-      if (options.facets().contains(IssueFilterParameters.CREATED_AT)) {
+      if (options.getFacets().contains(IssueFilterParameters.CREATED_AT)) {
         esSearch.addAggregation(getCreatedAtFacet(query, filters, esQuery));
       }
     }
   }
 
-  private void addSimpleStickyFacetIfNeeded(QueryContext options, StickyFacetBuilder stickyFacetBuilder, SearchRequestBuilder esSearch,
+  private void addSimpleStickyFacetIfNeeded(SearchOptions options, StickyFacetBuilder stickyFacetBuilder, SearchRequestBuilder esSearch,
     String facetName, String fieldName, Object... selectedValues) {
-    if (options.facets().contains(facetName)) {
-      esSearch.addAggregation(stickyFacetBuilder.buildStickyFacet(fieldName, facetName, DEFAULT_ISSUE_FACET_SIZE, selectedValues));
+    if (options.getFacets().contains(facetName)) {
+      esSearch.addAggregation(stickyFacetBuilder.buildStickyFacet(fieldName, facetName, DEFAULT_FACET_SIZE, selectedValues));
+    }
+  }
+
+  private AggregationBuilder getCreatedAtFacet(IssueQuery query, Map<String, FilterBuilder> filters, QueryBuilder esQuery) {
+    Date now = system.newDate();
+    SimpleDateFormat tzFormat = new SimpleDateFormat("XX");
+    tzFormat.setTimeZone(TimeZone.getDefault());
+    String timeZoneString = tzFormat.format(now);
+
+    DateHistogram.Interval bucketSize = DateHistogram.Interval.YEAR;
+    Date createdAfter = query.createdAfter();
+    long startTime = createdAfter == null ? getMinCreatedAt(filters, esQuery) : createdAfter.getTime();
+    Date createdBefore = query.createdBefore();
+    long endTime = createdBefore == null ? now.getTime() : createdBefore.getTime();
+    Duration timeSpan = new Duration(startTime, endTime);
+    if (timeSpan.isShorterThan(TWENTY_DAYS)) {
+      bucketSize = DateHistogram.Interval.DAY;
+    } else if (timeSpan.isShorterThan(TWENTY_WEEKS)) {
+      bucketSize = DateHistogram.Interval.WEEK;
+    } else if (timeSpan.isShorterThan(TWENTY_MONTHS)) {
+      bucketSize = DateHistogram.Interval.MONTH;
+    }
+
+    return AggregationBuilders.dateHistogram(IssueFilterParameters.CREATED_AT)
+      .field(IssueIndexDefinition.FIELD_ISSUE_FUNC_CREATED_AT)
+      .interval(bucketSize)
+      .minDocCount(0L)
+      .format(DateUtils.DATETIME_FORMAT)
+      .preZone(timeZoneString)
+      .postZone(timeZoneString)
+      .extendedBounds(startTime, endTime);
+  }
+
+  private long getMinCreatedAt(Map<String, FilterBuilder> filters, QueryBuilder esQuery) {
+    String facetNameAndField = IssueIndexDefinition.FIELD_ISSUE_FUNC_CREATED_AT;
+    SearchRequestBuilder esRequest = getClient()
+      .prepareSearch(IssueIndexDefinition.INDEX)
+      .setTypes(IssueIndexDefinition.TYPE_ISSUE)
+      .setSearchType(SearchType.COUNT);
+    BoolFilterBuilder esFilter = FilterBuilders.boolFilter();
+    for (FilterBuilder filter : filters.values()) {
+      if (filter != null) {
+        esFilter.must(filter);
+      }
     }
+    if (esFilter.hasClauses()) {
+      esRequest.setQuery(QueryBuilders.filteredQuery(esQuery, esFilter));
+    } else {
+      esRequest.setQuery(esQuery);
+    }
+    esRequest.addAggregation(AggregationBuilders.min(facetNameAndField).field(facetNameAndField));
+    Min minValue = esRequest.get().getAggregations().get(facetNameAndField);
+    return (long) minValue.getValue();
   }
 
-  private AggregationBuilder getAssigneesFacet(IssueQuery query, Map<String, FilterBuilder> filters, QueryBuilder esQuery) {
+  private AggregationBuilder createAssigneesFacet(IssueQuery query, Map<String, FilterBuilder> filters, QueryBuilder queryBuilder) {
     String fieldName = IssueIndexDefinition.FIELD_ISSUE_ASSIGNEE;
     String facetName = IssueFilterParameters.ASSIGNEES;
 
@@ -484,9 +523,9 @@ public class IssueIndex extends BaseIndex<Issue, FakeIssueDto, String> {
     Map<String, FilterBuilder> assigneeFilters = Maps.newHashMap(filters);
     assigneeFilters.remove("__isAssigned");
     assigneeFilters.remove(fieldName);
-    StickyFacetBuilder assigneeFacetBuilder = new StickyFacetBuilder(esQuery, assigneeFilters);
+    StickyFacetBuilder assigneeFacetBuilder = new StickyFacetBuilder(queryBuilder, assigneeFilters);
     BoolFilterBuilder facetFilter = assigneeFacetBuilder.getStickyFacetFilter(fieldName);
-    FilterAggregationBuilder facetTopAggregation = assigneeFacetBuilder.buildTopFacetAggregation(fieldName, facetName, facetFilter, DEFAULT_ISSUE_FACET_SIZE);
+    FilterAggregationBuilder facetTopAggregation = assigneeFacetBuilder.buildTopFacetAggregation(fieldName, facetName, facetFilter, DEFAULT_FACET_SIZE);
     List<String> assignees = Lists.newArrayList(query.assignees());
 
     UserSession session = UserSession.get();
@@ -507,7 +546,7 @@ public class IssueIndex extends BaseIndex<Issue, FakeIssueDto, String> {
       .subAggregation(facetTopAggregation);
   }
 
-  private AggregationBuilder getResolutionFacet(Map<String, FilterBuilder> filters, QueryBuilder esQuery) {
+  private AggregationBuilder createResolutionFacet(Map<String, FilterBuilder> filters, QueryBuilder esQuery) {
     String fieldName = IssueIndexDefinition.FIELD_ISSUE_RESOLUTION;
     String facetName = IssueFilterParameters.RESOLUTIONS;
 
@@ -517,7 +556,7 @@ public class IssueIndex extends BaseIndex<Issue, FakeIssueDto, String> {
     resolutionFilters.remove(fieldName);
     StickyFacetBuilder assigneeFacetBuilder = new StickyFacetBuilder(esQuery, resolutionFilters);
     BoolFilterBuilder facetFilter = assigneeFacetBuilder.getStickyFacetFilter(fieldName);
-    FilterAggregationBuilder facetTopAggregation = assigneeFacetBuilder.buildTopFacetAggregation(fieldName, facetName, facetFilter, DEFAULT_ISSUE_FACET_SIZE);
+    FilterAggregationBuilder facetTopAggregation = assigneeFacetBuilder.buildTopFacetAggregation(fieldName, facetName, facetFilter, DEFAULT_FACET_SIZE);
     facetTopAggregation = assigneeFacetBuilder.addSelectedItemsToFacet(fieldName, facetName, facetTopAggregation, Issue.RESOLUTIONS.toArray());
 
     // Add missing facet for unresolved issues
@@ -532,7 +571,7 @@ public class IssueIndex extends BaseIndex<Issue, FakeIssueDto, String> {
       .subAggregation(facetTopAggregation);
   }
 
-  private AggregationBuilder getActionPlansFacet(IssueQuery query, Map<String, FilterBuilder> filters, QueryBuilder esQuery) {
+  private AggregationBuilder createActionPlansFacet(IssueQuery query, Map<String, FilterBuilder> filters, QueryBuilder esQuery) {
     String fieldName = IssueIndexDefinition.FIELD_ISSUE_ACTION_PLAN;
     String facetName = IssueFilterParameters.ACTION_PLANS;
 
@@ -542,7 +581,7 @@ public class IssueIndex extends BaseIndex<Issue, FakeIssueDto, String> {
     actionPlanFilters.remove(fieldName);
     StickyFacetBuilder actionPlanFacetBuilder = new StickyFacetBuilder(esQuery, actionPlanFilters);
     BoolFilterBuilder facetFilter = actionPlanFacetBuilder.getStickyFacetFilter(fieldName);
-    FilterAggregationBuilder facetTopAggregation = actionPlanFacetBuilder.buildTopFacetAggregation(fieldName, facetName, facetFilter, DEFAULT_ISSUE_FACET_SIZE);
+    FilterAggregationBuilder facetTopAggregation = actionPlanFacetBuilder.buildTopFacetAggregation(fieldName, facetName, facetFilter, DEFAULT_FACET_SIZE);
     facetTopAggregation = actionPlanFacetBuilder.addSelectedItemsToFacet(fieldName, facetName, facetTopAggregation, query.actionPlans().toArray());
 
     // Add missing facet for unresolved issues
@@ -557,66 +596,8 @@ public class IssueIndex extends BaseIndex<Issue, FakeIssueDto, String> {
       .subAggregation(facetTopAggregation);
   }
 
-  private AggregationBuilder getCreatedAtFacet(IssueQuery query, Map<String, FilterBuilder> filters, QueryBuilder esQuery) {
-    Date now = system.newDate();
-    SimpleDateFormat tzFormat = new SimpleDateFormat("XX");
-    tzFormat.setTimeZone(TimeZone.getDefault());
-    String timeZoneString = tzFormat.format(now);
-
-    Interval bucketSize = Interval.YEAR;
-    Date createdAfter = query.createdAfter();
-    long startTime = createdAfter == null ? getMinCreatedAt(filters, esQuery) : createdAfter.getTime();
-    Date createdBefore = query.createdBefore();
-    long endTime = createdBefore == null ? now.getTime() : createdBefore.getTime();
-    Duration timeSpan = new Duration(startTime, endTime);
-    if (timeSpan.isShorterThan(TWENTY_DAYS)) {
-      bucketSize = Interval.DAY;
-    } else if (timeSpan.isShorterThan(TWENTY_WEEKS)) {
-      bucketSize = Interval.WEEK;
-    } else if (timeSpan.isShorterThan(TWENTY_MONTHS)) {
-      bucketSize = Interval.MONTH;
-    }
-
-
-    return AggregationBuilders.dateHistogram(IssueFilterParameters.CREATED_AT)
-      .field(IssueIndexDefinition.FIELD_ISSUE_FUNC_CREATED_AT)
-      .interval(bucketSize)
-      .minDocCount(0L)
-      .format(DateUtils.DATETIME_FORMAT)
-      .preZone(timeZoneString)
-      .postZone(timeZoneString)
-      .extendedBounds(startTime, endTime);
-  }
-
-  private long getMinCreatedAt(Map<String, FilterBuilder> filters, QueryBuilder esQuery) {
-    String facetNameAndField = IssueIndexDefinition.FIELD_ISSUE_FUNC_CREATED_AT;
-    SearchRequestBuilder minCount = getClient()
-      .prepareSearch(IssueIndexDefinition.INDEX)
-      .setTypes(IssueIndexDefinition.TYPE_ISSUE)
-      .setSearchType(SearchType.COUNT);
-    setQueryFilter(minCount, esQuery, filters);
-    minCount.addAggregation(AggregationBuilders.min(facetNameAndField).field(facetNameAndField));
-    Min minValue = minCount.get().getAggregations().get(facetNameAndField);
-    return (long) minValue.getValue();
-  }
-
-  private void setSorting(IssueQuery query, SearchRequestBuilder esRequest) {
-    String sortField = query.sort();
-    if (sortField != null) {
-      boolean asc = BooleanUtils.isTrue(query.asc());
-      sorting.fill(esRequest, sortField, asc);
-    } else {
-      sorting.fillDefault(esRequest);
-    }
-  }
-
-  protected void setPagination(QueryContext options, SearchRequestBuilder esSearch) {
-    esSearch.setFrom(options.getOffset());
-    esSearch.setSize(options.getLimit());
-  }
-
   @CheckForNull
-  private FilterBuilder matchFilter(String field, Collection<?> values) {
+  private FilterBuilder createTermsFilter(String field, Collection<?> values) {
     if (!values.isEmpty()) {
       return FilterBuilders.termsFilter(field, values);
     } else {
@@ -624,55 +605,100 @@ public class IssueIndex extends BaseIndex<Issue, FakeIssueDto, String> {
     }
   }
 
-  public Collection<String> listTagsMatching(@Nullable String query, int pageSize) {
-    return listTermsMatching(IssueIndexDefinition.FIELD_ISSUE_TAGS, query, pageSize);
+  public List<String> listTags(IssueQuery query, @Nullable String textQuery, int maxNumberOfTags) {
+    Terms terms = listTermsMatching(IssueIndexDefinition.FIELD_ISSUE_TAGS, query, textQuery, Terms.Order.term(true), maxNumberOfTags);
+    return EsUtils.termsKeys(terms);
   }
 
-  public Collection<String> listAuthorsMatching(@Nullable String query, int pageSize) {
-    return listTermsMatching(IssueIndexDefinition.FIELD_ISSUE_AUTHOR_LOGIN, query, pageSize);
+  public Map<String, Long> countTags(IssueQuery query, int maxNumberOfTags) {
+    Terms terms = listTermsMatching(IssueIndexDefinition.FIELD_ISSUE_TAGS, query, null, Terms.Order.count(false), maxNumberOfTags);
+    return EsUtils.termsToMap(terms);
   }
 
-  private Collection<String> listTermsMatching(String fieldName, @Nullable String query, int pageSize) {
-    SearchRequestBuilder count = getClient().prepareSearch(IssueIndexDefinition.INDEX)
-      .setTypes(IssueIndexDefinition.TYPE_ISSUE)
-      .setQuery(QueryBuilders.matchAllQuery());
+  public List<String> listAuthors(IssueQuery query, @Nullable String textQuery, int maxNumberOfAuthors) {
+    Terms terms = listTermsMatching(IssueIndexDefinition.FIELD_ISSUE_AUTHOR_LOGIN, query, textQuery, Terms.Order.term(true), maxNumberOfAuthors);
+    return EsUtils.termsKeys(terms);
+  }
+
+  private Terms listTermsMatching(String fieldName, IssueQuery query, @Nullable String textQuery, Terms.Order termsOrder, int maxNumberOfTags) {
+    SearchRequestBuilder requestBuilder = getClient()
+      .prepareSearch(IssueIndexDefinition.INDEX)
+      .setTypes(IssueIndexDefinition.TYPE_ISSUE);
+
+    requestBuilder.setQuery(QueryBuilders.filteredQuery(QueryBuilders.matchAllQuery(),
+      createBoolFilter(query)));
+
+    // TODO do not return hits
+
     TermsBuilder aggreg = AggregationBuilders.terms("_ref")
       .field(fieldName)
-      .size(pageSize)
-      .order(Order.term(true))
+      .size(maxNumberOfTags)
+      .order(termsOrder)
       .minDocCount(1L);
-    if (query != null) {
-      aggreg.include(".*" + query + ".*");
+    if (textQuery != null) {
+      aggreg.include(String.format(".*%s.*", textQuery));
     }
-    Terms result = count.addAggregation(aggreg).get().getAggregations().get("_ref");
 
-    return Collections2.transform(result.getBuckets(), new Function<Bucket, String>() {
-      @Override
-      public String apply(Bucket bucket) {
-        return bucket.getKey();
-      }
-    });
+    SearchResponse searchResponse = requestBuilder.addAggregation(aggreg).get();
+    return searchResponse.getAggregations().get("_ref");
   }
 
-  public Map<String, Long> listTagsForComponent(String componentUuid, int pageSize) {
-    SearchRequestBuilder count = getClient().prepareSearch(IssueIndexDefinition.INDEX)
-      .setTypes(IssueIndexDefinition.TYPE_ISSUE)
-      .setQuery(QueryBuilders.filteredQuery(QueryBuilders.matchAllQuery(),
-        FilterBuilders.boolFilter()
-          .must(getAuthorizationFilter(new QueryContext()))
-          .must(FilterBuilders.missingFilter(IssueIndexDefinition.FIELD_ISSUE_RESOLUTION))
-          .must(moduleRootFilter(Arrays.asList(componentUuid)))));
-    TermsBuilder aggreg = AggregationBuilders.terms("_ref")
-      .field(IssueIndexDefinition.FIELD_ISSUE_TAGS)
-      .size(pageSize)
-      .order(Order.count(false))
-      .minDocCount(1L);
-    Terms result = count.addAggregation(aggreg).get().getAggregations().get("_ref");
+  public void deleteClosedIssuesOfProjectBefore(String projectUuid, Date beforeDate) {
+    FilterBuilder projectFilter = FilterBuilders.boolFilter().must(FilterBuilders.termsFilter(IssueIndexDefinition.FIELD_ISSUE_PROJECT_UUID, projectUuid));
+    FilterBuilder dateFilter = FilterBuilders.rangeFilter(IssueIndexDefinition.FIELD_ISSUE_FUNC_CLOSED_AT).lt(beforeDate.getTime());
+    QueryBuilder queryBuilder = QueryBuilders.filteredQuery(
+      QueryBuilders.matchAllQuery(),
+      FilterBuilders.andFilter(projectFilter, dateFilter)
+      );
+
+    getClient().prepareDeleteByQuery(IssueIndexDefinition.INDEX).setQuery(queryBuilder).get();
+  }
 
-    Map<String, Long> map = Maps.newHashMap();
-    for (Bucket bucket : result.getBuckets()) {
-      map.put(bucket.getKey(), bucket.getDocCount());
+  public LinkedHashMap<String, Long> searchForAssignees(IssueQuery query) {
+    // TODO do not return hits
+    // TODO what's max size ?
+
+    SearchRequestBuilder esSearch = getClient()
+      .prepareSearch(IssueIndexDefinition.INDEX)
+      .setTypes(IssueIndexDefinition.TYPE_ISSUE);
+
+    QueryBuilder esQuery = QueryBuilders.matchAllQuery();
+    BoolFilterBuilder esFilter = createBoolFilter(query);
+    if (esFilter.hasClauses()) {
+      esSearch.setQuery(QueryBuilders.filteredQuery(esQuery, esFilter));
+    } else {
+      esSearch.setQuery(esQuery);
+    }
+    esSearch.addAggregation(AggregationBuilders.terms(IssueIndexDefinition.FIELD_ISSUE_ASSIGNEE)
+      .size(Integer.MAX_VALUE)
+      .field(IssueIndexDefinition.FIELD_ISSUE_ASSIGNEE));
+    esSearch.addAggregation(AggregationBuilders.missing("notAssigned")
+      .field(IssueIndexDefinition.FIELD_ISSUE_ASSIGNEE));
+
+    SearchResponse response = esSearch.get();
+    Terms aggregation = (Terms) response.getAggregations().getAsMap().get(IssueIndexDefinition.FIELD_ISSUE_ASSIGNEE);
+    LinkedHashMap<String, Long> result = EsUtils.termsToMap(aggregation);
+    result.put("_notAssigned_", ((InternalMissing) response.getAggregations().get("notAssigned")).getDocCount());
+    return result;
+  }
+
+  private BoolFilterBuilder createBoolFilter(IssueQuery query) {
+    BoolFilterBuilder boolFilter = FilterBuilders.boolFilter();
+    for (FilterBuilder filter : createFilters(query).values()) {
+      // TODO Can it be null ?
+      if (filter != null) {
+        boolFilter.must(filter);
+      }
     }
-    return map;
+    return boolFilter;
+  }
+
+  /**
+   * TODO used only by tests, so must be replaced by EsTester#countDocuments()
+   */
+  public long countAll() {
+    return getClient().prepareCount(IssueIndexDefinition.INDEX)
+      .setTypes(IssueIndexDefinition.TYPE_ISSUE)
+      .get().getCount();
   }
 }
index 3d500c0927ca163cb797d8753ac17fd185c695de..8096dc5dd62095d3098e66e00676d19b15b85cc9 100644 (file)
 package org.sonar.server.issue.ws;
 
 import com.google.common.io.Resources;
-import org.sonar.api.server.ws.*;
+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.NewAction;
 import org.sonar.api.utils.text.JsonWriter;
 import org.sonar.server.issue.IssueService;
 
-public class AuthorsAction implements RequestHandler {
+public class AuthorsAction implements BaseIssuesWsAction {
 
-  private static final String PARAM_PAGE_SIZE = "ps";
-  private static final String PARAM_QUERY = "q";
   private final IssueService service;
 
   public AuthorsAction(IssueService service) {
@@ -37,8 +37,8 @@ public class AuthorsAction implements RequestHandler {
 
   @Override
   public void handle(Request request, Response response) throws Exception {
-    String query = request.param(PARAM_QUERY);
-    int pageSize = request.mandatoryParamAsInt(PARAM_PAGE_SIZE);
+    String query = request.param(WebService.Param.TEXT_QUERY);
+    int pageSize = request.mandatoryParamAsInt(WebService.Param.PAGE_SIZE);
 
     JsonWriter json = response.newJsonWriter()
       .beginObject()
@@ -52,17 +52,18 @@ public class AuthorsAction implements RequestHandler {
     json.endArray().endObject().close();
   }
 
-  void define(WebService.NewController controller) {
+  @Override
+  public void define(WebService.NewController controller) {
     NewAction action = controller.createAction("authors")
       .setSince("5.1")
       .setDescription("Search SCM accounts which match a given query")
       .setResponseExample(Resources.getResource(this.getClass(), "example-authors.json"))
       .setHandler(this);
 
-    action.createParam(PARAM_QUERY)
+    action.createParam(WebService.Param.TEXT_QUERY)
       .setDescription("A pattern to match SCM accounts against")
       .setExampleValue("luke");
-    action.createParam(PARAM_PAGE_SIZE)
+    action.createParam(WebService.Param.PAGE_SIZE)
       .setDescription("The size of the list to return")
       .setExampleValue("25")
       .setDefaultValue("10");
diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/BaseIssuesWsAction.java b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/BaseIssuesWsAction.java
new file mode 100644 (file)
index 0000000..051a346
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * 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.issue.ws;
+
+import org.sonar.api.server.ws.RequestHandler;
+import org.sonar.api.server.ws.WebService;
+
+interface BaseIssuesWsAction extends RequestHandler {
+
+  void define(WebService.NewController controller);
+
+}
+
index e642686d0bb9e0caa89dea8ae9e1ac7aa9eb8e62..c68e9d00b78bc588b8b62279937619d1cfdbb80a 100644 (file)
@@ -21,7 +21,6 @@ package org.sonar.server.issue.ws;
 
 import com.google.common.io.Resources;
 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.api.server.ws.WebService.NewAction;
@@ -34,7 +33,7 @@ import java.util.Map;
  * List issue tags matching a given query.
  * @since 5.1
  */
-public class ComponentTagsAction implements RequestHandler {
+public class ComponentTagsAction implements BaseIssuesWsAction {
 
   private final IssueService service;
 
@@ -42,7 +41,8 @@ public class ComponentTagsAction implements RequestHandler {
     this.service = service;
   }
 
-  void define(WebService.NewController controller) {
+  @Override
+  public void define(WebService.NewController controller) {
     NewAction action = controller.createAction("component_tags")
       .setHandler(this)
       .setSince("5.1")
index 87340b7eaf19cf5ff560895eb31aa0266e420cc5..1c0604c028307a29920d18913fb4c9207a112abf 100644 (file)
@@ -29,7 +29,6 @@ import org.sonar.api.issue.internal.FieldDiffs;
 import org.sonar.api.server.debt.DebtCharacteristic;
 import org.sonar.api.server.debt.internal.DefaultDebtCharacteristic;
 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.api.user.User;
@@ -54,14 +53,13 @@ import org.sonar.server.user.UserSession;
 
 import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
-
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
 
 import static com.google.common.collect.Maps.newHashMap;
 
-public class IssueShowAction implements RequestHandler {
+public class IssueShowAction implements BaseIssuesWsAction {
 
   public static final String SHOW_ACTION = "show";
 
@@ -94,7 +92,8 @@ public class IssueShowAction implements RequestHandler {
     this.durations = durations;
   }
 
-  void define(WebService.NewController controller) {
+  @Override
+  public void define(WebService.NewController controller) {
     WebService.NewAction action = controller.createAction(SHOW_ACTION)
       .setDescription("Detail of issue")
       .setSince("4.2")
index 8efe7e1f039dabd51a31a97d53099f7adb21f8e3..df9e92da85078f24634bef8ab2864d28bded3e7c 100644 (file)
@@ -42,22 +42,10 @@ public class IssuesWs implements WebService {
   public static final String DO_ACTION_ACTION = "do_action";
   public static final String BULK_CHANGE_ACTION = "bulk_change";
 
-  private final IssueShowAction showAction;
-  private final SearchAction esSearchAction;
-  private final TagsAction tagsAction;
-  private final SetTagsAction setTagsAction;
-  private final ComponentTagsAction componentTagsAction;
-  private final AuthorsAction authorsAction;
-
-
-  public IssuesWs(IssueShowAction showAction, SearchAction searchAction, TagsAction tagsAction, SetTagsAction setTagsAction, ComponentTagsAction componentTagsAction,
-      AuthorsAction authorsAction) {
-    this.showAction = showAction;
-    this.esSearchAction = searchAction;
-    this.tagsAction = tagsAction;
-    this.setTagsAction = setTagsAction;
-    this.componentTagsAction = componentTagsAction;
-    this.authorsAction = authorsAction;
+  private final BaseIssuesWsAction[] actions;
+
+  public IssuesWs(BaseIssuesWsAction... actions) {
+    this.actions = actions;
   }
 
   @Override
@@ -65,14 +53,14 @@ public class IssuesWs implements WebService {
     NewController controller = context.createController(API_ENDPOINT);
     controller.setDescription("Coding rule issues");
     controller.setSince("3.6");
+    for (BaseIssuesWsAction action : actions) {
+      action.define(controller);
+    }
+    defineRailsActions(controller);
+    controller.done();
+  }
 
-    showAction.define(controller);
-    esSearchAction.define(controller);
-    tagsAction.define(controller);
-    setTagsAction.define(controller);
-    componentTagsAction.define(controller);
-    authorsAction.define(controller);
-
+  private void defineRailsActions(NewController controller) {
     defineChangelogAction(controller);
     defineAssignAction(controller);
     defineAddCommentAction(controller);
@@ -85,8 +73,6 @@ public class IssuesWs implements WebService {
     defineCreateAction(controller);
     defineDoActionAction(controller);
     defineBulkChangeAction(controller);
-
-    controller.done();
   }
 
   private void defineChangelogAction(NewController controller) {
index 6960a88c93e0591cb568a4a1fe30515728731a7a..528aa02d5af1ade5761c00c967eb2bd85375edd1 100644 (file)
@@ -22,6 +22,7 @@ package org.sonar.server.issue.ws;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
 import com.google.common.io.Resources;
 import org.apache.commons.lang.BooleanUtils;
 import org.sonar.api.i18n.I18n;
@@ -34,6 +35,7 @@ import org.sonar.api.resources.Languages;
 import org.sonar.api.rule.RuleKey;
 import org.sonar.api.rule.Severity;
 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.user.User;
 import org.sonar.api.user.UserFinder;
@@ -42,35 +44,37 @@ import org.sonar.api.utils.Duration;
 import org.sonar.api.utils.Durations;
 import org.sonar.api.utils.text.JsonWriter;
 import org.sonar.core.component.ComponentDto;
-import org.sonar.core.issue.db.IssueChangeDao;
 import org.sonar.core.persistence.DbSession;
 import org.sonar.markdown.Markdown;
 import org.sonar.server.db.DbClient;
+import org.sonar.server.es.SearchOptions;
+import org.sonar.server.es.SearchResult;
 import org.sonar.server.issue.IssueQuery;
 import org.sonar.server.issue.IssueQueryService;
 import org.sonar.server.issue.IssueService;
 import org.sonar.server.issue.actionplan.ActionPlanService;
 import org.sonar.server.issue.filter.IssueFilterParameters;
 import org.sonar.server.issue.index.IssueDoc;
+import org.sonar.server.issue.index.IssueIndex;
 import org.sonar.server.rule.Rule;
 import org.sonar.server.rule.RuleService;
-import org.sonar.server.search.FacetValue;
-import org.sonar.server.search.QueryContext;
-import org.sonar.server.search.Result;
-import org.sonar.server.search.ws.SearchRequestHandler;
 import org.sonar.server.user.UserSession;
 
 import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
 
-import java.util.*;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 import static com.google.common.collect.Lists.newArrayList;
 import static com.google.common.collect.Maps.newHashMap;
 import static com.google.common.collect.Sets.newHashSet;
 
-public class SearchAction extends SearchRequestHandler<IssueQuery, Issue> {
-
+public class SearchAction implements BaseIssuesWsAction {
 
   public static final String SEARCH_ACTION = "search";
 
@@ -84,7 +88,6 @@ public class SearchAction extends SearchRequestHandler<IssueQuery, Issue> {
 
   private static final String INTERNAL_PARAMETER_DISCLAIMER = "This parameter is mostly used by the Issues page, please prefer usage of the componentKeys parameter. ";
 
-  private final IssueChangeDao issueChangeDao;
   private final IssueService service;
   private final IssueActionsWriter actionsWriter;
 
@@ -97,11 +100,9 @@ public class SearchAction extends SearchRequestHandler<IssueQuery, Issue> {
   private final Durations durations;
   private final Languages languages;
 
-  public SearchAction(DbClient dbClient, IssueChangeDao issueChangeDao, IssueService service, IssueActionsWriter actionsWriter, IssueQueryService issueQueryService,
+  public SearchAction(DbClient dbClient, IssueService service, IssueActionsWriter actionsWriter, IssueQueryService issueQueryService,
     RuleService ruleService, ActionPlanService actionPlanService, UserFinder userFinder, I18n i18n, Durations durations, Languages languages) {
-    super(SEARCH_ACTION);
     this.dbClient = dbClient;
-    this.issueChangeDao = issueChangeDao;
     this.service = service;
     this.actionsWriter = actionsWriter;
     this.issueQueryService = issueQueryService;
@@ -114,12 +115,22 @@ public class SearchAction extends SearchRequestHandler<IssueQuery, Issue> {
   }
 
   @Override
-  protected void doDefinition(WebService.NewAction action) {
-    action.setDescription("Get a list of issues. If the number of issues is greater than 10,000, only the first 10,000 ones are returned by the web service. " +
-      "Requires Browse permission on project(s)")
+  public void define(WebService.NewController controller) {
+    WebService.NewAction action = controller
+      .createAction(SEARCH_ACTION)
+      .setHandler(this)
+      .setDescription(
+        "Get a list of issues. If the number of issues is greater than 10,000, only the first 10,000 ones are returned by the web service. Requires Browse permission on project(s)")
       .setSince("3.6")
       .setResponseExample(Resources.getResource(this.getClass(), "example-search.json"));
 
+    action.addPagingParams(100);
+    action.createParam(WebService.Param.FACETS)
+      .setDescription("Comma-separated list of the facets to be computed. No facet is computed by default.")
+      .setPossibleValues(IssueIndex.SUPPORTED_FACETS);
+    action.addSortParams(IssueQuery.SORTS, null, true);
+    // TODO support param "f"
+
     addComponentRelatedParams(action);
     action.createParam(IssueFilterParameters.ISSUES)
       .setDescription("Comma-separated list of issue keys")
@@ -182,14 +193,6 @@ public class SearchAction extends SearchRequestHandler<IssueQuery, Issue> {
     action.createParam(IssueFilterParameters.CREATED_BEFORE)
       .setDescription("To retrieve issues created before the given date (exclusive). Format: date or datetime ISO formats")
       .setExampleValue("2013-05-01 (or 2013-05-01T13:00:00+0100)");
-    action.createParam(SearchRequestHandler.PARAM_SORT)
-      .setDescription("Sort field")
-      .setDeprecatedKey(IssueFilterParameters.SORT)
-      .setPossibleValues(IssueQuery.SORTS);
-    action.createParam(SearchRequestHandler.PARAM_ASCENDING)
-      .setDeprecatedKey(IssueFilterParameters.ASC)
-      .setDescription("Ascending sort")
-      .setBooleanPossibleValues();
     action.createParam(IssueFilterParameters.IGNORE_PAGING)
       .setDescription("Return the full list of issues, regardless of paging. For internal use only")
       .setBooleanPossibleValues()
@@ -199,7 +202,6 @@ public class SearchAction extends SearchRequestHandler<IssueQuery, Issue> {
   }
 
   private void addComponentRelatedParams(WebService.NewAction action) {
-
     action.createParam(IssueFilterParameters.ON_COMPONENT_ONLY)
       .setDescription("Return only issues at a component's level, not on its descendants (modules, directories, files, etc). " +
         "This parameter is only considered when componentKeys or componentUuids is set. " +
@@ -260,49 +262,34 @@ public class SearchAction extends SearchRequestHandler<IssueQuery, Issue> {
   }
 
   @Override
-  protected IssueQuery doQuery(Request request) {
-    return issueQueryService.createFromRequest(request);
+  public final void handle(Request request, Response response) throws Exception {
+    SearchOptions options = new SearchOptions();
+    options.setPage(request.mandatoryParamAsInt(IssuesWs.Param.PAGE), request.mandatoryParamAsInt(IssuesWs.Param.PAGE_SIZE));
+    options.addFacets(request.paramAsStrings(WebService.Param.FACETS));
+
+    IssueQuery query = issueQueryService.createFromRequest(request);
+    SearchResult<IssueDoc> result = execute(query, options);
+
+    JsonWriter json = response.newJsonWriter().beginObject();
+    options.writeJson(json, result.getTotal());
+    options.writeDeprecatedJson(json, result.getTotal());
+
+    writeResponse(request, result, json);
+    if (!options.getFacets().isEmpty()) {
+      writeFacets(request, options, result, json);
+    }
+    json.endObject().close();
   }
 
-  @Override
-  protected Result<Issue> doSearch(IssueQuery query, QueryContext context) {
+  private SearchResult<IssueDoc> execute(IssueQuery query, SearchOptions options) {
     Collection<String> components = query.componentUuids();
     if (components != null && components.size() == 1 && BooleanUtils.isTrue(query.ignorePaging())) {
-      context.setShowFullResult(true);
+      options.disableLimit();
     }
-    return service.search(query, context);
+    return service.search(query, options);
   }
 
-  @Override
-  @CheckForNull
-  protected Collection<String> possibleFields() {
-    return Collections.emptyList();
-  }
-
-  @Override
-  @CheckForNull
-  protected Collection<String> possibleFacets() {
-    return Arrays.asList(new String[]{
-      IssueFilterParameters.SEVERITIES,
-      IssueFilterParameters.STATUSES,
-      IssueFilterParameters.RESOLUTIONS,
-      IssueFilterParameters.ACTION_PLANS,
-      IssueFilterParameters.PROJECT_UUIDS,
-      IssueFilterParameters.RULES,
-      IssueFilterParameters.ASSIGNEES,
-      IssueFilterParameters.REPORTERS,
-      IssueFilterParameters.AUTHORS,
-      IssueFilterParameters.MODULE_UUIDS,
-      IssueFilterParameters.FILE_UUIDS,
-      IssueFilterParameters.DIRECTORIES,
-      IssueFilterParameters.LANGUAGES,
-      IssueFilterParameters.TAGS,
-      IssueFilterParameters.CREATED_AT,
-    });
-  }
-
-  @Override
-  protected void doContextResponse(Request request, QueryContext context, Result<Issue> result, JsonWriter json) {
+  private void writeResponse(Request request, SearchResult<IssueDoc> result, JsonWriter json) {
     List<String> issueKeys = newArrayList();
     Set<RuleKey> ruleKeys = newHashSet();
     Set<String> projectUuids = newHashSet();
@@ -313,21 +300,19 @@ public class SearchAction extends SearchRequestHandler<IssueQuery, Issue> {
     Map<String, ComponentDto> componentsByUuid = newHashMap();
     Multimap<String, DefaultIssueComment> commentsByIssues = ArrayListMultimap.create();
     Collection<ComponentDto> componentDtos = newHashSet();
-    List<ComponentDto> projectDtos = newArrayList();
     Map<String, ComponentDto> projectsByComponentUuid = newHashMap();
 
-    for (Issue issue : result.getHits()) {
-      IssueDoc issueDoc = (IssueDoc) issue;
-      issueKeys.add(issue.key());
-      ruleKeys.add(issue.ruleKey());
+    for (IssueDoc issueDoc : result.getDocs()) {
+      issueKeys.add(issueDoc.key());
+      ruleKeys.add(issueDoc.ruleKey());
       projectUuids.add(issueDoc.projectUuid());
       componentUuids.add(issueDoc.componentUuid());
-      actionPlanKeys.add(issue.actionPlanKey());
-      if (issue.reporter() != null) {
-        userLogins.add(issue.reporter());
+      actionPlanKeys.add(issueDoc.actionPlanKey());
+      if (issueDoc.reporter() != null) {
+        userLogins.add(issueDoc.reporter());
       }
-      if (issue.assignee() != null) {
-        userLogins.add(issue.assignee());
+      if (issueDoc.assignee() != null) {
+        userLogins.add(issueDoc.assignee());
       }
     }
 
@@ -342,7 +327,7 @@ public class SearchAction extends SearchRequestHandler<IssueQuery, Issue> {
 
     DbSession session = dbClient.openSession(false);
     try {
-      List<DefaultIssueComment> comments = issueChangeDao.selectCommentsByIssues(session, issueKeys);
+      List<DefaultIssueComment> comments = dbClient.issueChangeDao().selectCommentsByIssues(session, issueKeys);
       for (DefaultIssueComment issueComment : comments) {
         userLogins.add(issueComment.userLogin());
         commentsByIssues.put(issueComment.issueKey(), issueComment);
@@ -357,7 +342,7 @@ public class SearchAction extends SearchRequestHandler<IssueQuery, Issue> {
         projectUuids.add(component.projectUuid());
       }
 
-      projectDtos = dbClient.componentDao().getByUuids(session, projectUuids);
+      List<ComponentDto> projectDtos = dbClient.componentDao().getByUuids(session, projectUuids);
       componentDtos.addAll(projectDtos);
       for (ComponentDto componentDto : componentDtos) {
         componentsByUuid.put(componentDto.uuid(), componentDto);
@@ -378,28 +363,24 @@ public class SearchAction extends SearchRequestHandler<IssueQuery, Issue> {
     writeUsers(json, usersByLogin);
     writeActionPlans(json, actionPlanByKeys.values());
     writeLanguages(json);
-
-    // TODO remove legacy paging. Handled by the SearchRequestHandler
-    writeLegacyPaging(context, json, result);
   }
 
-  private void collectRuleKeys(Request request, Result<Issue> result, Set<RuleKey> ruleKeys) {
-    Collection<FacetValue> facetRules = result.getFacetValues(IssueFilterParameters.RULES);
+  private void collectRuleKeys(Request request, SearchResult<IssueDoc> result, Set<RuleKey> ruleKeys) {
+    Set<String> facetRules = result.getFacets().getBucketKeys(IssueFilterParameters.RULES);
     if (facetRules != null) {
-      for (FacetValue rule: facetRules) {
-        ruleKeys.add(RuleKey.parse(rule.getKey()));
+      for (String rule : facetRules) {
+        ruleKeys.add(RuleKey.parse(rule));
       }
     }
     List<String> rulesFromRequest = request.paramAsStrings(IssueFilterParameters.RULES);
-    if (rulesFromRequest != null ) {
-      for (String ruleKey: rulesFromRequest) {
+    if (rulesFromRequest != null) {
+      for (String ruleKey : rulesFromRequest) {
         ruleKeys.add(RuleKey.parse(ruleKey));
       }
     }
   }
 
-  @Override
-  protected void writeFacets(Request request, QueryContext context, Result<?> results, JsonWriter json) {
+  protected void writeFacets(Request request, SearchOptions options, SearchResult<IssueDoc> results, JsonWriter json) {
     addMandatoryFacetValues(results, IssueFilterParameters.SEVERITIES, Severity.ALL);
     addMandatoryFacetValues(results, IssueFilterParameters.STATUSES, Issue.STATUSES);
     List<String> resolutions = Lists.newArrayList("");
@@ -429,37 +410,51 @@ public class SearchAction extends SearchRequestHandler<IssueQuery, Issue> {
     addMandatoryFacetValues(results, IssueFilterParameters.ACTION_PLANS, actionPlans);
     addMandatoryFacetValues(results, IssueFilterParameters.COMPONENT_UUIDS, request.paramAsStrings(IssueFilterParameters.COMPONENT_UUIDS));
 
-    super.writeFacets(request, context, results, json);
+    json.name("facets").beginArray();
+    for (String facetName : options.getFacets()) {
+      json.beginObject();
+      json.prop("property", facetName);
+      json.name("values").beginArray();
+      if (results.getFacets().contains(facetName)) {
+        Set<String> itemsFromFacets = Sets.newHashSet();
+        for (Map.Entry<String, Long> bucket : results.getFacets().get(facetName).entrySet()) {
+          itemsFromFacets.add(bucket.getKey());
+          json.beginObject();
+          json.prop("val", bucket.getKey());
+          json.prop("count", bucket.getValue());
+          json.endObject();
+        }
+        addZeroFacetsForSelectedItems(request, facetName, itemsFromFacets, json);
+      }
+      json.endArray().endObject();
+    }
+    json.endArray();
   }
 
-  private void collectFacetsData(Request request, Result<Issue> result, Set<String> projectUuids, Set<String> componentUuids, List<String> userLogins, Set<String> actionPlanKeys) {
-    collectFacetKeys(result, IssueFilterParameters.PROJECT_UUIDS, projectUuids);
+  private void collectFacetsData(Request request, SearchResult<IssueDoc> result, Set<String> projectUuids, Set<String> componentUuids, List<String> userLogins,
+    Set<String> actionPlanKeys) {
+    collectBucketKeys(result, IssueFilterParameters.PROJECT_UUIDS, projectUuids);
     collectParameterValues(request, IssueFilterParameters.PROJECT_UUIDS, projectUuids);
 
-    collectFacetKeys(result, IssueFilterParameters.COMPONENT_UUIDS, componentUuids);
+    collectBucketKeys(result, IssueFilterParameters.COMPONENT_UUIDS, componentUuids);
     collectParameterValues(request, IssueFilterParameters.COMPONENT_UUIDS, componentUuids);
-    collectFacetKeys(result, IssueFilterParameters.FILE_UUIDS, componentUuids);
+    collectBucketKeys(result, IssueFilterParameters.FILE_UUIDS, componentUuids);
     collectParameterValues(request, IssueFilterParameters.FILE_UUIDS, componentUuids);
 
-    collectFacetKeys(result, IssueFilterParameters.MODULE_UUIDS, componentUuids);
+    collectBucketKeys(result, IssueFilterParameters.MODULE_UUIDS, componentUuids);
     collectParameterValues(request, IssueFilterParameters.MODULE_UUIDS, componentUuids);
     collectParameterValues(request, IssueFilterParameters.COMPONENT_ROOT_UUIDS, componentUuids);
 
-    collectFacetKeys(result, IssueFilterParameters.ASSIGNEES, userLogins);
+    collectBucketKeys(result, IssueFilterParameters.ASSIGNEES, userLogins);
     collectParameterValues(request, IssueFilterParameters.ASSIGNEES, userLogins);
-    collectFacetKeys(result, IssueFilterParameters.REPORTERS, userLogins);
+    collectBucketKeys(result, IssueFilterParameters.REPORTERS, userLogins);
     collectParameterValues(request, IssueFilterParameters.REPORTERS, userLogins);
-    collectFacetKeys(result, IssueFilterParameters.ACTION_PLANS, actionPlanKeys);
+    collectBucketKeys(result, IssueFilterParameters.ACTION_PLANS, actionPlanKeys);
     collectParameterValues(request, IssueFilterParameters.ACTION_PLANS, actionPlanKeys);
   }
 
-  private void collectFacetKeys(Result<Issue> result, String facetName, Collection<String> facetKeys) {
-    Collection<FacetValue> facetValues = result.getFacetValues(facetName);
-    if (facetValues != null) {
-      for (FacetValue project : facetValues) {
-        facetKeys.add(project.getKey());
-      }
-    }
+  private void collectBucketKeys(SearchResult<IssueDoc> result, String facetName, Collection<String> bucketKeys) {
+    bucketKeys.addAll(result.getFacets().getBucketKeys(facetName));
   }
 
   private void collectParameterValues(Request request, String facetName, Collection<String> facetKeys) {
@@ -469,28 +464,6 @@ public class SearchAction extends SearchRequestHandler<IssueQuery, Issue> {
     }
   }
 
-  private void writeLegacyPaging(QueryContext context, JsonWriter json, Result<?> result) {
-    // TODO remove with stas on HTML side
-    json.prop("maxResultsReached", false);
-
-    long pages = context.getLimit();
-    if (pages > 0) {
-      pages = result.getTotal() / context.getLimit();
-      if (result.getTotal() % context.getLimit() > 0) {
-        pages++;
-      }
-    }
-
-    json.name("paging").beginObject()
-      .prop("pageIndex", context.getPage())
-      .prop("pageSize", context.getLimit())
-      .prop("total", result.getTotal())
-      // TODO Remove as part of Front-end rework on Issue Domain
-      .prop("fTotal", i18n.formatInteger(UserSession.get().locale(), (int) result.getTotal()))
-      .prop("pages", pages)
-      .endObject();
-  }
-
   // TODO change to use the RuleMapper
   private void writeRules(JsonWriter json, Collection<Rule> rules) {
     json.name("rules").beginArray();
@@ -508,11 +481,12 @@ public class SearchAction extends SearchRequestHandler<IssueQuery, Issue> {
     json.endArray();
   }
 
-  private void writeIssues(Result<Issue> result, Multimap<String, DefaultIssueComment> commentsByIssues, Map<String, User> usersByLogin, Map<String, ActionPlan> actionPlanByKeys,
+  private void writeIssues(SearchResult<IssueDoc> result, Multimap<String, DefaultIssueComment> commentsByIssues, Map<String, User> usersByLogin,
+    Map<String, ActionPlan> actionPlanByKeys,
     Map<String, ComponentDto> componentsByUuid, Map<String, ComponentDto> projectsByComponentUuid, @Nullable List<String> extraFields, JsonWriter json) {
     json.name("issues").beginArray();
 
-    for (Issue issue : result.getHits()) {
+    for (IssueDoc issue : result.getDocs()) {
       json.beginObject();
 
       String actionPlanKey = issue.actionPlanKey();
@@ -520,7 +494,7 @@ public class SearchAction extends SearchRequestHandler<IssueQuery, Issue> {
       ComponentDto project = null, subProject = null;
       if (file != null) {
         project = projectsByComponentUuid.get(file.uuid());
-        if (! file.projectUuid().equals(file.moduleUuid())) {
+        if (!file.projectUuid().equals(file.moduleUuid())) {
           subProject = componentsByUuid.get(file.moduleUuid());
         }
       }
@@ -565,7 +539,7 @@ public class SearchAction extends SearchRequestHandler<IssueQuery, Issue> {
     Collection<String> tags = issue.tags();
     if (tags != null && !tags.isEmpty()) {
       json.name("tags").beginArray();
-      for (String tag: tags) {
+      for (String tag : tags) {
         json.value(tag);
       }
       json.endArray();
@@ -806,4 +780,28 @@ public class SearchAction extends SearchRequestHandler<IssueQuery, Issue> {
     return null;
   }
 
+  protected void addMandatoryFacetValues(SearchResult<IssueDoc> results, String facetName, @Nullable List<String> mandatoryValues) {
+    Map<String, Long> buckets = results.getFacets().get(facetName);
+    if (buckets != null && mandatoryValues != null) {
+      for (String mandatoryValue : mandatoryValues) {
+        if (!buckets.containsKey(mandatoryValue)) {
+          buckets.put(mandatoryValue, 0L);
+        }
+      }
+    }
+  }
+
+  private void addZeroFacetsForSelectedItems(Request request, String facetName, Set<String> itemsFromFacets, JsonWriter json) {
+    List<String> requestParams = request.paramAsStrings(facetName);
+    if (requestParams != null) {
+      for (String param : requestParams) {
+        if (!itemsFromFacets.contains(param)) {
+          json.beginObject();
+          json.prop("val", param);
+          json.prop("count", 0);
+          json.endObject();
+        }
+      }
+    }
+  }
 }
index 1119fc96a6f593a7b0d09a022752206d58f8f8c2..f6ccf7dc444d2f4641085453ad563a9557ae5525 100644 (file)
  */
 package org.sonar.server.issue.ws;
 
-import com.google.common.base.Splitter;
-import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableSet;
+import com.google.common.base.Objects;
 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.api.server.ws.WebService.NewAction;
@@ -31,14 +28,13 @@ import org.sonar.api.utils.text.JsonWriter;
 import org.sonar.server.issue.IssueService;
 
 import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
 
 /**
- * Set tags on an issue.
- * @since 5.1
+ * Set tags on an issue
  */
-public class SetTagsAction implements RequestHandler {
-
-  private static final Splitter WS_TAGS_SPLITTER = Splitter.on(',').omitEmptyStrings().trimResults();
+public class SetTagsAction implements BaseIssuesWsAction {
 
   private final IssueService service;
 
@@ -46,7 +42,8 @@ public class SetTagsAction implements RequestHandler {
     this.service = service;
   }
 
-  void define(WebService.NewController controller) {
+  @Override
+  public void define(WebService.NewController controller) {
     NewAction action = controller.createAction("set_tags")
       .setHandler(this)
       .setPost(true)
@@ -64,8 +61,8 @@ public class SetTagsAction implements RequestHandler {
   @Override
   public void handle(Request request, Response response) throws Exception {
     String key = request.mandatoryParam("key");
-    String tags = Strings.nullToEmpty(request.param("tags"));
-    Collection<String> resultTags = service.setTags(key, ImmutableSet.copyOf(WS_TAGS_SPLITTER.split(tags)));
+    List<String> tags = Objects.firstNonNull(request.paramAsStrings("tags"), Collections.<String>emptyList());
+    Collection<String> resultTags = service.setTags(key, tags);
     JsonWriter json = response.newJsonWriter().beginObject().name("tags").beginArray();
     for (String tag : resultTags) {
       json.value(tag);
index ab47393cc397b128721c44f0700fd49fec21c946..c3da60898a4169276f5fbe6464b6a2a9227705d9 100644 (file)
@@ -20,7 +20,9 @@
 package org.sonar.server.issue.ws;
 
 import com.google.common.io.Resources;
-import org.sonar.api.server.ws.*;
+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.NewAction;
 import org.sonar.api.utils.text.JsonWriter;
 import org.sonar.server.issue.IssueService;
@@ -29,7 +31,7 @@ import org.sonar.server.issue.IssueService;
  * List issue tags matching a given query.
  * @since 5.1
  */
-public class TagsAction implements RequestHandler {
+public class TagsAction implements BaseIssuesWsAction {
 
   private final IssueService service;
 
@@ -37,7 +39,8 @@ public class TagsAction implements RequestHandler {
     this.service = service;
   }
 
-  void define(WebService.NewController controller) {
+  @Override
+  public void define(WebService.NewController controller) {
     NewAction action = controller.createAction("tags")
       .setHandler(this)
       .setSince("5.1")
index ba932a23915b531487fb555a6c7f47e87610a461..53b36c093babe8310fc6cb2fe816536cfb3cc4c8 100644 (file)
@@ -35,12 +35,10 @@ import java.util.Map;
 public class StickyFacetBuilder {
 
   private static final int FACET_DEFAULT_MIN_DOC_COUNT = 1;
-
   private static final int FACET_DEFAULT_SIZE = 10;
 
-  private QueryBuilder query;
-
-  private Map<String, FilterBuilder> filters;
+  private final QueryBuilder query;
+  private final Map<String, FilterBuilder> filters;
 
   public StickyFacetBuilder(QueryBuilder query, Map<String, FilterBuilder> filters) {
     this.query = query;
index 0cbf88ebb1d5a9016286a030a2a018a161ee1834..9d0393a340a3e8a922bf7c416332f8f32bcf2822 100644 (file)
@@ -25,20 +25,18 @@ import org.elasticsearch.action.search.SearchRequestBuilder;
 import org.elasticsearch.index.query.QueryBuilders;
 import org.elasticsearch.search.SearchHit;
 import org.elasticsearch.search.sort.SortOrder;
-import org.sonar.api.ServerComponent;
+import org.sonar.server.es.BaseIndex;
 import org.sonar.server.es.EsClient;
 import org.sonar.server.exceptions.NotFoundException;
 
 import java.util.List;
 
-public class SourceLineIndex implements ServerComponent {
+public class SourceLineIndex extends BaseIndex {
 
   private static final int MAX_RESULT = 500000;
 
-  private final EsClient esClient;
-
   public SourceLineIndex(EsClient esClient) {
-    this.esClient = esClient;
+    super(esClient);
   }
 
   /**
@@ -61,7 +59,7 @@ public class SourceLineIndex implements ServerComponent {
     }
     int toLimited = size + from - 1;
 
-    for (SearchHit hit : esClient.prepareSearch(SourceLineIndexDefinition.INDEX)
+    for (SearchHit hit : getClient().prepareSearch(SourceLineIndexDefinition.INDEX)
       .setTypes(SourceLineIndexDefinition.TYPE)
       .setSize(size)
       .setQuery(QueryBuilders.boolQuery()
@@ -79,7 +77,7 @@ public class SourceLineIndex implements ServerComponent {
 
   public SourceLineDoc getLine(String fileUuid, int line) {
     Preconditions.checkArgument(line > 0, "Line should be greater than 0");
-    SearchRequestBuilder request = esClient.prepareSearch(SourceLineIndexDefinition.INDEX)
+    SearchRequestBuilder request = getClient().prepareSearch(SourceLineIndexDefinition.INDEX)
       .setTypes(SourceLineIndexDefinition.TYPE)
       .setSize(1)
       .setQuery(QueryBuilders.boolQuery()
index ff8974c9fc59fefe3dabf79e24e79b40945c0e68..8494c14e03ef1b40f547a22e09b7f03146fae6b5 100644 (file)
@@ -1,12 +1,7 @@
 {
-  "securityExclusions": false,
-  "maxResultsReached": false,
-  "paging": {
-    "pageIndex": 1,
-    "pageSize": 5,
-    "total": 206,
-    "pages": 42
-  },
+  "total": 206,
+  "p": 1,
+  "ps": 5,
   "issues": [
     {
       "key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123",
index 0cb24b9a9f7bfd01944bee970b4a91ac85445879..f2a58bd610784fa587205f0cb9a0012814c0910d 100644 (file)
@@ -25,12 +25,15 @@ import org.junit.Test;
 import org.slf4j.Logger;
 import org.sonar.api.CoreProperties;
 import org.sonar.api.config.Settings;
-import org.sonar.core.persistence.DbSession;
-import org.sonar.core.purge.*;
 import org.sonar.core.computation.dbcleaner.period.DefaultPeriodCleaner;
+import org.sonar.core.persistence.DbSession;
+import org.sonar.core.purge.IdUuidPair;
+import org.sonar.core.purge.PurgeConfiguration;
+import org.sonar.core.purge.PurgeDao;
+import org.sonar.core.purge.PurgeListener;
+import org.sonar.core.purge.PurgeProfiler;
 import org.sonar.server.issue.index.IssueIndex;
 import org.sonar.server.properties.ProjectSettingsFactory;
-import org.sonar.server.search.IndexClient;
 
 import java.util.Date;
 
@@ -41,35 +44,25 @@ import static org.mockito.Mockito.*;
 public class ProjectCleanerTest {
 
   private ProjectCleaner sut;
-  private PurgeDao dao;
-  private PurgeProfiler profiler;
-  private DefaultPeriodCleaner periodCleaner;
-  private PurgeListener purgeListener;
+  private PurgeDao dao= mock(PurgeDao.class);
+  private PurgeProfiler profiler= mock(PurgeProfiler.class);
+  private DefaultPeriodCleaner periodCleaner= mock(DefaultPeriodCleaner.class);
+  private PurgeListener purgeListener= mock(PurgeListener.class);
   private ProjectSettingsFactory projectSettingsFactory;
-  private IndexClient indexClient;
-  private IssueIndex issueIndex;
-  private Settings settings;
+  private IssueIndex issueIndex= mock(IssueIndex.class);
+  private Settings settings = new Settings();
 
   @Before
   public void before() throws Exception {
-    this.dao = mock(PurgeDao.class);
-    this.profiler = mock(PurgeProfiler.class);
-    this.periodCleaner = mock(DefaultPeriodCleaner.class);
-    this.purgeListener = mock(PurgeListener.class);
-    this.settings = mock(Settings.class);
     this.projectSettingsFactory = mock(ProjectSettingsFactory.class);
     when(projectSettingsFactory.newProjectSettings(any(DbSession.class), any(Long.class))).thenReturn(settings);
 
-    this.issueIndex = mock(IssueIndex.class);
-    this.indexClient = mock(IndexClient.class);
-    when(indexClient.get(IssueIndex.class)).thenReturn(issueIndex);
-
-    this.sut = new ProjectCleaner(dao, periodCleaner, profiler, purgeListener, projectSettingsFactory, indexClient);
+    this.sut = new ProjectCleaner(dao, periodCleaner, profiler, purgeListener, projectSettingsFactory, issueIndex);
   }
 
   @Test
   public void no_profiling_when_property_is_false() throws Exception {
-    when(settings.getBoolean(CoreProperties.PROFILING_LOG_PROPERTY)).thenReturn(false);
+    settings.setProperty(CoreProperties.PROFILING_LOG_PROPERTY, false);
     when(projectSettingsFactory.newProjectSettings(any(DbSession.class), any(Long.class))).thenReturn(settings);
 
     sut.purge(mock(DbSession.class), mock(IdUuidPair.class));
@@ -83,12 +76,12 @@ public class ProjectCleanerTest {
 
     sut.purge(mock(DbSession.class), mock(IdUuidPair.class));
 
-    verify(indexClient, never()).get(IssueIndex.class);
+    verifyZeroInteractions(issueIndex);
   }
 
   @Test
   public void profiling_when_property_is_true() throws Exception {
-    when(settings.getBoolean(CoreProperties.PROFILING_LOG_PROPERTY)).thenReturn(true);
+    settings.setProperty(CoreProperties.PROFILING_LOG_PROPERTY, true);
 
     sut.purge(mock(DbSession.class), mock(IdUuidPair.class));
 
@@ -97,7 +90,7 @@ public class ProjectCleanerTest {
 
   @Test
   public void call_period_cleaner_index_client_and_purge_dao() throws Exception {
-    when(settings.getInt(DbCleanerConstants.DAYS_BEFORE_DELETING_CLOSED_ISSUES)).thenReturn(5);
+    settings.setProperty(DbCleanerConstants.DAYS_BEFORE_DELETING_CLOSED_ISSUES, 5);
 
     sut.purge(mock(DbSession.class), mock(IdUuidPair.class));
 
diff --git a/server/sonar-server/src/test/java/org/sonar/server/es/EsUtilsTest.java b/server/sonar-server/src/test/java/org/sonar/server/es/EsUtilsTest.java
new file mode 100644 (file)
index 0000000..e5139c0
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * 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.es;
+
+import com.google.common.base.Function;
+import org.elasticsearch.search.SearchHit;
+import org.elasticsearch.search.SearchHits;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.sonar.server.issue.index.IssueDoc;
+import org.sonar.server.search.BaseDoc;
+import org.sonar.test.TestUtils;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class EsUtilsTest {
+
+  @Test
+  public void convertToDocs_empty() throws Exception {
+    SearchHits hits = mock(SearchHits.class, Mockito.RETURNS_MOCKS);
+    List<BaseDoc> docs = EsUtils.convertToDocs(hits, new Function<Map<String, Object>, BaseDoc>() {
+      @Override
+      public BaseDoc apply(Map<String, Object> input) {
+        return new IssueDoc(input);
+      }
+    });
+    assertThat(docs).isEmpty();
+  }
+
+  @Test
+  public void convertToDocs() throws Exception {
+    SearchHits hits = mock(SearchHits.class, Mockito.RETURNS_MOCKS);
+    when(hits.getHits()).thenReturn(new SearchHit[]{mock(SearchHit.class)});
+    List<BaseDoc> docs = EsUtils.convertToDocs(hits, new Function<Map<String, Object>, BaseDoc>() {
+      @Override
+      public BaseDoc apply(Map<String, Object> input) {
+        return new IssueDoc(input);
+      }
+    });
+    assertThat(docs).hasSize(1);
+  }
+
+  @Test
+  public void util_class() throws Exception {
+    assertThat(TestUtils.hasOnlyPrivateConstructors(EsUtils.class));
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/es/SearchOptionsTest.java b/server/sonar-server/src/test/java/org/sonar/server/es/SearchOptionsTest.java
new file mode 100644 (file)
index 0000000..e4cac26
--- /dev/null
@@ -0,0 +1,156 @@
+/*
+ * 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.es;
+
+import org.junit.Test;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.sonar.api.utils.text.JsonWriter;
+import org.sonar.server.search.QueryContext;
+
+import java.io.StringWriter;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.fail;
+
+public class SearchOptionsTest {
+
+  @Test
+  public void defaults() throws Exception {
+    SearchOptions options = new SearchOptions();
+
+    assertThat(options.getFacets()).isEmpty();
+    assertThat(options.getFields()).isEmpty();
+    assertThat(options.getOffset()).isEqualTo(0);
+    assertThat(options.getLimit()).isEqualTo(10);
+    assertThat(options.getPage()).isEqualTo(1);
+  }
+
+  @Test
+  public void page_shortcut_for_limit_and_offset() throws Exception {
+    SearchOptions options = new SearchOptions().setPage(3, 10);
+
+    assertThat(options.getLimit()).isEqualTo(10);
+    assertThat(options.getOffset()).isEqualTo(20);
+  }
+
+  @Test
+  public void page_starts_at_one() throws Exception {
+    SearchOptions options = new SearchOptions().setPage(1, 10);
+    assertThat(options.getLimit()).isEqualTo(10);
+    assertThat(options.getOffset()).isEqualTo(0);
+    assertThat(options.getPage()).isEqualTo(1);
+  }
+
+  @Test
+  public void with_zero_page_size() throws Exception {
+    SearchOptions options = new SearchOptions().setPage(1, 0);
+    assertThat(options.getLimit()).isEqualTo(0);
+    assertThat(options.getOffset()).isEqualTo(0);
+    assertThat(options.getPage()).isEqualTo(0);
+  }
+
+  @Test
+  public void page_must_be_strictly_positive() throws Exception {
+    try {
+      new SearchOptions().setPage(0, 10);
+      fail();
+    } catch (IllegalArgumentException e) {
+      assertThat(e).hasMessage("Page must be greater or equal to 1 (got 0)");
+    }
+  }
+
+  @Test
+  public void use_max_limit_if_negative() throws Exception {
+    SearchOptions options = new SearchOptions().setPage(2, -1);
+    assertThat(options.getLimit()).isEqualTo(SearchOptions.MAX_LIMIT);
+  }
+
+  @Test
+  public void max_limit() throws Exception {
+    SearchOptions options = new SearchOptions().setLimit(42);
+    assertThat(options.getLimit()).isEqualTo(42);
+
+    options.setLimit(SearchOptions.MAX_LIMIT + 10);
+    assertThat(options.getLimit()).isEqualTo(QueryContext.MAX_LIMIT);
+  }
+
+  @Test
+  public void disable_limit() throws Exception {
+    SearchOptions options = new SearchOptions().disableLimit();
+    assertThat(options.getLimit()).isEqualTo(999999);
+  }
+
+  @Test
+  public void max_page_size() throws Exception {
+    SearchOptions options = new SearchOptions().setPage(3, QueryContext.MAX_LIMIT + 10);
+    assertThat(options.getOffset()).isEqualTo(QueryContext.MAX_LIMIT * 2);
+    assertThat(options.getLimit()).isEqualTo(QueryContext.MAX_LIMIT);
+  }
+
+  @Test
+  public void hasField() throws Exception {
+    // parameter is missing -> all the fields are returned by default
+    SearchOptions options = new SearchOptions();
+    assertThat(options.hasField("repo")).isTrue();
+
+    // parameter is set to empty -> all the fields are returned by default
+    options = new SearchOptions().addFields("");
+    assertThat(options.hasField("repo")).isTrue();
+
+    // parameter is set -> return only the selected fields
+    options = new SearchOptions().addFields("name", "repo");
+    assertThat(options.hasField("name")).isTrue();
+    assertThat(options.hasField("repo")).isTrue();
+    assertThat(options.hasField("severity")).isFalse();
+  }
+
+  @Test
+  public void writeJson() throws Exception {
+    SearchOptions options = new SearchOptions().setPage(3, 10);
+    StringWriter json = new StringWriter();
+    JsonWriter jsonWriter = JsonWriter.of(json).beginObject();
+    options.writeJson(jsonWriter, 42L);
+    jsonWriter.endObject().close();
+
+    JSONAssert.assertEquals("{\"total\": 42, \"p\": 3, \"ps\": 10}", json.toString(), true);
+  }
+
+  @Test
+  public void writeDeprecatedJson() throws Exception {
+    SearchOptions options = new SearchOptions().setPage(3, 10);
+    StringWriter json = new StringWriter();
+    JsonWriter jsonWriter = JsonWriter.of(json).beginObject();
+    options.writeDeprecatedJson(jsonWriter, 42L);
+    jsonWriter.endObject().close();
+
+    JSONAssert.assertEquals("{\"paging\": {\"pageIndex\": 3, \"pageSize\": 10, \"total\": 42, \"fTotal\": \"42\", \"pages\": 5}}", json.toString(), true);
+  }
+
+  @Test
+  public void writeDeprecatedJson_exact_nb_of_pages() throws Exception {
+    SearchOptions options = new SearchOptions().setPage(3, 10);
+    StringWriter json = new StringWriter();
+    JsonWriter jsonWriter = JsonWriter.of(json).beginObject();
+    options.writeDeprecatedJson(jsonWriter, 30L);
+    jsonWriter.endObject().close();
+
+    JSONAssert.assertEquals("{\"paging\": {\"pageIndex\": 3, \"pageSize\": 10, \"total\": 30, \"fTotal\": \"30\", \"pages\": 3}}", json.toString(), true);
+  }
+}
index 939b355c973e744027fdce3274604423c2a0c72c..553a424f293a814ef9f7fd58de803ea9dededcf5 100644 (file)
@@ -24,12 +24,11 @@ import org.junit.Test;
 import org.sonar.api.issue.Issue;
 import org.sonar.server.user.UserSession;
 
-import java.util.List;
+import java.util.Collection;
 import java.util.Map;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
-
 public class ActionTest {
 
   @Test
@@ -37,9 +36,10 @@ public class ActionTest {
     try {
       new Action("") {
         @Override
-        boolean verify(Map<String, Object> properties, List<Issue> issues, UserSession userSession) {
+        boolean verify(Map<String, Object> properties, Collection<Issue> issues, UserSession userSession) {
           return false;
         }
+
         @Override
         boolean execute(Map<String, Object> properties, Context context) {
           return false;
@@ -55,9 +55,10 @@ public class ActionTest {
     try {
       new Action(null) {
         @Override
-        boolean verify(Map<String, Object> properties, List<Issue> issues, UserSession userSession) {
+        boolean verify(Map<String, Object> properties, Collection<Issue> issues, UserSession userSession) {
           return false;
         }
+
         @Override
         boolean execute(Map<String, Object> properties, Context context) {
           return false;
index 10a2efadbcc53e80e2a8ea9e0e32bc597507d9db..b85e27a46e5b0b163dfa32e00f6e415ec10bc50b 100644 (file)
@@ -21,7 +21,6 @@
 package org.sonar.server.issue;
 
 import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Maps;
 import org.junit.Before;
 import org.junit.Test;
@@ -40,14 +39,16 @@ import org.sonar.core.issue.DefaultIssueFilter;
 import org.sonar.core.resource.ResourceDao;
 import org.sonar.core.resource.ResourceDto;
 import org.sonar.core.resource.ResourceQuery;
+import org.sonar.server.es.SearchOptions;
 import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.exceptions.Message;
 import org.sonar.server.issue.actionplan.ActionPlanService;
 import org.sonar.server.issue.filter.IssueFilterService;
-import org.sonar.server.search.QueryContext;
 import org.sonar.server.user.UserSession;
 
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 
 import static com.google.common.collect.Lists.newArrayList;
@@ -57,10 +58,7 @@ import static org.junit.Assert.fail;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anyString;
 import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.*;
 
 @RunWith(MockitoJUnitRunner.class)
 public class InternalRubyIssueServiceTest {
@@ -563,7 +561,7 @@ public class InternalRubyIssueServiceTest {
   @Test
   public void execute_issue_filter_from_issue_query() {
     service.execute(Maps.<String, Object>newHashMap());
-    verify(issueFilterService).execute(any(IssueQuery.class), any(QueryContext.class));
+    verify(issueFilterService).execute(any(IssueQuery.class), any(SearchOptions.class));
   }
 
   @Test
@@ -584,14 +582,14 @@ public class InternalRubyIssueServiceTest {
     service.execute(10L, overrideProps);
 
     ArgumentCaptor<IssueQuery> issueQueryArgumentCaptor = ArgumentCaptor.forClass(IssueQuery.class);
-    ArgumentCaptor<QueryContext> contextArgumentCaptor = ArgumentCaptor.forClass(QueryContext.class);
+    ArgumentCaptor<SearchOptions> contextArgumentCaptor = ArgumentCaptor.forClass(SearchOptions.class);
 
     verify(issueFilterService).execute(issueQueryArgumentCaptor.capture(), contextArgumentCaptor.capture());
     verify(issueFilterService).find(eq(10L), any(UserSession.class));
 
-    QueryContext queryContext = contextArgumentCaptor.getValue();
-    assertThat(queryContext.getLimit()).isEqualTo(20);
-    assertThat(queryContext.getPage()).isEqualTo(2);
+    SearchOptions searchOptions = contextArgumentCaptor.getValue();
+    assertThat(searchOptions.getLimit()).isEqualTo(20);
+    assertThat(searchOptions.getPage()).isEqualTo(2);
   }
 
   @Test
@@ -687,25 +685,25 @@ public class InternalRubyIssueServiceTest {
     Map<String, Object> map = newHashMap();
     map.put("pageSize", 10l);
     map.put("pageIndex", 50);
-    QueryContext context = InternalRubyIssueService.toContext(map);
-    assertThat(context.getLimit()).isEqualTo(10);
-    assertThat(context.getPage()).isEqualTo(50);
+    SearchOptions searchOptions = InternalRubyIssueService.toSearchOptions(map);
+    assertThat(searchOptions.getLimit()).isEqualTo(10);
+    assertThat(searchOptions.getPage()).isEqualTo(50);
 
     map = newHashMap();
     map.put("pageSize", -1);
     map.put("pageIndex", 50);
-    context = InternalRubyIssueService.toContext(map);
-    assertThat(context.getLimit()).isEqualTo(500);
-    assertThat(context.getPage()).isEqualTo(1);
+    searchOptions = InternalRubyIssueService.toSearchOptions(map);
+    assertThat(searchOptions.getLimit()).isEqualTo(500);
+    assertThat(searchOptions.getPage()).isEqualTo(1);
 
-    context = InternalRubyIssueService.toContext(Maps.<String, Object>newHashMap());
-    assertThat(context.getLimit()).isEqualTo(100);
-    assertThat(context.getPage()).isEqualTo(1);
+    searchOptions = InternalRubyIssueService.toSearchOptions(Maps.<String, Object>newHashMap());
+    assertThat(searchOptions.getLimit()).isEqualTo(100);
+    assertThat(searchOptions.getPage()).isEqualTo(1);
   }
 
   @Test
   public void list_tags() throws Exception {
-    ImmutableSet<String> tags = ImmutableSet.of("tag1", "tag2", "tag3");
+    List<String> tags = Arrays.asList("tag1", "tag2", "tag3");
     when(issueService.listTags(null, 0)).thenReturn(tags);
     assertThat(service.listTags()).isEqualTo(tags);
   }
index 39cb75ea3fd8be2a88c210d6178aec5c5852ad82..a781c2deeec04df6d77427a1c6b7f5fc5c5bc312 100644 (file)
 
 package org.sonar.server.issue;
 
-import com.google.common.collect.Lists;
-import org.junit.Before;
-import org.junit.Test;
-import org.sonar.api.issue.Issue;
-import org.sonar.api.issue.condition.Condition;
-import org.sonar.api.issue.internal.DefaultIssue;
-import org.sonar.api.notifications.NotificationManager;
-import org.sonar.api.resources.Qualifiers;
-import org.sonar.api.resources.Scopes;
-import org.sonar.api.rules.Rule;
-import org.sonar.core.component.ComponentDto;
-import org.sonar.core.issue.db.IssueDto;
-import org.sonar.core.issue.db.IssueStorage;
-import org.sonar.core.persistence.DbSession;
-import org.sonar.server.component.db.ComponentDao;
-import org.sonar.server.db.DbClient;
-import org.sonar.server.exceptions.BadRequestException;
-import org.sonar.server.exceptions.UnauthorizedException;
-import org.sonar.server.issue.db.IssueDao;
-import org.sonar.server.issue.notification.IssueChangeNotification;
-import org.sonar.server.rule.DefaultRuleFinder;
-import org.sonar.server.rule.RuleTesting;
-import org.sonar.server.search.QueryContext;
-import org.sonar.server.user.MockUserSession;
-import org.sonar.server.user.UserSession;
-
-import java.util.List;
-import java.util.Map;
-
-import static com.google.common.collect.Lists.newArrayList;
-import static com.google.common.collect.Maps.newHashMap;
-import static com.google.common.collect.Sets.newHashSet;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.Assert.fail;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyListOf;
-import static org.mockito.Matchers.anyMap;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.*;
-
 public class IssueBulkChangeServiceTest {
 
-  DbClient dbClient = mock(DbClient.class);
-  DbSession dbSession = mock(DbSession.class);
-
-  IssueDao issueDao = mock(IssueDao.class);
-  IssueService issueService = mock(IssueService.class);
-  IssueStorage issueStorage = mock(IssueStorage.class);
-  DefaultRuleFinder ruleFinder = mock(DefaultRuleFinder.class);
-  ComponentDao componentDao = mock(ComponentDao.class);
-  NotificationManager notificationService = mock(NotificationManager.class);
-
-  IssueBulkChangeService service;
-
-  UserSession userSession = MockUserSession.create().setLogin("john").setUserId(10);
-
-  DefaultIssue issue;
-  Rule rule;
-  ComponentDto project;
-  ComponentDto file;
-
-  List<Action> actions;
-
-  @Before
-  public void before() {
-    when(dbClient.openSession(false)).thenReturn(dbSession);
-    when(dbClient.componentDao()).thenReturn(componentDao);
-    when(dbClient.issueDao()).thenReturn(issueDao);
-
-    rule = Rule.create("repo", "key").setName("the rule name");
-    when(ruleFinder.findByKeys(newHashSet(rule.ruleKey()))).thenReturn(newArrayList(rule));
-
-    project = new ComponentDto()
-      .setId(1L)
-      .setKey("MyProject")
-      .setLongName("My Project")
-      .setQualifier(Qualifiers.PROJECT)
-      .setScope(Scopes.PROJECT);
-    when(componentDao.getByKeys(dbSession, newHashSet(project.key()))).thenReturn(newArrayList(project));
-
-    file = new ComponentDto()
-      .setId(2L)
-      .setParentProjectId(project.getId())
-      .setKey("MyComponent")
-      .setLongName("My Component");
-    when(componentDao.getByKeys(dbSession, newHashSet(file.key()))).thenReturn(newArrayList(file));
-
-    IssueDto issueDto = IssueTesting.newDto(RuleTesting.newDto(rule.ruleKey()).setId(50), file, project).setKee("ABCD");
-    issue = issueDto.toDefaultIssue();
-
-    org.sonar.server.search.Result<Issue> result = mock(org.sonar.server.search.Result.class);
-    when(result.getHits()).thenReturn(newArrayList((Issue) issue));
-    when(issueService.search(any(IssueQuery.class), any(QueryContext.class))).thenReturn(result);
-    when(issueDao.selectByKeys(dbSession, newArrayList(issue.key()))).thenReturn(newArrayList(issueDto));
-
-    actions = newArrayList();
-    service = new IssueBulkChangeService(dbClient, issueService, issueStorage, ruleFinder, notificationService, actions);
-  }
-
-  @Test
-  public void should_execute_bulk_change() {
-    Map<String, Object> properties = newHashMap();
-    properties.put("issues", "ABCD");
-    properties.put("actions", "assign");
-    properties.put("assign.assignee", "fred");
-    actions.add(new MockAction("assign"));
-
-    IssueBulkChangeQuery issueBulkChangeQuery = new IssueBulkChangeQuery(properties, true);
-    IssueBulkChangeResult result = service.execute(issueBulkChangeQuery, userSession);
-    assertThat(result.issuesChanged()).hasSize(1);
-    assertThat(result.issuesNotChanged()).isEmpty();
-
-    verify(issueStorage).save(eq(issue));
-    verifyNoMoreInteractions(issueStorage);
-    verify(notificationService).scheduleForSending(any(IssueChangeNotification.class));
-    verifyNoMoreInteractions(notificationService);
-  }
-
-  @Test
-  public void should_skip_send_notifications() {
-    Map<String, Object> properties = newHashMap();
-    properties.put("issues", "ABCD");
-    properties.put("actions", "assign");
-    properties.put("assign.assignee", "fred");
-    actions.add(new MockAction("assign"));
-
-    IssueBulkChangeQuery issueBulkChangeQuery = new IssueBulkChangeQuery(properties, false);
-    IssueBulkChangeResult result = service.execute(issueBulkChangeQuery, userSession);
-    assertThat(result.issuesChanged()).hasSize(1);
-    assertThat(result.issuesNotChanged()).isEmpty();
-
-    verify(issueStorage).save(eq(issue));
-    verifyNoMoreInteractions(issueStorage);
-    verifyZeroInteractions(notificationService);
-  }
-
-  @Test
-  public void should_execute_bulk_change_with_comment() {
-    Map<String, Object> properties = newHashMap();
-    properties.put("issues", "ABCD");
-    properties.put("actions", "assign");
-    properties.put("assign.assignee", "fred");
-
-    Action commentAction = mock(Action.class);
-    when(commentAction.key()).thenReturn("comment");
-    when(commentAction.supports(any(Issue.class))).thenReturn(true);
-    when(commentAction.verify(anyMap(), anyListOf(Issue.class), any(UserSession.class))).thenReturn(true);
-    when(commentAction.execute(anyMap(), any(IssueBulkChangeService.ActionContext.class))).thenReturn(true);
-    actions.add(commentAction);
-    actions.add(new MockAction("assign"));
-
-    IssueBulkChangeQuery issueBulkChangeQuery = new IssueBulkChangeQuery(properties, "my comment", true);
-    IssueBulkChangeResult result = service.execute(issueBulkChangeQuery, userSession);
-    assertThat(result.issuesChanged()).hasSize(1);
-    assertThat(result.issuesNotChanged()).isEmpty();
-
-    verify(commentAction).execute(anyMap(), any(IssueBulkChangeService.ActionContext.class));
-    verify(issueStorage).save(eq(issue));
-  }
-
-  @Test
-  public void should_execute_bulk_change_with_comment_only_on_changed_issues() {
-    IssueDto issueDto1 = IssueTesting.newDto(RuleTesting.newDto(rule.ruleKey()).setId(50), file, project).setKee("ABCD");
-    IssueDto issueDto2 = IssueTesting.newDto(RuleTesting.newDto(rule.ruleKey()).setId(50), file, project).setKee("EFGH");
-
-    org.sonar.server.search.Result<Issue> resultIssues = mock(org.sonar.server.search.Result.class);
-    when(resultIssues.getHits()).thenReturn(Lists.<Issue>newArrayList(issueDto1.toDefaultIssue(), issueDto2.toDefaultIssue()));
-    when(issueService.search(any(IssueQuery.class), any(QueryContext.class))).thenReturn(resultIssues);
-    when(issueDao.selectByKeys(dbSession, newArrayList("ABCD", "EFGH"))).thenReturn(newArrayList(issueDto1, issueDto2));
-
-    Map<String, Object> properties = newHashMap();
-    properties.put("issues", "ABCD,EFGH");
-    properties.put("actions", "assign");
-    properties.put("assign.assignee", "fred");
-
-    Action commentAction = mock(Action.class);
-    when(commentAction.key()).thenReturn("comment");
-    when(commentAction.supports(any(Issue.class))).thenReturn(true);
-    when(commentAction.verify(anyMap(), anyListOf(Issue.class), any(UserSession.class))).thenReturn(true);
-    when(commentAction.execute(anyMap(), any(IssueBulkChangeService.ActionContext.class))).thenReturn(true);
-    actions.add(commentAction);
-
-    // This action will only be executed on the first issue, not the second
-    Action assignAction = mock(Action.class);
-    when(assignAction.key()).thenReturn("assign");
-    when(assignAction.supports(any(Issue.class))).thenReturn(true).thenReturn(false);
-    when(assignAction.verify(anyMap(), anyListOf(Issue.class), any(UserSession.class))).thenReturn(true);
-    when(assignAction.execute(anyMap(), any(IssueBulkChangeService.ActionContext.class))).thenReturn(true).thenReturn(false);
-    actions.add(assignAction);
-
-    IssueBulkChangeQuery issueBulkChangeQuery = new IssueBulkChangeQuery(properties, "my comment", true);
-    IssueBulkChangeResult result = service.execute(issueBulkChangeQuery, userSession);
-    assertThat(result.issuesChanged()).hasSize(1);
-    assertThat(result.issuesNotChanged()).hasSize(1);
-
-    // Only one issue will receive the comment
-    verify(assignAction, times(1)).execute(anyMap(), any(IssueBulkChangeService.ActionContext.class));
-    verify(issueStorage).save(eq(issueDto1.toDefaultIssue()));
-  }
-
-  @Test
-  public void should_save_once_per_issue() {
-    Map<String, Object> properties = newHashMap();
-    properties.put("issues", "ABCD");
-    properties.put("actions", "assign,set_severity");
-    properties.put("assign.assignee", "fred");
-    properties.put("set_severity.severity", "MINOR");
-
-    actions.add(new MockAction("set_severity"));
-    actions.add(new MockAction("assign"));
-
-    IssueBulkChangeQuery issueBulkChangeQuery = new IssueBulkChangeQuery(properties, true);
-    IssueBulkChangeResult result = service.execute(issueBulkChangeQuery, userSession);
-    assertThat(result.issuesChanged()).hasSize(1);
-    assertThat(result.issuesNotChanged()).isEmpty();
-
-    verify(issueStorage, times(1)).save(eq(issue));
-    verifyNoMoreInteractions(issueStorage);
-    verify(notificationService).scheduleForSending(any(IssueChangeNotification.class));
-    verifyNoMoreInteractions(notificationService);
-  }
-
-  @Test
-  public void should_not_execute_bulk_if_issue_does_not_support_action() {
-    Map<String, Object> properties = newHashMap();
-    properties.put("issues", "ABCD");
-    properties.put("actions", "assign");
-    properties.put("assign.assignee", "fred");
-    actions.add(new MockAction("assign", true, true, false));
-
-    IssueBulkChangeQuery issueBulkChangeQuery = new IssueBulkChangeQuery(properties, true);
-    IssueBulkChangeResult result = service.execute(issueBulkChangeQuery, userSession);
-    assertThat(result.issuesChanged()).isEmpty();
-    assertThat(result.issuesNotChanged()).hasSize(1);
-
-    verifyZeroInteractions(issueStorage);
-    verifyZeroInteractions(notificationService);
-  }
-
-  @Test
-  public void should_not_execute_bulk_if_action_is_not_verified() {
-    Map<String, Object> properties = newHashMap();
-    properties.put("issues", "ABCD");
-    properties.put("actions", "assign");
-    properties.put("assign.assignee", "fred");
-    actions.add(new MockAction("assign", false, true, true));
-
-    IssueBulkChangeQuery issueBulkChangeQuery = new IssueBulkChangeQuery(properties, true);
-    IssueBulkChangeResult result = service.execute(issueBulkChangeQuery, userSession);
-    assertThat(result.issuesChanged()).isEmpty();
-    assertThat(result.issuesNotChanged()).isEmpty();
-
-    verifyZeroInteractions(issueStorage);
-    verifyZeroInteractions(notificationService);
-  }
-
-  @Test
-  public void should_not_execute_bulk_if_action_could_not_be_executed_on_issue() {
-    Map<String, Object> properties = newHashMap();
-    properties.put("issues", "ABCD");
-    properties.put("actions", "assign");
-    properties.put("assign.assignee", "fred");
-    actions.add(new MockAction("assign", true, false, true));
-
-    IssueBulkChangeQuery issueBulkChangeQuery = new IssueBulkChangeQuery(properties, true);
-    IssueBulkChangeResult result = service.execute(issueBulkChangeQuery, userSession);
-    assertThat(result.issuesChanged()).isEmpty();
-    assertThat(result.issuesNotChanged()).hasSize(1);
-
-    verifyZeroInteractions(issueStorage);
-    verifyZeroInteractions(notificationService);
-  }
-
-  @Test
-  public void should_not_execute_bulk_on_unexpected_error() {
-    Map<String, Object> properties = newHashMap();
-    properties.put("issues", "ABCD");
-    properties.put("actions", "assign");
-    properties.put("assign.assignee", "fred");
-
-    Action action = mock(Action.class);
-    when(action.key()).thenReturn("assign");
-    when(action.supports(any(Issue.class))).thenReturn(true);
-    when(action.verify(anyMap(), anyListOf(Issue.class), any(UserSession.class))).thenReturn(true);
-    doThrow(new RuntimeException("Error")).when(action).execute(anyMap(), any(IssueBulkChangeService.ActionContext.class));
-    actions.add(action);
-
-    IssueBulkChangeQuery issueBulkChangeQuery = new IssueBulkChangeQuery(properties, true);
-    IssueBulkChangeResult result = service.execute(issueBulkChangeQuery, userSession);
-    assertThat(result.issuesChanged()).isEmpty();
-    assertThat(result.issuesNotChanged()).hasSize(1);
-
-    verifyZeroInteractions(issueStorage);
-    verifyZeroInteractions(notificationService);
-  }
-
-  @Test
-  public void should_fail_if_user_not_logged() {
-    userSession = MockUserSession.create().setLogin(null);
-
-    Map<String, Object> properties = newHashMap();
-    properties.put("issues", "ABCD");
-    properties.put("actions", "assign");
-    properties.put("assign.assignee", "fred");
-    IssueBulkChangeQuery issueBulkChangeQuery = new IssueBulkChangeQuery(properties, true);
-    try {
-      service.execute(issueBulkChangeQuery, userSession);
-      fail();
-    } catch (Exception e) {
-      assertThat(e).isInstanceOf(UnauthorizedException.class);
-    }
-    verifyZeroInteractions(issueStorage);
-    verifyZeroInteractions(notificationService);
-  }
-
-  @Test
-  public void should_fail_if_action_not_found() {
-    Map<String, Object> properties = newHashMap();
-    properties.put("issues", "ABCD");
-    properties.put("actions", "unknown");
-    properties.put("unknown.unknown", "unknown");
-    IssueBulkChangeQuery issueBulkChangeQuery = new IssueBulkChangeQuery(properties, true);
-    try {
-      service.execute(issueBulkChangeQuery, userSession);
-      fail();
-    } catch (Exception e) {
-      assertThat(e).isInstanceOf(BadRequestException.class).hasMessage("The action : 'unknown' is unknown");
-    }
-    verifyZeroInteractions(issueStorage);
-    verifyZeroInteractions(notificationService);
-  }
-
-  class MockAction extends Action {
-
-    private boolean verify;
-    private boolean execute;
-
-    public MockAction(String key, boolean verify, boolean execute, final boolean support) {
-      super(key);
-      this.verify = verify;
-      this.execute = execute;
-      setConditions(new Condition() {
-        @Override
-        public boolean matches(Issue issue) {
-          return support;
-        }
-      });
-    }
-
-    public MockAction(String key) {
-      this(key, true, true, true);
-    }
-
-    @Override
-    boolean verify(Map<String, Object> properties, List<Issue> issues, UserSession userSession) {
-      return verify;
-    }
-
-    @Override
-    boolean execute(Map<String, Object> properties, Context context) {
-      return execute;
-    }
-  }
+//  DbClient dbClient = mock(DbClient.class);
+//  DbSession dbSession = mock(DbSession.class);
+//
+//  IssueDao issueDao = mock(IssueDao.class);
+//  IssueService issueService = mock(IssueService.class);
+//  IssueStorage issueStorage = mock(IssueStorage.class);
+//  DefaultRuleFinder ruleFinder = mock(DefaultRuleFinder.class);
+//  ComponentDao componentDao = mock(ComponentDao.class);
+//  NotificationManager notificationService = mock(NotificationManager.class);
+//
+//  IssueBulkChangeService service;
+//
+//  UserSession userSession = MockUserSession.create().setLogin("john").setUserId(10);
+//
+//  IssueDoc issue;
+//  Rule rule;
+//  ComponentDto project;
+//  ComponentDto file;
+//
+//  List<Action> actions;
+//
+//  @Before
+//  public void before() {
+//    when(dbClient.openSession(false)).thenReturn(dbSession);
+//    when(dbClient.componentDao()).thenReturn(componentDao);
+//    when(dbClient.issueDao()).thenReturn(issueDao);
+//
+//    rule = Rule.create("repo", "key").setName("the rule name");
+//    when(ruleFinder.findByKeys(newHashSet(rule.ruleKey()))).thenReturn(newArrayList(rule));
+//
+//    project = new ComponentDto()
+//      .setId(1L)
+//      .setKey("MyProject")
+//      .setLongName("My Project")
+//      .setQualifier(Qualifiers.PROJECT)
+//      .setScope(Scopes.PROJECT);
+//    when(componentDao.getByKeys(dbSession, newHashSet(project.key()))).thenReturn(newArrayList(project));
+//
+//    file = new ComponentDto()
+//      .setId(2L)
+//      .setParentProjectId(project.getId())
+//      .setKey("MyComponent")
+//      .setLongName("My Component");
+//    when(componentDao.getByKeys(dbSession, newHashSet(file.key()))).thenReturn(newArrayList(file));
+//
+//    IssueDoc issueDto = IssueTesting.newDoc("ABCD", file).setRuleKey(rule.ruleKey().toString());
+//    issue = issueDto.toDefaultIssue();
+//
+//    SearchResult<IssueDoc> result = mock(SearchResult.class);
+//    when(result.getDocs()).thenReturn(newArrayList((IssueDoc) issue));
+//    when(issueService.search(any(IssueQuery.class), any(SearchOptions.class))).thenReturn(result);
+//    when(issueDao.selectByKeys(dbSession, newArrayList(issue.key()))).thenReturn(newArrayList(issueDto));
+//
+//    actions = newArrayList();
+//    service = new IssueBulkChangeService(dbClient, issueService, issueStorage, ruleFinder, notificationService, actions);
+//  }
+//
+//  @Test
+//  public void should_execute_bulk_change() {
+//    Map<String, Object> properties = newHashMap();
+//    properties.put("issues", "ABCD");
+//    properties.put("actions", "assign");
+//    properties.put("assign.assignee", "fred");
+//    actions.add(new MockAction("assign"));
+//
+//    IssueBulkChangeQuery issueBulkChangeQuery = new IssueBulkChangeQuery(properties, true);
+//    IssueBulkChangeResult result = service.execute(issueBulkChangeQuery, userSession);
+//    assertThat(result.issuesChanged()).hasSize(1);
+//    assertThat(result.issuesNotChanged()).isEmpty();
+//
+//    verify(issueStorage).save(eq(issue));
+//    verifyNoMoreInteractions(issueStorage);
+//    verify(notificationService).scheduleForSending(any(IssueChangeNotification.class));
+//    verifyNoMoreInteractions(notificationService);
+//  }
+//
+//  @Test
+//  public void should_skip_send_notifications() {
+//    Map<String, Object> properties = newHashMap();
+//    properties.put("issues", "ABCD");
+//    properties.put("actions", "assign");
+//    properties.put("assign.assignee", "fred");
+//    actions.add(new MockAction("assign"));
+//
+//    IssueBulkChangeQuery issueBulkChangeQuery = new IssueBulkChangeQuery(properties, false);
+//    IssueBulkChangeResult result = service.execute(issueBulkChangeQuery, userSession);
+//    assertThat(result.issuesChanged()).hasSize(1);
+//    assertThat(result.issuesNotChanged()).isEmpty();
+//
+//    verify(issueStorage).save(eq(issue));
+//    verifyNoMoreInteractions(issueStorage);
+//    verifyZeroInteractions(notificationService);
+//  }
+//
+//  @Test
+//  public void should_execute_bulk_change_with_comment() {
+//    Map<String, Object> properties = newHashMap();
+//    properties.put("issues", "ABCD");
+//    properties.put("actions", "assign");
+//    properties.put("assign.assignee", "fred");
+//
+//    Action commentAction = mock(Action.class);
+//    when(commentAction.key()).thenReturn("comment");
+//    when(commentAction.supports(any(Issue.class))).thenReturn(true);
+//    when(commentAction.verify(anyMap(), anyListOf(Issue.class), any(UserSession.class))).thenReturn(true);
+//    when(commentAction.execute(anyMap(), any(IssueBulkChangeService.ActionContext.class))).thenReturn(true);
+//    actions.add(commentAction);
+//    actions.add(new MockAction("assign"));
+//
+//    IssueBulkChangeQuery issueBulkChangeQuery = new IssueBulkChangeQuery(properties, "my comment", true);
+//    IssueBulkChangeResult result = service.execute(issueBulkChangeQuery, userSession);
+//    assertThat(result.issuesChanged()).hasSize(1);
+//    assertThat(result.issuesNotChanged()).isEmpty();
+//
+//    verify(commentAction).execute(anyMap(), any(IssueBulkChangeService.ActionContext.class));
+//    verify(issueStorage).save(eq(issue));
+//  }
+//
+//  @Test
+//  public void should_execute_bulk_change_with_comment_only_on_changed_issues() {
+//    IssueDto issueDto1 = IssueTesting.newDto(RuleTesting.newDto(rule.ruleKey()).setId(50), file, project).setKee("ABCD");
+//    IssueDto issueDto2 = IssueTesting.newDto(RuleTesting.newDto(rule.ruleKey()).setId(50), file, project).setKee("EFGH");
+//
+//    org.sonar.server.search.Result<Issue> resultIssues = mock(org.sonar.server.search.Result.class);
+//    when(resultIssues.getHits()).thenReturn(Lists.<Issue>newArrayList(issueDto1.toDefaultIssue(), issueDto2.toDefaultIssue()));
+//    when(issueService.search(any(IssueQuery.class), any(QueryContext.class))).thenReturn(resultIssues);
+//    when(issueDao.selectByKeys(dbSession, newArrayList("ABCD", "EFGH"))).thenReturn(newArrayList(issueDto1, issueDto2));
+//
+//    Map<String, Object> properties = newHashMap();
+//    properties.put("issues", "ABCD,EFGH");
+//    properties.put("actions", "assign");
+//    properties.put("assign.assignee", "fred");
+//
+//    Action commentAction = mock(Action.class);
+//    when(commentAction.key()).thenReturn("comment");
+//    when(commentAction.supports(any(Issue.class))).thenReturn(true);
+//    when(commentAction.verify(anyMap(), anyListOf(Issue.class), any(UserSession.class))).thenReturn(true);
+//    when(commentAction.execute(anyMap(), any(IssueBulkChangeService.ActionContext.class))).thenReturn(true);
+//    actions.add(commentAction);
+//
+//    // This action will only be executed on the first issue, not the second
+//    Action assignAction = mock(Action.class);
+//    when(assignAction.key()).thenReturn("assign");
+//    when(assignAction.supports(any(Issue.class))).thenReturn(true).thenReturn(false);
+//    when(assignAction.verify(anyMap(), anyListOf(Issue.class), any(UserSession.class))).thenReturn(true);
+//    when(assignAction.execute(anyMap(), any(IssueBulkChangeService.ActionContext.class))).thenReturn(true).thenReturn(false);
+//    actions.add(assignAction);
+//
+//    IssueBulkChangeQuery issueBulkChangeQuery = new IssueBulkChangeQuery(properties, "my comment", true);
+//    IssueBulkChangeResult result = service.execute(issueBulkChangeQuery, userSession);
+//    assertThat(result.issuesChanged()).hasSize(1);
+//    assertThat(result.issuesNotChanged()).hasSize(1);
+//
+//    // Only one issue will receive the comment
+//    verify(assignAction, times(1)).execute(anyMap(), any(IssueBulkChangeService.ActionContext.class));
+//    verify(issueStorage).save(eq(issueDto1.toDefaultIssue()));
+//  }
+//
+//  @Test
+//  public void should_save_once_per_issue() {
+//    Map<String, Object> properties = newHashMap();
+//    properties.put("issues", "ABCD");
+//    properties.put("actions", "assign,set_severity");
+//    properties.put("assign.assignee", "fred");
+//    properties.put("set_severity.severity", "MINOR");
+//
+//    actions.add(new MockAction("set_severity"));
+//    actions.add(new MockAction("assign"));
+//
+//    IssueBulkChangeQuery issueBulkChangeQuery = new IssueBulkChangeQuery(properties, true);
+//    IssueBulkChangeResult result = service.execute(issueBulkChangeQuery, userSession);
+//    assertThat(result.issuesChanged()).hasSize(1);
+//    assertThat(result.issuesNotChanged()).isEmpty();
+//
+//    verify(issueStorage, times(1)).save(eq(issue));
+//    verifyNoMoreInteractions(issueStorage);
+//    verify(notificationService).scheduleForSending(any(IssueChangeNotification.class));
+//    verifyNoMoreInteractions(notificationService);
+//  }
+//
+//  @Test
+//  public void should_not_execute_bulk_if_issue_does_not_support_action() {
+//    Map<String, Object> properties = newHashMap();
+//    properties.put("issues", "ABCD");
+//    properties.put("actions", "assign");
+//    properties.put("assign.assignee", "fred");
+//    actions.add(new MockAction("assign", true, true, false));
+//
+//    IssueBulkChangeQuery issueBulkChangeQuery = new IssueBulkChangeQuery(properties, true);
+//    IssueBulkChangeResult result = service.execute(issueBulkChangeQuery, userSession);
+//    assertThat(result.issuesChanged()).isEmpty();
+//    assertThat(result.issuesNotChanged()).hasSize(1);
+//
+//    verifyZeroInteractions(issueStorage);
+//    verifyZeroInteractions(notificationService);
+//  }
+//
+//  @Test
+//  public void should_not_execute_bulk_if_action_is_not_verified() {
+//    Map<String, Object> properties = newHashMap();
+//    properties.put("issues", "ABCD");
+//    properties.put("actions", "assign");
+//    properties.put("assign.assignee", "fred");
+//    actions.add(new MockAction("assign", false, true, true));
+//
+//    IssueBulkChangeQuery issueBulkChangeQuery = new IssueBulkChangeQuery(properties, true);
+//    IssueBulkChangeResult result = service.execute(issueBulkChangeQuery, userSession);
+//    assertThat(result.issuesChanged()).isEmpty();
+//    assertThat(result.issuesNotChanged()).isEmpty();
+//
+//    verifyZeroInteractions(issueStorage);
+//    verifyZeroInteractions(notificationService);
+//  }
+//
+//  @Test
+//  public void should_not_execute_bulk_if_action_could_not_be_executed_on_issue() {
+//    Map<String, Object> properties = newHashMap();
+//    properties.put("issues", "ABCD");
+//    properties.put("actions", "assign");
+//    properties.put("assign.assignee", "fred");
+//    actions.add(new MockAction("assign", true, false, true));
+//
+//    IssueBulkChangeQuery issueBulkChangeQuery = new IssueBulkChangeQuery(properties, true);
+//    IssueBulkChangeResult result = service.execute(issueBulkChangeQuery, userSession);
+//    assertThat(result.issuesChanged()).isEmpty();
+//    assertThat(result.issuesNotChanged()).hasSize(1);
+//
+//    verifyZeroInteractions(issueStorage);
+//    verifyZeroInteractions(notificationService);
+//  }
+//
+//  @Test
+//  public void should_not_execute_bulk_on_unexpected_error() {
+//    Map<String, Object> properties = newHashMap();
+//    properties.put("issues", "ABCD");
+//    properties.put("actions", "assign");
+//    properties.put("assign.assignee", "fred");
+//
+//    Action action = mock(Action.class);
+//    when(action.key()).thenReturn("assign");
+//    when(action.supports(any(Issue.class))).thenReturn(true);
+//    when(action.verify(anyMap(), anyListOf(Issue.class), any(UserSession.class))).thenReturn(true);
+//    doThrow(new RuntimeException("Error")).when(action).execute(anyMap(), any(IssueBulkChangeService.ActionContext.class));
+//    actions.add(action);
+//
+//    IssueBulkChangeQuery issueBulkChangeQuery = new IssueBulkChangeQuery(properties, true);
+//    IssueBulkChangeResult result = service.execute(issueBulkChangeQuery, userSession);
+//    assertThat(result.issuesChanged()).isEmpty();
+//    assertThat(result.issuesNotChanged()).hasSize(1);
+//
+//    verifyZeroInteractions(issueStorage);
+//    verifyZeroInteractions(notificationService);
+//  }
+//
+//  @Test
+//  public void should_fail_if_user_not_logged() {
+//    userSession = MockUserSession.create().setLogin(null);
+//
+//    Map<String, Object> properties = newHashMap();
+//    properties.put("issues", "ABCD");
+//    properties.put("actions", "assign");
+//    properties.put("assign.assignee", "fred");
+//    IssueBulkChangeQuery issueBulkChangeQuery = new IssueBulkChangeQuery(properties, true);
+//    try {
+//      service.execute(issueBulkChangeQuery, userSession);
+//      fail();
+//    } catch (Exception e) {
+//      assertThat(e).isInstanceOf(UnauthorizedException.class);
+//    }
+//    verifyZeroInteractions(issueStorage);
+//    verifyZeroInteractions(notificationService);
+//  }
+//
+//  @Test
+//  public void should_fail_if_action_not_found() {
+//    Map<String, Object> properties = newHashMap();
+//    properties.put("issues", "ABCD");
+//    properties.put("actions", "unknown");
+//    properties.put("unknown.unknown", "unknown");
+//    IssueBulkChangeQuery issueBulkChangeQuery = new IssueBulkChangeQuery(properties, true);
+//    try {
+//      service.execute(issueBulkChangeQuery, userSession);
+//      fail();
+//    } catch (Exception e) {
+//      assertThat(e).isInstanceOf(BadRequestException.class).hasMessage("The action : 'unknown' is unknown");
+//    }
+//    verifyZeroInteractions(issueStorage);
+//    verifyZeroInteractions(notificationService);
+//  }
+//
+//  class MockAction extends Action {
+//
+//    private boolean verify;
+//    private boolean execute;
+//
+//    public MockAction(String key, boolean verify, boolean execute, final boolean support) {
+//      super(key);
+//      this.verify = verify;
+//      this.execute = execute;
+//      setConditions(new Condition() {
+//        @Override
+//        public boolean matches(Issue issue) {
+//          return support;
+//        }
+//      });
+//    }
+//
+//    public MockAction(String key) {
+//      this(key, true, true, true);
+//    }
+//
+//    @Override
+//    boolean verify(Map<String, Object> properties, List<Issue> issues, UserSession userSession) {
+//      return verify;
+//    }
+//
+//    @Override
+//    boolean execute(Map<String, Object> properties, Context context) {
+//      return execute;
+//    }
+//  }
 }
index b405eef9efd916fbafe06f15447e87aa5e025feb..0ae5cf970a6cd00381c11ecee9d7e9b4e241800b 100644 (file)
  */
 package org.sonar.server.issue;
 
-import com.google.common.collect.*;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterators;
+import com.google.common.collect.Multiset;
+import com.google.common.collect.Sets;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.ClassRule;
@@ -44,6 +48,8 @@ import org.sonar.core.user.UserDto;
 import org.sonar.server.component.ComponentTesting;
 import org.sonar.server.component.db.ComponentDao;
 import org.sonar.server.db.DbClient;
+import org.sonar.server.es.SearchOptions;
+import org.sonar.server.es.SearchResult;
 import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.exceptions.NotFoundException;
 import org.sonar.server.issue.db.IssueDao;
@@ -55,8 +61,6 @@ import org.sonar.server.permission.PermissionChange;
 import org.sonar.server.rule.RuleTesting;
 import org.sonar.server.rule.db.RuleDao;
 import org.sonar.server.search.BaseNormalizer;
-import org.sonar.server.search.IndexClient;
-import org.sonar.server.search.QueryContext;
 import org.sonar.server.source.index.SourceLineDoc;
 import org.sonar.server.source.index.SourceLineIndexer;
 import org.sonar.server.source.index.SourceLineResultSetIterator;
@@ -66,7 +70,6 @@ import org.sonar.server.user.NewUser;
 import org.sonar.server.user.UserService;
 import org.sonar.server.user.db.GroupDao;
 
-import java.util.Arrays;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
@@ -85,7 +88,7 @@ public class IssueServiceMediumTest {
   public static ServerTester tester = new ServerTester();
 
   DbClient db;
-  IndexClient indexClient;
+  IssueIndex IssueIndex;
   DbSession session;
   IssueService service;
 
@@ -93,7 +96,7 @@ public class IssueServiceMediumTest {
   public void setUp() throws Exception {
     tester.clearDbAndIndexes();
     db = tester.get(DbClient.class);
-    indexClient = tester.get(IndexClient.class);
+    IssueIndex = tester.get(IssueIndex.class);
     session = db.openSession(false);
     service = tester.get(IssueService.class);
   }
@@ -126,14 +129,14 @@ public class IssueServiceMediumTest {
     saveIssue(IssueTesting.newDto(rule, file, project).setActionPlanKey("P1"));
     saveIssue(IssueTesting.newDto(rule, file, project).setActionPlanKey("P2").setResolution("NONE"));
 
-    org.sonar.server.search.Result<Issue> result = service.search(IssueQuery.builder().build(), new QueryContext());
-    assertThat(result.getHits()).hasSize(2);
-    assertThat(result.getFacets()).isEmpty();
+    SearchResult<IssueDoc> result = service.search(IssueQuery.builder().build(), new SearchOptions());
+    assertThat(result.getDocs()).hasSize(2);
+    assertThat(result.getFacets().getNames()).isEmpty();
 
-    result = service.search(IssueQuery.builder().build(), new QueryContext().addFacets(Arrays.asList("actionPlans", "assignees")));
-    assertThat(result.getFacets().keySet()).hasSize(2);
-    assertThat(result.getFacetKeys("actionPlans")).hasSize(2);
-    assertThat(result.getFacetKeys("assignees")).hasSize(1);
+    result = service.search(IssueQuery.builder().build(), new SearchOptions().addFacets("actionPlans", "assignees"));
+    assertThat(result.getFacets().getNames()).hasSize(2);
+    assertThat(result.getFacets().get("actionPlans")).hasSize(2);
+    assertThat(result.getFacets().get("assignees")).hasSize(1);
   }
 
   @Test
@@ -162,11 +165,11 @@ public class IssueServiceMediumTest {
 
     IssueDto issue = saveIssue(IssueTesting.newDto(rule, file, project).setStatus(Issue.STATUS_OPEN));
 
-    assertThat(indexClient.get(IssueIndex.class).getByKey(issue.getKey()).status()).isEqualTo(Issue.STATUS_OPEN);
+    assertThat(IssueIndex.getByKey(issue.getKey()).status()).isEqualTo(Issue.STATUS_OPEN);
 
     service.doTransition(issue.getKey(), DefaultTransitions.CONFIRM);
 
-    assertThat(indexClient.get(IssueIndex.class).getByKey(issue.getKey()).status()).isEqualTo(Issue.STATUS_CONFIRMED);
+    assertThat(IssueIndex.getByKey(issue.getKey()).status()).isEqualTo(Issue.STATUS_CONFIRMED);
   }
 
   @Test
@@ -183,11 +186,11 @@ public class IssueServiceMediumTest {
     session.commit();
     index();
 
-    assertThat(indexClient.get(IssueIndex.class).getByKey(issue.getKey()).assignee()).isNull();
+    assertThat(IssueIndex.getByKey(issue.getKey()).assignee()).isNull();
 
     service.assign(issue.getKey(), user.getLogin());
 
-    assertThat(indexClient.get(IssueIndex.class).getByKey(issue.getKey()).assignee()).isEqualTo("perceval");
+    assertThat(IssueIndex.getByKey(issue.getKey()).assignee()).isEqualTo("perceval");
   }
 
   @Test
@@ -204,11 +207,11 @@ public class IssueServiceMediumTest {
     session.commit();
     index();
 
-    assertThat(indexClient.get(IssueIndex.class).getByKey(issue.getKey()).assignee()).isEqualTo("perceval");
+    assertThat(IssueIndex.getByKey(issue.getKey()).assignee()).isEqualTo("perceval");
 
     service.assign(issue.getKey(), "");
 
-    assertThat(indexClient.get(IssueIndex.class).getByKey(issue.getKey()).assignee()).isNull();
+    assertThat(IssueIndex.getByKey(issue.getKey()).assignee()).isNull();
   }
 
   @Test
@@ -242,11 +245,11 @@ public class IssueServiceMediumTest {
     session.commit();
     index();
 
-    assertThat(indexClient.get(IssueIndex.class).getByKey(issue.getKey()).actionPlanKey()).isNull();
+    assertThat(IssueIndex.getByKey(issue.getKey()).actionPlanKey()).isNull();
 
     service.plan(issue.getKey(), actionPlanKey);
 
-    assertThat(indexClient.get(IssueIndex.class).getByKey(issue.getKey()).actionPlanKey()).isEqualTo(actionPlanKey);
+    assertThat(IssueIndex.getByKey(issue.getKey()).actionPlanKey()).isEqualTo(actionPlanKey);
   }
 
   @Test
@@ -260,11 +263,11 @@ public class IssueServiceMediumTest {
     db.actionPlanDao().save(new ActionPlanDto().setKey(actionPlanKey).setProjectId(project.getId()));
     IssueDto issue = saveIssue(IssueTesting.newDto(rule, file, project).setActionPlanKey(actionPlanKey));
 
-    assertThat(indexClient.get(IssueIndex.class).getByKey(issue.getKey()).actionPlanKey()).isEqualTo(actionPlanKey);
+    assertThat(IssueIndex.getByKey(issue.getKey()).actionPlanKey()).isEqualTo(actionPlanKey);
 
     service.plan(issue.getKey(), null);
 
-    assertThat(indexClient.get(IssueIndex.class).getByKey(issue.getKey()).actionPlanKey()).isNull();
+    assertThat(IssueIndex.getByKey(issue.getKey()).actionPlanKey()).isNull();
   }
 
   @Test
@@ -292,11 +295,11 @@ public class IssueServiceMediumTest {
 
     IssueDto issue = saveIssue(IssueTesting.newDto(rule, file, project).setSeverity(Severity.BLOCKER));
 
-    assertThat(indexClient.get(IssueIndex.class).getByKey(issue.getKey()).severity()).isEqualTo(Severity.BLOCKER);
+    assertThat(IssueIndex.getByKey(issue.getKey()).severity()).isEqualTo(Severity.BLOCKER);
 
     service.setSeverity(issue.getKey(), Severity.MINOR);
 
-    assertThat(indexClient.get(IssueIndex.class).getByKey(issue.getKey()).severity()).isEqualTo(Severity.MINOR);
+    assertThat(IssueIndex.getByKey(issue.getKey()).severity()).isEqualTo(Severity.MINOR);
   }
 
   @Test
@@ -311,7 +314,7 @@ public class IssueServiceMediumTest {
 
     Issue result = service.createManualIssue(file.key(), manualRule.getKey(), null, "Fix it", Severity.MINOR, 2d);
 
-    IssueDoc manualIssue = (IssueDoc) indexClient.get(IssueIndex.class).getByKey(result.key());
+    IssueDoc manualIssue = (IssueDoc) IssueIndex.getByKey(result.key());
     assertThat(manualIssue.componentUuid()).isEqualTo(file.uuid());
     assertThat(manualIssue.projectUuid()).isEqualTo(project.uuid());
     assertThat(manualIssue.ruleKey()).isEqualTo(manualRule.getKey());
@@ -337,7 +340,7 @@ public class IssueServiceMediumTest {
 
     Issue result = service.createManualIssue(file.key(), manualRule.getKey(), 1, "Fix it", Severity.MINOR, 2d);
 
-    IssueDoc manualIssue = (IssueDoc) indexClient.get(IssueIndex.class).getByKey(result.key());
+    IssueDoc manualIssue = (IssueDoc) IssueIndex.getByKey(result.key());
     assertThat(manualIssue.componentUuid()).isEqualTo(file.uuid());
     assertThat(manualIssue.projectUuid()).isEqualTo(project.uuid());
     assertThat(manualIssue.ruleKey()).isEqualTo(manualRule.getKey());
@@ -361,7 +364,7 @@ public class IssueServiceMediumTest {
 
     Issue result = service.createManualIssue(file.key(), manualRule.getKey(), null, "Fix it", null, 2d);
 
-    Issue manualIssue = indexClient.get(IssueIndex.class).getByKey(result.key());
+    Issue manualIssue = IssueIndex.getByKey(result.key());
     assertThat(manualIssue.severity()).isEqualTo(Severity.MAJOR);
   }
 
@@ -377,7 +380,7 @@ public class IssueServiceMediumTest {
 
     Issue result = service.createManualIssue(file.key(), manualRule.getKey(), null, null, null, 2d);
 
-    Issue manualIssue = indexClient.get(IssueIndex.class).getByKey(result.key());
+    Issue manualIssue = IssueIndex.getByKey(result.key());
     assertThat(manualIssue.message()).isEqualTo("Manual rule name");
   }
 
@@ -397,7 +400,7 @@ public class IssueServiceMediumTest {
 
     Issue result = service.createManualIssue(file.key(), manualRule.getKey(), 1, "Fix it", Severity.MINOR, 2d);
 
-    IssueDoc manualIssue = (IssueDoc) indexClient.get(IssueIndex.class).getByKey(result.key());
+    IssueDoc manualIssue = (IssueDoc) IssueIndex.getByKey(result.key());
     assertThat(manualIssue.assignee()).isNull();
   }
 
@@ -415,7 +418,7 @@ public class IssueServiceMediumTest {
 
     Issue result = service.createManualIssue(file.key(), manualRule.getKey(), 1, "Fix it", Severity.MINOR, 2d);
 
-    IssueDoc manualIssue = (IssueDoc) indexClient.get(IssueIndex.class).getByKey(result.key());
+    IssueDoc manualIssue = (IssueDoc) IssueIndex.getByKey(result.key());
     assertThat(manualIssue.assignee()).isNull();
   }
 
@@ -496,7 +499,7 @@ public class IssueServiceMediumTest {
     ComponentDto file = newFile(project);
     saveIssue(IssueTesting.newDto(rule, file, project));
 
-    List<Issue> result = service.search(IssueQuery.builder().build(), new QueryContext()).getHits();
+    List<IssueDoc> result = service.search(IssueQuery.builder().build(), new SearchOptions()).getDocs();
     assertThat(result).hasSize(1);
   }
 
@@ -548,15 +551,15 @@ public class IssueServiceMediumTest {
 
     IssueDto issue = saveIssue(IssueTesting.newDto(rule, file, project));
 
-    assertThat(indexClient.get(IssueIndex.class).getByKey(issue.getKey()).tags()).isEmpty();
+    assertThat(IssueIndex.getByKey(issue.getKey()).tags()).isEmpty();
 
     // Tags are lowercased
     service.setTags(issue.getKey(), ImmutableSet.of("bug", "Convention"));
-    assertThat(indexClient.get(IssueIndex.class).getByKey(issue.getKey()).tags()).containsOnly("bug", "convention");
+    assertThat(IssueIndex.getByKey(issue.getKey()).tags()).containsOnly("bug", "convention");
 
     // nulls and empty tags are ignored
     service.setTags(issue.getKey(), Sets.newHashSet("security", null, "", "convention"));
-    assertThat(indexClient.get(IssueIndex.class).getByKey(issue.getKey()).tags()).containsOnly("security", "convention");
+    assertThat(IssueIndex.getByKey(issue.getKey()).tags()).containsOnly("security", "convention");
 
     // tag validation
     try {
@@ -564,14 +567,14 @@ public class IssueServiceMediumTest {
     } catch (Exception exception) {
       assertThat(exception).isInstanceOf(IllegalArgumentException.class);
     }
-    assertThat(indexClient.get(IssueIndex.class).getByKey(issue.getKey()).tags()).containsOnly("security", "convention");
+    assertThat(IssueIndex.getByKey(issue.getKey()).tags()).containsOnly("security", "convention");
 
     // unchanged tags
     service.setTags(issue.getKey(), ImmutableSet.of("convention", "security"));
-    assertThat(indexClient.get(IssueIndex.class).getByKey(issue.getKey()).tags()).containsOnly("security", "convention");
+    assertThat(IssueIndex.getByKey(issue.getKey()).tags()).containsOnly("security", "convention");
 
     service.setTags(issue.getKey(), ImmutableSet.<String>of());
-    assertThat(indexClient.get(IssueIndex.class).getByKey(issue.getKey()).tags()).isEmpty();
+    assertThat(IssueIndex.getByKey(issue.getKey()).tags()).isEmpty();
   }
 
   @Test
@@ -585,7 +588,7 @@ public class IssueServiceMediumTest {
     saveIssue(IssueTesting.newDto(rule, file, project).setTags(ImmutableSet.of("convention", "java8", "bug")).setResolution(Issue.RESOLUTION_FIXED));
     saveIssue(IssueTesting.newDto(rule, file, project).setTags(ImmutableSet.of("convention")));
 
-    assertThat(service.listTagsForComponent(project.uuid(), 5)).contains(entry("convention", 3L), entry("bug", 2L), entry("java8", 1L));
+    assertThat(service.listTagsForComponent(project.uuid(), 5)).containsOnly(entry("convention", 3L), entry("bug", 2L), entry("java8", 1L));
     assertThat(service.listTagsForComponent(project.uuid(), 2)).contains(entry("convention", 3L), entry("bug", 2L)).doesNotContainEntry("java8", 1L);
     assertThat(service.listTagsForComponent("other", 10)).isEmpty();
   }
index bc1dd5ba7f94401657efe98f99e6954beb8d3a99..363de1c16e5a7c31f7b5615a1709e389c6699d59 100644 (file)
@@ -26,8 +26,6 @@ import org.hamcrest.BaseMatcher;
 import org.hamcrest.Description;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
-import org.sonar.api.issue.Issue;
-import org.sonar.api.issue.internal.DefaultIssue;
 import org.sonar.api.web.UserRole;
 import org.sonar.core.issue.DefaultIssueFilter;
 import org.sonar.core.issue.IssueFilterSerializer;
@@ -37,14 +35,15 @@ import org.sonar.core.issue.db.IssueFilterFavouriteDao;
 import org.sonar.core.issue.db.IssueFilterFavouriteDto;
 import org.sonar.core.permission.GlobalPermissions;
 import org.sonar.core.user.AuthorizationDao;
+import org.sonar.server.es.SearchOptions;
+import org.sonar.server.es.SearchResult;
 import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.exceptions.NotFoundException;
 import org.sonar.server.exceptions.UnauthorizedException;
 import org.sonar.server.issue.IssueQuery;
+import org.sonar.server.issue.index.IssueDoc;
 import org.sonar.server.issue.index.IssueIndex;
-import org.sonar.server.search.QueryContext;
-import org.sonar.server.search.Result;
 import org.sonar.server.user.MockUserSession;
 import org.sonar.server.user.UserSession;
 
@@ -528,14 +527,14 @@ public class IssueFilterServiceTest {
   @Test
   public void should_execute_from_issue_query() {
     IssueQuery issueQuery = IssueQuery.builder().build();
-    QueryContext queryContext = new QueryContext().setPage(2, 50);
+    SearchOptions searchOptions = new SearchOptions().setPage(2, 50);
 
-    Result<Issue> result = mock(Result.class);
-    when(result.getHits()).thenReturn(newArrayList((Issue) new DefaultIssue()));
+    SearchResult<IssueDoc> result = mock(SearchResult.class);
+    when(result.getDocs()).thenReturn(newArrayList((IssueDoc) new IssueDoc()));
     when(result.getTotal()).thenReturn(100L);
-    when(issueIndex.search(issueQuery, queryContext)).thenReturn(result);
+    when(issueIndex.search(issueQuery, searchOptions)).thenReturn(result);
 
-    IssueFilterService.IssueFilterResult issueFilterResult = service.execute(issueQuery, queryContext);
+    IssueFilterService.IssueFilterResult issueFilterResult = service.execute(issueQuery, searchOptions);
     assertThat(issueFilterResult.issues()).hasSize(1);
     assertThat(issueFilterResult.paging().total()).isEqualTo(100);
     assertThat(issueFilterResult.paging().pageIndex()).isEqualTo(2);
index fb39f468d5e8056e28e55529cf7daf097f87a48d..bcb26d6a3e2fd5df835b770fd18a08e25b21b076 100644 (file)
@@ -32,12 +32,11 @@ import org.sonar.api.utils.DateUtils;
 import org.sonar.api.utils.KeyValueFormat;
 import org.sonar.core.component.ComponentDto;
 import org.sonar.server.component.ComponentTesting;
+import org.sonar.server.es.SearchOptions;
+import org.sonar.server.es.SearchResult;
 import org.sonar.server.exceptions.NotFoundException;
 import org.sonar.server.issue.IssueQuery;
 import org.sonar.server.issue.IssueTesting;
-import org.sonar.server.search.FacetValue;
-import org.sonar.server.search.QueryContext;
-import org.sonar.server.search.Result;
 import org.sonar.server.tester.ServerTester;
 import org.sonar.server.user.MockUserSession;
 import org.sonar.server.view.index.ViewDoc;
@@ -46,12 +45,15 @@ import org.sonar.server.view.index.ViewIndexer;
 import javax.annotation.Nullable;
 
 import java.util.Arrays;
-import java.util.Collection;
 import java.util.Date;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 
 import static com.google.common.collect.Lists.newArrayList;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.entry;
 
 /**
  * As soon as IssueIndex take {@link org.sonar.server.es.EsClient} in its constructor, ServerTester should be replaced by EsTester, it will make this test going faster !
@@ -126,9 +128,9 @@ public class IssueIndexMediumTest {
       IssueTesting.newDoc("1", ComponentTesting.newFileDto(project)),
       IssueTesting.newDoc("2", ComponentTesting.newFileDto(project)));
 
-    assertThat(index.search(IssueQuery.builder().issueKeys(newArrayList("1", "2")).build(), new QueryContext()).getHits()).hasSize(2);
-    assertThat(index.search(IssueQuery.builder().issueKeys(newArrayList("1")).build(), new QueryContext()).getHits()).hasSize(1);
-    assertThat(index.search(IssueQuery.builder().issueKeys(newArrayList("3", "4")).build(), new QueryContext()).getHits()).isEmpty();
+    assertThat(index.search(IssueQuery.builder().issueKeys(newArrayList("1", "2")).build(), new SearchOptions()).getDocs()).hasSize(2);
+    assertThat(index.search(IssueQuery.builder().issueKeys(newArrayList("1")).build(), new SearchOptions()).getDocs()).hasSize(1);
+    assertThat(index.search(IssueQuery.builder().issueKeys(newArrayList("3", "4")).build(), new SearchOptions()).getDocs()).isEmpty();
   }
 
   @Test
@@ -145,8 +147,8 @@ public class IssueIndexMediumTest {
       IssueTesting.newDoc("ISSUE5", subModule),
       IssueTesting.newDoc("ISSUE6", ComponentTesting.newFileDto(subModule)));
 
-    assertThat(index.search(IssueQuery.builder().projectUuids(newArrayList(project.uuid())).build(), new QueryContext()).getHits()).hasSize(6);
-    assertThat(index.search(IssueQuery.builder().projectUuids(newArrayList("unknown")).build(), new QueryContext()).getHits()).isEmpty();
+    assertThat(index.search(IssueQuery.builder().projectUuids(newArrayList(project.uuid())).build(), new SearchOptions()).getDocs()).hasSize(6);
+    assertThat(index.search(IssueQuery.builder().projectUuids(newArrayList("unknown")).build(), new SearchOptions()).getDocs()).isEmpty();
   }
 
   @Test
@@ -159,9 +161,9 @@ public class IssueIndexMediumTest {
       IssueTesting.newDoc("ISSUE2", ComponentTesting.newFileDto(project)),
       IssueTesting.newDoc("ISSUE3", ComponentTesting.newFileDto(project2)));
 
-    Result<Issue> result = index.search(IssueQuery.builder().build(), new QueryContext().addFacets(newArrayList("projectUuids")));
-    assertThat(result.getFacets()).containsOnlyKeys("projectUuids");
-    assertThat(result.getFacets().get("projectUuids")).containsOnly(new FacetValue("ABCD", 2), new FacetValue("EFGH", 1));
+    SearchResult<IssueDoc> result = index.search(IssueQuery.builder().build(), new SearchOptions().addFacets(newArrayList("projectUuids")));
+    assertThat(result.getFacets().getNames()).containsOnly("projectUuids");
+    assertThat(result.getFacets().get("projectUuids")).containsOnly(entry("ABCD", 2L), entry("EFGH", 1L));
   }
 
   @Test
@@ -177,15 +179,15 @@ public class IssueIndexMediumTest {
       IssueTesting.newDoc("ISSUE2", file));
 
     assertThat(index.search(IssueQuery.builder().projectUuids(newArrayList(project.uuid()))
-      .moduleUuids(newArrayList(file.uuid())).build(), new QueryContext()).getHits()).isEmpty();
+      .moduleUuids(newArrayList(file.uuid())).build(), new SearchOptions()).getDocs()).isEmpty();
     assertThat(index.search(IssueQuery.builder().projectUuids(newArrayList(project.uuid()))
-      .moduleUuids(newArrayList(module.uuid())).build(), new QueryContext()).getHits()).hasSize(1);
+      .moduleUuids(newArrayList(module.uuid())).build(), new SearchOptions()).getDocs()).hasSize(1);
     assertThat(index.search(IssueQuery.builder().projectUuids(newArrayList(project.uuid()))
-      .moduleUuids(newArrayList(subModule.uuid())).build(), new QueryContext()).getHits()).hasSize(1);
+      .moduleUuids(newArrayList(subModule.uuid())).build(), new SearchOptions()).getDocs()).hasSize(1);
     assertThat(index.search(IssueQuery.builder().projectUuids(newArrayList(project.uuid()))
-      .moduleUuids(newArrayList(project.uuid())).build(), new QueryContext()).getHits()).hasSize(1);
+      .moduleUuids(newArrayList(project.uuid())).build(), new SearchOptions()).getDocs()).hasSize(1);
     assertThat(index.search(IssueQuery.builder().projectUuids(newArrayList(project.uuid()))
-      .moduleUuids(newArrayList("unknown")).build(), new QueryContext()).getHits()).isEmpty();
+      .moduleUuids(newArrayList("unknown")).build(), new SearchOptions()).getDocs()).isEmpty();
   }
 
   @Test
@@ -207,20 +209,20 @@ public class IssueIndexMediumTest {
       IssueTesting.newDoc("ISSUE5", subModule),
       IssueTesting.newDoc("ISSUE6", file3));
 
-    assertThat(index.search(IssueQuery.builder().setContextualized(true).fileUuids(newArrayList(file1.uuid(), file2.uuid(), file3.uuid())).build(), new QueryContext())
-      .getHits()).hasSize(3);
-    assertThat(index.search(IssueQuery.builder().setContextualized(true).fileUuids(newArrayList(file1.uuid())).build(), new QueryContext())
-      .getHits()).hasSize(1);
-    assertThat(index.search(IssueQuery.builder().setContextualized(true).moduleRootUuids(newArrayList(subModule.uuid())).build(), new QueryContext())
-      .getHits()).hasSize(2);
-    assertThat(index.search(IssueQuery.builder().setContextualized(true).moduleRootUuids(newArrayList(module.uuid())).build(), new QueryContext())
-      .getHits()).hasSize(4);
-    assertThat(index.search(IssueQuery.builder().setContextualized(true).projectUuids(newArrayList(project.uuid())).build(), new QueryContext())
-      .getHits()).hasSize(6);
-    assertThat(index.search(IssueQuery.builder().setContextualized(true).viewUuids(newArrayList(view)).build(), new QueryContext())
-      .getHits()).hasSize(6);
-    assertThat(index.search(IssueQuery.builder().setContextualized(true).projectUuids(newArrayList("unknown")).build(), new QueryContext())
-      .getHits()).isEmpty();
+    assertThat(index.search(IssueQuery.builder().setContextualized(true).fileUuids(newArrayList(file1.uuid(), file2.uuid(), file3.uuid())).build(), new SearchOptions())
+      .getDocs()).hasSize(3);
+    assertThat(index.search(IssueQuery.builder().setContextualized(true).fileUuids(newArrayList(file1.uuid())).build(), new SearchOptions())
+      .getDocs()).hasSize(1);
+    assertThat(index.search(IssueQuery.builder().setContextualized(true).moduleRootUuids(newArrayList(subModule.uuid())).build(), new SearchOptions())
+      .getDocs()).hasSize(2);
+    assertThat(index.search(IssueQuery.builder().setContextualized(true).moduleRootUuids(newArrayList(module.uuid())).build(), new SearchOptions())
+      .getDocs()).hasSize(4);
+    assertThat(index.search(IssueQuery.builder().setContextualized(true).projectUuids(newArrayList(project.uuid())).build(), new SearchOptions())
+      .getDocs()).hasSize(6);
+    assertThat(index.search(IssueQuery.builder().setContextualized(true).viewUuids(newArrayList(view)).build(), new SearchOptions())
+      .getDocs()).hasSize(6);
+    assertThat(index.search(IssueQuery.builder().setContextualized(true).projectUuids(newArrayList("unknown")).build(), new SearchOptions())
+      .getDocs()).isEmpty();
   }
 
   @Test
@@ -242,15 +244,15 @@ public class IssueIndexMediumTest {
       IssueTesting.newDoc("ISSUE5", subModule),
       IssueTesting.newDoc("ISSUE6", file3));
 
-    assertThat(index.search(IssueQuery.builder().projectUuids(newArrayList("unknown")).build(), new QueryContext()).getHits()).isEmpty();
-    assertThat(index.search(IssueQuery.builder().projectUuids(newArrayList(project.uuid())).build(), new QueryContext()).getHits()).hasSize(6);
-    assertThat(index.search(IssueQuery.builder().viewUuids(newArrayList(view)).build(), new QueryContext()).getHits()).hasSize(6);
-    assertThat(index.search(IssueQuery.builder().moduleUuids(newArrayList(module.uuid())).build(), new QueryContext()).getHits()).hasSize(2);
-    assertThat(index.search(IssueQuery.builder().moduleUuids(newArrayList(subModule.uuid())).build(), new QueryContext()).getHits()).hasSize(1); // XXX
-                                                                                                                                                 // Misleading
-                                                                                                                                                 // !
-    assertThat(index.search(IssueQuery.builder().fileUuids(newArrayList(file1.uuid())).build(), new QueryContext()).getHits()).hasSize(1);
-    assertThat(index.search(IssueQuery.builder().fileUuids(newArrayList(file1.uuid(), file2.uuid(), file3.uuid())).build(), new QueryContext()).getHits()).hasSize(3);
+    assertThat(index.search(IssueQuery.builder().projectUuids(newArrayList("unknown")).build(), new SearchOptions()).getDocs()).isEmpty();
+    assertThat(index.search(IssueQuery.builder().projectUuids(newArrayList(project.uuid())).build(), new SearchOptions()).getDocs()).hasSize(6);
+    assertThat(index.search(IssueQuery.builder().viewUuids(newArrayList(view)).build(), new SearchOptions()).getDocs()).hasSize(6);
+    assertThat(index.search(IssueQuery.builder().moduleUuids(newArrayList(module.uuid())).build(), new SearchOptions()).getDocs()).hasSize(2);
+    assertThat(index.search(IssueQuery.builder().moduleUuids(newArrayList(subModule.uuid())).build(), new SearchOptions()).getDocs()).hasSize(1); // XXX
+                                                                                                                                                  // Misleading
+                                                                                                                                                  // !
+    assertThat(index.search(IssueQuery.builder().fileUuids(newArrayList(file1.uuid())).build(), new SearchOptions()).getDocs()).hasSize(1);
+    assertThat(index.search(IssueQuery.builder().fileUuids(newArrayList(file1.uuid(), file2.uuid(), file3.uuid())).build(), new SearchOptions()).getDocs()).hasSize(3);
   }
 
   @Test
@@ -267,9 +269,10 @@ public class IssueIndexMediumTest {
       IssueTesting.newDoc("ISSUE4", file2),
       IssueTesting.newDoc("ISSUE5", file3));
 
-    Result<Issue> result = index.search(IssueQuery.builder().build(), new QueryContext().addFacets(newArrayList("fileUuids")));
-    assertThat(result.getFacets()).containsOnlyKeys("fileUuids");
-    assertThat(result.getFacets().get("fileUuids")).containsOnly(new FacetValue("A", 1), new FacetValue("ABCD", 1), new FacetValue("BCDE", 2), new FacetValue("CDEF", 1));
+    SearchResult<IssueDoc> result = index.search(IssueQuery.builder().build(), new SearchOptions().addFacets(newArrayList("fileUuids")));
+    assertThat(result.getFacets().getNames()).containsOnly("fileUuids");
+    assertThat(result.getFacets().get("fileUuids"))
+      .containsOnly(entry("A", 1L), entry("ABCD", 1L), entry("BCDE", 2L), entry("CDEF", 1L));
   }
 
   @Test
@@ -282,9 +285,9 @@ public class IssueIndexMediumTest {
       IssueTesting.newDoc("ISSUE1", file1).setDirectoryPath("/src/main/xoo"),
       IssueTesting.newDoc("ISSUE2", file2).setDirectoryPath("/"));
 
-    assertThat(index.search(IssueQuery.builder().directories(newArrayList("/src/main/xoo")).build(), new QueryContext()).getHits()).hasSize(1);
-    assertThat(index.search(IssueQuery.builder().directories(newArrayList("/")).build(), new QueryContext()).getHits()).hasSize(1);
-    assertThat(index.search(IssueQuery.builder().directories(newArrayList("unknown")).build(), new QueryContext()).getHits()).isEmpty();
+    assertThat(index.search(IssueQuery.builder().directories(newArrayList("/src/main/xoo")).build(), new SearchOptions()).getDocs()).hasSize(1);
+    assertThat(index.search(IssueQuery.builder().directories(newArrayList("/")).build(), new SearchOptions()).getDocs()).hasSize(1);
+    assertThat(index.search(IssueQuery.builder().directories(newArrayList("unknown")).build(), new SearchOptions()).getDocs()).isEmpty();
   }
 
   @Test
@@ -297,9 +300,9 @@ public class IssueIndexMediumTest {
       IssueTesting.newDoc("ISSUE1", file1).setDirectoryPath("/src/main/xoo"),
       IssueTesting.newDoc("ISSUE2", file2).setDirectoryPath("/"));
 
-    Result<Issue> result = index.search(IssueQuery.builder().build(), new QueryContext().addFacets(newArrayList("directories")));
-    assertThat(result.getFacets()).containsOnlyKeys("directories");
-    assertThat(result.getFacets().get("directories")).containsOnly(new FacetValue("/src/main/xoo", 1), new FacetValue("/", 1));
+    SearchResult<IssueDoc> result = index.search(IssueQuery.builder().build(), new SearchOptions().addFacets(newArrayList("directories")));
+    assertThat(result.getFacets().getNames()).containsOnly("directories");
+    assertThat(result.getFacets().get("directories")).containsOnly(entry("/src/main/xoo", 1L), entry("/", 1L));
   }
 
   @Test
@@ -322,10 +325,10 @@ public class IssueIndexMediumTest {
     String view2 = "CDEF";
     indexView(view2, newArrayList(project2.uuid()));
 
-    assertThat(index.search(IssueQuery.builder().viewUuids(newArrayList(view1)).build(), new QueryContext()).getHits()).hasSize(2);
-    assertThat(index.search(IssueQuery.builder().viewUuids(newArrayList(view2)).build(), new QueryContext()).getHits()).hasSize(1);
-    assertThat(index.search(IssueQuery.builder().viewUuids(newArrayList(view1, view2)).build(), new QueryContext()).getHits()).hasSize(3);
-    assertThat(index.search(IssueQuery.builder().viewUuids(newArrayList("unknown")).build(), new QueryContext()).getHits()).isEmpty();
+    assertThat(index.search(IssueQuery.builder().viewUuids(newArrayList(view1)).build(), new SearchOptions()).getDocs()).hasSize(2);
+    assertThat(index.search(IssueQuery.builder().viewUuids(newArrayList(view2)).build(), new SearchOptions()).getDocs()).hasSize(1);
+    assertThat(index.search(IssueQuery.builder().viewUuids(newArrayList(view1, view2)).build(), new SearchOptions()).getDocs()).hasSize(3);
+    assertThat(index.search(IssueQuery.builder().viewUuids(newArrayList("unknown")).build(), new SearchOptions()).getDocs()).isEmpty();
   }
 
   @Test
@@ -337,9 +340,9 @@ public class IssueIndexMediumTest {
       IssueTesting.newDoc("ISSUE1", file).setSeverity(Severity.INFO),
       IssueTesting.newDoc("ISSUE2", file).setSeverity(Severity.MAJOR));
 
-    assertThat(index.search(IssueQuery.builder().severities(newArrayList(Severity.INFO, Severity.MAJOR)).build(), new QueryContext()).getHits()).hasSize(2);
-    assertThat(index.search(IssueQuery.builder().severities(newArrayList(Severity.INFO)).build(), new QueryContext()).getHits()).hasSize(1);
-    assertThat(index.search(IssueQuery.builder().severities(newArrayList(Severity.BLOCKER)).build(), new QueryContext()).getHits()).isEmpty();
+    assertThat(index.search(IssueQuery.builder().severities(newArrayList(Severity.INFO, Severity.MAJOR)).build(), new SearchOptions()).getDocs()).hasSize(2);
+    assertThat(index.search(IssueQuery.builder().severities(newArrayList(Severity.INFO)).build(), new SearchOptions()).getDocs()).hasSize(1);
+    assertThat(index.search(IssueQuery.builder().severities(newArrayList(Severity.BLOCKER)).build(), new SearchOptions()).getDocs()).isEmpty();
   }
 
   @Test
@@ -352,9 +355,9 @@ public class IssueIndexMediumTest {
       IssueTesting.newDoc("ISSUE2", file).setSeverity(Severity.INFO),
       IssueTesting.newDoc("ISSUE3", file).setSeverity(Severity.MAJOR));
 
-    Result<Issue> result = index.search(IssueQuery.builder().build(), new QueryContext().addFacets(newArrayList("severities")));
-    assertThat(result.getFacets()).containsOnlyKeys("severities");
-    assertThat(result.getFacets().get("severities")).containsOnly(new FacetValue("INFO", 2), new FacetValue("MAJOR", 1));
+    SearchResult<IssueDoc> result = index.search(IssueQuery.builder().build(), new SearchOptions().addFacets(newArrayList("severities")));
+    assertThat(result.getFacets().getNames()).containsOnly("severities");
+    assertThat(result.getFacets().get("severities")).containsOnly(entry("INFO", 2L), entry("MAJOR", 1L));
   }
 
   @Test
@@ -366,9 +369,9 @@ public class IssueIndexMediumTest {
       IssueTesting.newDoc("ISSUE1", file).setStatus(Issue.STATUS_CLOSED),
       IssueTesting.newDoc("ISSUE2", file).setStatus(Issue.STATUS_OPEN));
 
-    assertThat(index.search(IssueQuery.builder().statuses(newArrayList(Issue.STATUS_CLOSED, Issue.STATUS_OPEN)).build(), new QueryContext()).getHits()).hasSize(2);
-    assertThat(index.search(IssueQuery.builder().statuses(newArrayList(Issue.STATUS_CLOSED)).build(), new QueryContext()).getHits()).hasSize(1);
-    assertThat(index.search(IssueQuery.builder().statuses(newArrayList(Issue.STATUS_CONFIRMED)).build(), new QueryContext()).getHits()).isEmpty();
+    assertThat(index.search(IssueQuery.builder().statuses(newArrayList(Issue.STATUS_CLOSED, Issue.STATUS_OPEN)).build(), new SearchOptions()).getDocs()).hasSize(2);
+    assertThat(index.search(IssueQuery.builder().statuses(newArrayList(Issue.STATUS_CLOSED)).build(), new SearchOptions()).getDocs()).hasSize(1);
+    assertThat(index.search(IssueQuery.builder().statuses(newArrayList(Issue.STATUS_CONFIRMED)).build(), new SearchOptions()).getDocs()).isEmpty();
   }
 
   @Test
@@ -381,9 +384,9 @@ public class IssueIndexMediumTest {
       IssueTesting.newDoc("ISSUE2", file).setStatus(Issue.STATUS_CLOSED),
       IssueTesting.newDoc("ISSUE3", file).setStatus(Issue.STATUS_OPEN));
 
-    Result<Issue> result = index.search(IssueQuery.builder().build(), new QueryContext().addFacets(newArrayList("statuses")));
-    assertThat(result.getFacets()).containsOnlyKeys("statuses");
-    assertThat(result.getFacets().get("statuses")).containsOnly(new FacetValue("CLOSED", 2), new FacetValue("OPEN", 1));
+    SearchResult<IssueDoc> result = index.search(IssueQuery.builder().build(), new SearchOptions().addFacets(newArrayList("statuses")));
+    assertThat(result.getFacets().getNames()).containsOnly("statuses");
+    assertThat(result.getFacets().get("statuses")).containsOnly(entry("CLOSED", 2L), entry("OPEN", 1L));
   }
 
   @Test
@@ -395,10 +398,10 @@ public class IssueIndexMediumTest {
       IssueTesting.newDoc("ISSUE1", file).setResolution(Issue.RESOLUTION_FALSE_POSITIVE),
       IssueTesting.newDoc("ISSUE2", file).setResolution(Issue.RESOLUTION_FIXED));
 
-    assertThat(index.search(IssueQuery.builder().resolutions(newArrayList(Issue.RESOLUTION_FALSE_POSITIVE, Issue.RESOLUTION_FIXED)).build(), new QueryContext()).getHits())
+    assertThat(index.search(IssueQuery.builder().resolutions(newArrayList(Issue.RESOLUTION_FALSE_POSITIVE, Issue.RESOLUTION_FIXED)).build(), new SearchOptions()).getDocs())
       .hasSize(2);
-    assertThat(index.search(IssueQuery.builder().resolutions(newArrayList(Issue.RESOLUTION_FALSE_POSITIVE)).build(), new QueryContext()).getHits()).hasSize(1);
-    assertThat(index.search(IssueQuery.builder().resolutions(newArrayList(Issue.RESOLUTION_REMOVED)).build(), new QueryContext()).getHits()).isEmpty();
+    assertThat(index.search(IssueQuery.builder().resolutions(newArrayList(Issue.RESOLUTION_FALSE_POSITIVE)).build(), new SearchOptions()).getDocs()).hasSize(1);
+    assertThat(index.search(IssueQuery.builder().resolutions(newArrayList(Issue.RESOLUTION_REMOVED)).build(), new SearchOptions()).getDocs()).isEmpty();
   }
 
   @Test
@@ -411,9 +414,9 @@ public class IssueIndexMediumTest {
       IssueTesting.newDoc("ISSUE2", file).setResolution(Issue.RESOLUTION_FALSE_POSITIVE),
       IssueTesting.newDoc("ISSUE3", file).setResolution(Issue.RESOLUTION_FIXED));
 
-    Result<Issue> result = index.search(IssueQuery.builder().build(), new QueryContext().addFacets(newArrayList("resolutions")));
-    assertThat(result.getFacets()).containsOnlyKeys("resolutions");
-    assertThat(result.getFacets().get("resolutions")).containsOnly(new FacetValue("FALSE-POSITIVE", 2), new FacetValue("FIXED", 1));
+    SearchResult<IssueDoc> result = index.search(IssueQuery.builder().build(), new SearchOptions().addFacets(newArrayList("resolutions")));
+    assertThat(result.getFacets().getNames()).containsOnly("resolutions");
+    assertThat(result.getFacets().get("resolutions")).containsOnly(entry("FALSE-POSITIVE", 2L), entry("FIXED", 1L));
   }
 
   @Test
@@ -426,9 +429,9 @@ public class IssueIndexMediumTest {
       IssueTesting.newDoc("ISSUE2", file).setStatus(Issue.STATUS_OPEN).setResolution(null),
       IssueTesting.newDoc("ISSUE3", file).setStatus(Issue.STATUS_OPEN).setResolution(null));
 
-    assertThat(index.search(IssueQuery.builder().resolved(true).build(), new QueryContext()).getHits()).hasSize(1);
-    assertThat(index.search(IssueQuery.builder().resolved(false).build(), new QueryContext()).getHits()).hasSize(2);
-    assertThat(index.search(IssueQuery.builder().resolved(null).build(), new QueryContext()).getHits()).hasSize(3);
+    assertThat(index.search(IssueQuery.builder().resolved(true).build(), new SearchOptions()).getDocs()).hasSize(1);
+    assertThat(index.search(IssueQuery.builder().resolved(false).build(), new SearchOptions()).getDocs()).hasSize(2);
+    assertThat(index.search(IssueQuery.builder().resolved(null).build(), new SearchOptions()).getDocs()).hasSize(3);
   }
 
   @Test
@@ -440,10 +443,9 @@ public class IssueIndexMediumTest {
       IssueTesting.newDoc("ISSUE1", file).setActionPlanKey("plan1"),
       IssueTesting.newDoc("ISSUE2", file).setActionPlanKey("plan2"));
 
-    assertThat(index.search(IssueQuery.builder().actionPlans(newArrayList("plan1")).build(), new QueryContext()).getHits()).hasSize(1);
-    assertThat(index.search(IssueQuery.builder().actionPlans(newArrayList("plan1", "plan2")).build(), new
-      QueryContext()).getHits()).hasSize(2);
-    assertThat(index.search(IssueQuery.builder().actionPlans(newArrayList("unknown")).build(), new QueryContext()).getHits()).isEmpty();
+    assertThat(index.search(IssueQuery.builder().actionPlans(newArrayList("plan1")).build(), new SearchOptions()).getDocs()).hasSize(1);
+    assertThat(index.search(IssueQuery.builder().actionPlans(newArrayList("plan1", "plan2")).build(), new SearchOptions()).getDocs()).hasSize(2);
+    assertThat(index.search(IssueQuery.builder().actionPlans(newArrayList("unknown")).build(), new SearchOptions()).getDocs()).isEmpty();
   }
 
   @Test
@@ -455,9 +457,9 @@ public class IssueIndexMediumTest {
       IssueTesting.newDoc("ISSUE1", file).setActionPlanKey("plan1"),
       IssueTesting.newDoc("ISSUE2", file).setActionPlanKey("plan2"));
 
-    Result<Issue> result = index.search(IssueQuery.builder().build(), new QueryContext().addFacets(newArrayList("actionPlans")));
-    assertThat(result.getFacets()).containsOnlyKeys("actionPlans");
-    assertThat(result.getFacets().get("actionPlans")).containsOnly(new FacetValue("plan1", 1), new FacetValue("plan2", 1));
+    SearchResult<IssueDoc> result = index.search(IssueQuery.builder().build(), new SearchOptions().addFacets(newArrayList("actionPlans")));
+    assertThat(result.getFacets().getNames()).containsOnly("actionPlans");
+    assertThat(result.getFacets().get("actionPlans")).containsOnly(entry("plan1", 1L), entry("plan2", 1L));
   }
 
   @Test
@@ -470,9 +472,9 @@ public class IssueIndexMediumTest {
       IssueTesting.newDoc("ISSUE2", file).setActionPlanKey(null),
       IssueTesting.newDoc("ISSUE3", file).setActionPlanKey(null));
 
-    assertThat(index.search(IssueQuery.builder().planned(true).build(), new QueryContext()).getHits()).hasSize(1);
-    assertThat(index.search(IssueQuery.builder().planned(false).build(), new QueryContext()).getHits()).hasSize(2);
-    assertThat(index.search(IssueQuery.builder().planned(null).build(), new QueryContext()).getHits()).hasSize(3);
+    assertThat(index.search(IssueQuery.builder().planned(true).build(), new SearchOptions()).getDocs()).hasSize(1);
+    assertThat(index.search(IssueQuery.builder().planned(false).build(), new SearchOptions()).getDocs()).hasSize(2);
+    assertThat(index.search(IssueQuery.builder().planned(null).build(), new SearchOptions()).getDocs()).hasSize(3);
   }
 
   @Test
@@ -483,8 +485,8 @@ public class IssueIndexMediumTest {
 
     indexIssues(IssueTesting.newDoc("ISSUE1", file).setRuleKey(ruleKey.toString()));
 
-    assertThat(index.search(IssueQuery.builder().rules(newArrayList(ruleKey)).build(), new QueryContext()).getHits()).hasSize(1);
-    assertThat(index.search(IssueQuery.builder().rules(newArrayList(RuleKey.of("rule", "without issue"))).build(), new QueryContext()).getHits()).isEmpty();
+    assertThat(index.search(IssueQuery.builder().rules(newArrayList(ruleKey)).build(), new SearchOptions()).getDocs()).hasSize(1);
+    assertThat(index.search(IssueQuery.builder().rules(newArrayList(RuleKey.of("rule", "without issue"))).build(), new SearchOptions()).getDocs()).isEmpty();
   }
 
   @Test
@@ -495,9 +497,9 @@ public class IssueIndexMediumTest {
 
     indexIssues(IssueTesting.newDoc("ISSUE1", file).setRuleKey(ruleKey.toString()).setLanguage("xoo"));
 
-    assertThat(index.search(IssueQuery.builder().languages(newArrayList("xoo")).build(), new
-      QueryContext()).getHits()).hasSize(1);
-    assertThat(index.search(IssueQuery.builder().languages(newArrayList("unknown")).build(), new QueryContext()).getHits()).isEmpty();
+    assertThat(index.search(IssueQuery.builder().languages(newArrayList("xoo")).build(),
+      new SearchOptions()).getDocs()).hasSize(1);
+    assertThat(index.search(IssueQuery.builder().languages(newArrayList("unknown")).build(), new SearchOptions()).getDocs()).isEmpty();
   }
 
   @Test
@@ -508,9 +510,9 @@ public class IssueIndexMediumTest {
 
     indexIssues(IssueTesting.newDoc("ISSUE1", file).setRuleKey(ruleKey.toString()).setLanguage("xoo"));
 
-    Result<Issue> result = index.search(IssueQuery.builder().build(), new QueryContext().addFacets(newArrayList("languages")));
-    assertThat(result.getFacets()).containsOnlyKeys("languages");
-    assertThat(result.getFacets().get("languages")).containsOnly(new FacetValue("xoo", 1));
+    SearchResult<IssueDoc> result = index.search(IssueQuery.builder().build(), new SearchOptions().addFacets(newArrayList("languages")));
+    assertThat(result.getFacets().getNames()).containsOnly("languages");
+    assertThat(result.getFacets().get("languages")).containsOnly(entry("xoo", 1L));
   }
 
   @Test
@@ -523,9 +525,9 @@ public class IssueIndexMediumTest {
       IssueTesting.newDoc("ISSUE2", file).setAssignee("simon"),
       IssueTesting.newDoc("ISSUE3", file).setAssignee(null));
 
-    assertThat(index.search(IssueQuery.builder().assignees(newArrayList("steph")).build(), new QueryContext()).getHits()).hasSize(1);
-    assertThat(index.search(IssueQuery.builder().assignees(newArrayList("steph", "simon")).build(), new QueryContext()).getHits()).hasSize(2);
-    assertThat(index.search(IssueQuery.builder().assignees(newArrayList("unknown")).build(), new QueryContext()).getHits()).isEmpty();
+    assertThat(index.search(IssueQuery.builder().assignees(newArrayList("steph")).build(), new SearchOptions()).getDocs()).hasSize(1);
+    assertThat(index.search(IssueQuery.builder().assignees(newArrayList("steph", "simon")).build(), new SearchOptions()).getDocs()).hasSize(2);
+    assertThat(index.search(IssueQuery.builder().assignees(newArrayList("unknown")).build(), new SearchOptions()).getDocs()).isEmpty();
   }
 
   @Test
@@ -539,9 +541,9 @@ public class IssueIndexMediumTest {
       IssueTesting.newDoc("ISSUE3", file).setAssignee("simon"),
       IssueTesting.newDoc("ISSUE4", file).setAssignee(null));
 
-    Result<Issue> result = index.search(IssueQuery.builder().build(), new QueryContext().addFacets(newArrayList("assignees")));
-    assertThat(result.getFacets()).containsOnlyKeys("assignees");
-    assertThat(result.getFacets().get("assignees")).containsOnly(new FacetValue("steph", 1), new FacetValue("simon", 2), new FacetValue("", 1));
+    SearchResult<IssueDoc> result = index.search(IssueQuery.builder().build(), new SearchOptions().addFacets(newArrayList("assignees")));
+    assertThat(result.getFacets().getNames()).containsOnly("assignees");
+    assertThat(result.getFacets().get("assignees")).containsOnly(entry("steph", 1L), entry("simon", 2L), entry("", 1L));
   }
 
   @Test
@@ -554,9 +556,9 @@ public class IssueIndexMediumTest {
       IssueTesting.newDoc("ISSUE2", file).setAssignee(null),
       IssueTesting.newDoc("ISSUE3", file).setAssignee(null));
 
-    assertThat(index.search(IssueQuery.builder().assigned(true).build(), new QueryContext()).getHits()).hasSize(1);
-    assertThat(index.search(IssueQuery.builder().assigned(false).build(), new QueryContext()).getHits()).hasSize(2);
-    assertThat(index.search(IssueQuery.builder().assigned(null).build(), new QueryContext()).getHits()).hasSize(3);
+    assertThat(index.search(IssueQuery.builder().assigned(true).build(), new SearchOptions()).getDocs()).hasSize(1);
+    assertThat(index.search(IssueQuery.builder().assigned(false).build(), new SearchOptions()).getDocs()).hasSize(2);
+    assertThat(index.search(IssueQuery.builder().assigned(null).build(), new SearchOptions()).getDocs()).hasSize(3);
   }
 
   @Test
@@ -568,9 +570,9 @@ public class IssueIndexMediumTest {
       IssueTesting.newDoc("ISSUE1", file).setReporter("fabrice"),
       IssueTesting.newDoc("ISSUE2", file).setReporter("stephane"));
 
-    assertThat(index.search(IssueQuery.builder().reporters(newArrayList("fabrice", "stephane")).build(), new QueryContext()).getHits()).hasSize(2);
-    assertThat(index.search(IssueQuery.builder().reporters(newArrayList("fabrice")).build(), new QueryContext()).getHits()).hasSize(1);
-    assertThat(index.search(IssueQuery.builder().reporters(newArrayList("unknown")).build(), new QueryContext()).getHits()).isEmpty();
+    assertThat(index.search(IssueQuery.builder().reporters(newArrayList("fabrice", "stephane")).build(), new SearchOptions()).getDocs()).hasSize(2);
+    assertThat(index.search(IssueQuery.builder().reporters(newArrayList("fabrice")).build(), new SearchOptions()).getDocs()).hasSize(1);
+    assertThat(index.search(IssueQuery.builder().reporters(newArrayList("unknown")).build(), new SearchOptions()).getDocs()).isEmpty();
   }
 
   @Test
@@ -583,9 +585,9 @@ public class IssueIndexMediumTest {
       IssueTesting.newDoc("ISSUE2", file).setAuthorLogin("simon"),
       IssueTesting.newDoc("ISSUE3", file).setAssignee(null));
 
-    assertThat(index.search(IssueQuery.builder().authors(newArrayList("steph")).build(), new QueryContext()).getHits()).hasSize(1);
-    assertThat(index.search(IssueQuery.builder().authors(newArrayList("steph", "simon")).build(), new QueryContext()).getHits()).hasSize(2);
-    assertThat(index.search(IssueQuery.builder().authors(newArrayList("unknown")).build(), new QueryContext()).getHits()).isEmpty();
+    assertThat(index.search(IssueQuery.builder().authors(newArrayList("steph")).build(), new SearchOptions()).getDocs()).hasSize(1);
+    assertThat(index.search(IssueQuery.builder().authors(newArrayList("steph", "simon")).build(), new SearchOptions()).getDocs()).hasSize(2);
+    assertThat(index.search(IssueQuery.builder().authors(newArrayList("unknown")).build(), new SearchOptions()).getDocs()).isEmpty();
   }
 
   @Test
@@ -599,9 +601,9 @@ public class IssueIndexMediumTest {
       IssueTesting.newDoc("ISSUE3", file).setAuthorLogin("simon"),
       IssueTesting.newDoc("ISSUE4", file).setAuthorLogin(null));
 
-    Result<Issue> result = index.search(IssueQuery.builder().build(), new QueryContext().addFacets(newArrayList("authors")));
-    assertThat(result.getFacets()).containsOnlyKeys("authors");
-    assertThat(result.getFacets().get("authors")).containsOnly(new FacetValue("steph", 1), new FacetValue("simon", 2));
+    SearchResult<IssueDoc> result = index.search(IssueQuery.builder().build(), new SearchOptions().addFacets(newArrayList("authors")));
+    assertThat(result.getFacets().getNames()).containsOnly("authors");
+    assertThat(result.getFacets().get("authors")).containsOnly(entry("steph", 1L), entry("simon", 2L));
   }
 
   @Test
@@ -613,10 +615,10 @@ public class IssueIndexMediumTest {
       IssueTesting.newDoc("ISSUE1", file).setFuncCreationDate(DateUtils.parseDate("2014-09-20")),
       IssueTesting.newDoc("ISSUE2", file).setFuncCreationDate(DateUtils.parseDate("2014-09-23")));
 
-    assertThat(index.search(IssueQuery.builder().createdAfter(DateUtils.parseDate("2014-09-19")).build(), new QueryContext()).getHits()).hasSize(2);
-    assertThat(index.search(IssueQuery.builder().createdAfter(DateUtils.parseDate("2014-09-20")).build(), new QueryContext()).getHits()).hasSize(2);
-    assertThat(index.search(IssueQuery.builder().createdAfter(DateUtils.parseDate("2014-09-21")).build(), new QueryContext()).getHits()).hasSize(1);
-    assertThat(index.search(IssueQuery.builder().createdAfter(DateUtils.parseDate("2014-09-25")).build(), new QueryContext()).getHits()).isEmpty();
+    assertThat(index.search(IssueQuery.builder().createdAfter(DateUtils.parseDate("2014-09-19")).build(), new SearchOptions()).getDocs()).hasSize(2);
+    assertThat(index.search(IssueQuery.builder().createdAfter(DateUtils.parseDate("2014-09-20")).build(), new SearchOptions()).getDocs()).hasSize(2);
+    assertThat(index.search(IssueQuery.builder().createdAfter(DateUtils.parseDate("2014-09-21")).build(), new SearchOptions()).getDocs()).hasSize(1);
+    assertThat(index.search(IssueQuery.builder().createdAfter(DateUtils.parseDate("2014-09-25")).build(), new SearchOptions()).getDocs()).isEmpty();
   }
 
   @Test
@@ -628,10 +630,10 @@ public class IssueIndexMediumTest {
       IssueTesting.newDoc("ISSUE1", file).setFuncCreationDate(DateUtils.parseDate("2014-09-20")),
       IssueTesting.newDoc("ISSUE2", file).setFuncCreationDate(DateUtils.parseDate("2014-09-23")));
 
-    assertThat(index.search(IssueQuery.builder().createdBefore(DateUtils.parseDate("2014-09-19")).build(), new QueryContext()).getHits()).isEmpty();
-    assertThat(index.search(IssueQuery.builder().createdBefore(DateUtils.parseDate("2014-09-20")).build(), new QueryContext()).getHits()).hasSize(1);
-    assertThat(index.search(IssueQuery.builder().createdBefore(DateUtils.parseDate("2014-09-21")).build(), new QueryContext()).getHits()).hasSize(1);
-    assertThat(index.search(IssueQuery.builder().createdBefore(DateUtils.parseDate("2014-09-25")).build(), new QueryContext()).getHits()).hasSize(2);
+    assertThat(index.search(IssueQuery.builder().createdBefore(DateUtils.parseDate("2014-09-19")).build(), new SearchOptions()).getDocs()).isEmpty();
+    assertThat(index.search(IssueQuery.builder().createdBefore(DateUtils.parseDate("2014-09-20")).build(), new SearchOptions()).getDocs()).hasSize(1);
+    assertThat(index.search(IssueQuery.builder().createdBefore(DateUtils.parseDate("2014-09-21")).build(), new SearchOptions()).getDocs()).hasSize(1);
+    assertThat(index.search(IssueQuery.builder().createdBefore(DateUtils.parseDate("2014-09-25")).build(), new SearchOptions()).getDocs()).hasSize(2);
   }
 
   @Test
@@ -641,121 +643,114 @@ public class IssueIndexMediumTest {
 
     indexIssues(IssueTesting.newDoc("ISSUE1", file).setFuncCreationDate(DateUtils.parseDate("2014-09-20")));
 
-    assertThat(index.search(IssueQuery.builder().createdAt(DateUtils.parseDate("2014-09-20")).build(), new QueryContext()).getHits()).hasSize(1);
-    assertThat(index.search(IssueQuery.builder().createdAt(DateUtils.parseDate("2014-09-21")).build(), new QueryContext()).getHits()).isEmpty();
+    assertThat(index.search(IssueQuery.builder().createdAt(DateUtils.parseDate("2014-09-20")).build(), new SearchOptions()).getDocs()).hasSize(1);
+    assertThat(index.search(IssueQuery.builder().createdAt(DateUtils.parseDate("2014-09-21")).build(), new SearchOptions()).getDocs()).isEmpty();
   }
 
   @Test
   public void facet_on_created_at_with_less_than_20_days() throws Exception {
 
-    QueryContext queryContext = fixtureForCreatedAtFacet();
-
-    Collection<FacetValue> createdAt = index.search(IssueQuery.builder().createdAfter(DateUtils.parseDate("2014-09-01")).createdBefore(DateUtils.parseDate("2014-09-08")).build(),
-      queryContext).getFacets().get("createdAt");
-    assertThat(createdAt).hasSize(8)
-      .containsOnly(
-        new FacetValue("2014-08-31T01:00:00+0000", 0),
-        new FacetValue("2014-09-01T01:00:00+0000", 2),
-        new FacetValue("2014-09-02T01:00:00+0000", 1),
-        new FacetValue("2014-09-03T01:00:00+0000", 0),
-        new FacetValue("2014-09-04T01:00:00+0000", 0),
-        new FacetValue("2014-09-05T01:00:00+0000", 1),
-        new FacetValue("2014-09-06T01:00:00+0000", 0),
-        new FacetValue("2014-09-07T01:00:00+0000", 0));
+    SearchOptions options = fixtureForCreatedAtFacet();
+
+    IssueQuery query = IssueQuery.builder()
+      .createdAfter(DateUtils.parseDate("2014-09-01"))
+      .createdBefore(DateUtils.parseDate("2014-09-08"))
+      .checkAuthorization(false)
+      .build();
+    SearchResult<IssueDoc> result = index.search(query, options);
+    Map<String, Long> buckets = result.getFacets().get("createdAt");
+    assertThat(buckets).containsOnly(
+      entry("2014-08-31T01:00:00+0000", 0L),
+      entry("2014-09-01T01:00:00+0000", 2L),
+      entry("2014-09-02T01:00:00+0000", 1L),
+      entry("2014-09-03T01:00:00+0000", 0L),
+      entry("2014-09-04T01:00:00+0000", 0L),
+      entry("2014-09-05T01:00:00+0000", 1L),
+      entry("2014-09-06T01:00:00+0000", 0L),
+      entry("2014-09-07T01:00:00+0000", 0L));
   }
 
   @Test
   public void facet_on_created_at_with_less_than_20_weeks() throws Exception {
 
-    QueryContext queryContext = fixtureForCreatedAtFacet();
+    SearchOptions SearchOptions = fixtureForCreatedAtFacet();
 
-    Collection<FacetValue> createdAt = index.search(IssueQuery.builder().createdAfter(DateUtils.parseDate("2014-09-01")).createdBefore(DateUtils.parseDate("2014-09-21")).build(),
-      queryContext).getFacets().get("createdAt");
-    assertThat(createdAt).hasSize(4)
-      .containsOnly(
-        new FacetValue("2014-08-25T01:00:00+0000", 0),
-        new FacetValue("2014-09-01T01:00:00+0000", 4),
-        new FacetValue("2014-09-08T01:00:00+0000", 0),
-        new FacetValue("2014-09-15T01:00:00+0000", 1));
+    Map<String, Long> createdAt = index.search(IssueQuery.builder().createdAfter(DateUtils.parseDate("2014-09-01")).createdBefore(DateUtils.parseDate("2014-09-21")).build(),
+      SearchOptions).getFacets().get("createdAt");
+    assertThat(createdAt).containsOnly(
+      entry("2014-08-25T01:00:00+0000", 0L),
+      entry("2014-09-01T01:00:00+0000", 4L),
+      entry("2014-09-08T01:00:00+0000", 0L),
+      entry("2014-09-15T01:00:00+0000", 1L));
   }
 
   @Test
   public void facet_on_created_at_with_less_than_20_months() throws Exception {
 
-    QueryContext queryContext = fixtureForCreatedAtFacet();
-
-    Collection<FacetValue> createdAt = index.search(IssueQuery.builder().createdAfter(DateUtils.parseDate("2014-09-01")).createdBefore(DateUtils.parseDate("2015-01-19")).build(),
-      queryContext).getFacets().get("createdAt");
-    assertThat(createdAt).hasSize(6)
-      .containsOnly(
-        new FacetValue("2014-08-01T01:00:00+0000", 0),
-        new FacetValue("2014-09-01T01:00:00+0000", 5),
-        new FacetValue("2014-10-01T01:00:00+0000", 0),
-        new FacetValue("2014-11-01T01:00:00+0000", 0),
-        new FacetValue("2014-12-01T01:00:00+0000", 0),
-        new FacetValue("2015-01-01T01:00:00+0000", 1));
+    SearchOptions SearchOptions = fixtureForCreatedAtFacet();
+
+    Map<String, Long> createdAt = index.search(IssueQuery.builder().createdAfter(DateUtils.parseDate("2014-09-01")).createdBefore(DateUtils.parseDate("2015-01-19")).build(),
+      SearchOptions).getFacets().get("createdAt");
+    assertThat(createdAt).containsOnly(
+      entry("2014-08-01T01:00:00+0000", 0L),
+      entry("2014-09-01T01:00:00+0000", 5L),
+      entry("2014-10-01T01:00:00+0000", 0L),
+      entry("2014-11-01T01:00:00+0000", 0L),
+      entry("2014-12-01T01:00:00+0000", 0L),
+      entry("2015-01-01T01:00:00+0000", 1L));
   }
 
   @Test
   public void facet_on_created_at_with_more_than_20_months() throws Exception {
-
-    QueryContext queryContext = fixtureForCreatedAtFacet();
-
-    Collection<FacetValue> createdAt = index.search(IssueQuery.builder().createdAfter(DateUtils.parseDate("2011-01-01")).createdBefore(DateUtils.parseDate("2016-01-01")).build(),
-      queryContext).getFacets().get("createdAt");
-    assertThat(createdAt).hasSize(6)
-      .containsOnly(
-        new FacetValue("2011-01-01T01:00:00+0000", 1),
-        new FacetValue("2012-01-01T01:00:00+0000", 0),
-        new FacetValue("2013-01-01T01:00:00+0000", 0),
-        new FacetValue("2014-01-01T01:00:00+0000", 5),
-        new FacetValue("2015-01-01T01:00:00+0000", 1),
-        new FacetValue("2016-01-01T01:00:00+0000", 0));
+    SearchOptions SearchOptions = fixtureForCreatedAtFacet();
+
+    Map<String, Long> createdAt = index.search(IssueQuery.builder().createdAfter(DateUtils.parseDate("2011-01-01")).createdBefore(DateUtils.parseDate("2016-01-01")).build(),
+      SearchOptions).getFacets().get("createdAt");
+    assertThat(createdAt).containsOnly(
+      entry("2011-01-01T01:00:00+0000", 1L),
+      entry("2012-01-01T01:00:00+0000", 0L),
+      entry("2013-01-01T01:00:00+0000", 0L),
+      entry("2014-01-01T01:00:00+0000", 5L),
+      entry("2015-01-01T01:00:00+0000", 1L),
+      entry("2016-01-01T01:00:00+0000", 0L));
 
   }
 
   @Test
   public void facet_on_created_at_with_bounds_outside_of_data() throws Exception {
-
-    QueryContext queryContext = fixtureForCreatedAtFacet();
-
-    Collection<FacetValue> createdAt = index.search(IssueQuery.builder()
-        .createdAfter(DateUtils.parseDate("2009-01-01"))
-        .createdBefore(DateUtils.parseDate("2016-01-01"))
-        .build(),
-      queryContext).getFacets().get("createdAt");
-    assertThat(createdAt).hasSize(8)
-      .containsOnly(
-        new FacetValue("2009-01-01T01:00:00+0000", 0),
-        new FacetValue("2010-01-01T01:00:00+0000", 0),
-        new FacetValue("2011-01-01T01:00:00+0000", 1),
-        new FacetValue("2012-01-01T01:00:00+0000", 0),
-        new FacetValue("2013-01-01T01:00:00+0000", 0),
-        new FacetValue("2014-01-01T01:00:00+0000", 5),
-        new FacetValue("2015-01-01T01:00:00+0000", 1),
-        new FacetValue("2016-01-01T01:00:00+0000", 0));
-
+    SearchOptions options = fixtureForCreatedAtFacet();
+
+    Map<String, Long> createdAt = index.search(IssueQuery.builder()
+      .createdAfter(DateUtils.parseDate("2009-01-01"))
+      .createdBefore(DateUtils.parseDate("2016-01-01"))
+      .build(), options).getFacets().get("createdAt");
+    assertThat(createdAt).containsOnly(
+      entry("2009-01-01T01:00:00+0000", 0L),
+      entry("2010-01-01T01:00:00+0000", 0L),
+      entry("2011-01-01T01:00:00+0000", 1L),
+      entry("2012-01-01T01:00:00+0000", 0L),
+      entry("2013-01-01T01:00:00+0000", 0L),
+      entry("2014-01-01T01:00:00+0000", 5L),
+      entry("2015-01-01T01:00:00+0000", 1L),
+      entry("2016-01-01T01:00:00+0000", 0L));
   }
 
   @Test
   public void facet_on_created_at_without_start_bound() throws Exception {
-
-    QueryContext queryContext = fixtureForCreatedAtFacet();
-
-    Collection<FacetValue> createdAt = index.search(IssueQuery.builder()
-      .createdBefore(DateUtils.parseDate("2016-01-01")).build(),
-      queryContext).getFacets().get("createdAt");
-    assertThat(createdAt).hasSize(6)
-      .containsOnly(
-        new FacetValue("2011-01-01T01:00:00+0000", 1),
-        new FacetValue("2012-01-01T01:00:00+0000", 0),
-        new FacetValue("2013-01-01T01:00:00+0000", 0),
-        new FacetValue("2014-01-01T01:00:00+0000", 5),
-        new FacetValue("2015-01-01T01:00:00+0000", 1),
-        new FacetValue("2016-01-01T01:00:00+0000", 0));
+    SearchOptions SearchOptions = fixtureForCreatedAtFacet();
+
+    Map<String, Long> createdAt = index.search(IssueQuery.builder().createdBefore(DateUtils.parseDate("2016-01-01")).build(),
+      SearchOptions).getFacets().get("createdAt");
+    assertThat(createdAt).containsOnly(
+      entry("2011-01-01T01:00:00+0000", 1L),
+      entry("2012-01-01T01:00:00+0000", 0L),
+      entry("2013-01-01T01:00:00+0000", 0L),
+      entry("2014-01-01T01:00:00+0000", 5L),
+      entry("2015-01-01T01:00:00+0000", 1L),
+      entry("2016-01-01T01:00:00+0000", 0L));
   }
 
-  protected QueryContext fixtureForCreatedAtFacet() {
+  protected SearchOptions fixtureForCreatedAtFacet() {
     ComponentDto project = ComponentTesting.newProjectDto();
     ComponentDto file = ComponentTesting.newFileDto(project);
 
@@ -769,8 +764,7 @@ public class IssueIndexMediumTest {
 
     indexIssues(issue0, issue1, issue2, issue3, issue4, issue5, issue6);
 
-    QueryContext queryContext = new QueryContext().addFacets(Arrays.asList("createdAt"));
-    return queryContext;
+    return new SearchOptions().addFacets("createdAt");
   }
 
   @Test
@@ -783,16 +777,16 @@ public class IssueIndexMediumTest {
 
     IssueQuery.Builder query = IssueQuery.builder();
     // There are 12 issues in total, with 10 issues per page, the page 2 should only contain 2 elements
-    Result<Issue> result = index.search(query.build(), new QueryContext().setPage(2, 10));
-    assertThat(result.getHits()).hasSize(2);
+    SearchResult<IssueDoc> result = index.search(query.build(), new SearchOptions().setPage(2, 10));
+    assertThat(result.getDocs()).hasSize(2);
     assertThat(result.getTotal()).isEqualTo(12);
 
-    result = index.search(IssueQuery.builder().build(), new QueryContext().setOffset(0).setLimit(5));
-    assertThat(result.getHits()).hasSize(5);
+    result = index.search(IssueQuery.builder().build(), new SearchOptions().setOffset(0).setLimit(5));
+    assertThat(result.getDocs()).hasSize(5);
     assertThat(result.getTotal()).isEqualTo(12);
 
-    result = index.search(IssueQuery.builder().build(), new QueryContext().setOffset(2).setLimit(0));
-    assertThat(result.getHits()).hasSize(0);
+    result = index.search(IssueQuery.builder().build(), new SearchOptions().setOffset(2).setLimit(0));
+    assertThat(result.getDocs()).hasSize(0);
     assertThat(result.getTotal()).isEqualTo(12);
   }
 
@@ -808,8 +802,8 @@ public class IssueIndexMediumTest {
     indexIssues(issues.toArray(new IssueDoc[] {}));
 
     IssueQuery.Builder query = IssueQuery.builder();
-    Result<Issue> result = index.search(query.build(), new QueryContext().setMaxLimit());
-    assertThat(result.getHits()).hasSize(500);
+    SearchResult<IssueDoc> result = index.search(query.build(), new SearchOptions().setLimit(Integer.MAX_VALUE));
+    assertThat(result.getDocs()).hasSize(SearchOptions.MAX_LIMIT);
   }
 
   @Test
@@ -823,16 +817,16 @@ public class IssueIndexMediumTest {
       IssueTesting.newDoc("ISSUE3", file).setStatus(Issue.STATUS_REOPENED));
 
     IssueQuery.Builder query = IssueQuery.builder().sort(IssueQuery.SORT_BY_STATUS).asc(true);
-    Result<Issue> result = index.search(query.build(), new QueryContext());
-    assertThat(result.getHits().get(0).status()).isEqualTo(Issue.STATUS_CLOSED);
-    assertThat(result.getHits().get(1).status()).isEqualTo(Issue.STATUS_OPEN);
-    assertThat(result.getHits().get(2).status()).isEqualTo(Issue.STATUS_REOPENED);
+    SearchResult<IssueDoc> result = index.search(query.build(), new SearchOptions());
+    assertThat(result.getDocs().get(0).status()).isEqualTo(Issue.STATUS_CLOSED);
+    assertThat(result.getDocs().get(1).status()).isEqualTo(Issue.STATUS_OPEN);
+    assertThat(result.getDocs().get(2).status()).isEqualTo(Issue.STATUS_REOPENED);
 
     query = IssueQuery.builder().sort(IssueQuery.SORT_BY_STATUS).asc(false);
-    result = index.search(query.build(), new QueryContext());
-    assertThat(result.getHits().get(0).status()).isEqualTo(Issue.STATUS_REOPENED);
-    assertThat(result.getHits().get(1).status()).isEqualTo(Issue.STATUS_OPEN);
-    assertThat(result.getHits().get(2).status()).isEqualTo(Issue.STATUS_CLOSED);
+    result = index.search(query.build(), new SearchOptions());
+    assertThat(result.getDocs().get(0).status()).isEqualTo(Issue.STATUS_REOPENED);
+    assertThat(result.getDocs().get(1).status()).isEqualTo(Issue.STATUS_OPEN);
+    assertThat(result.getDocs().get(2).status()).isEqualTo(Issue.STATUS_CLOSED);
   }
 
   @Test
@@ -848,20 +842,20 @@ public class IssueIndexMediumTest {
       IssueTesting.newDoc("ISSUE5", file).setSeverity(Severity.MAJOR));
 
     IssueQuery.Builder query = IssueQuery.builder().sort(IssueQuery.SORT_BY_SEVERITY).asc(true);
-    Result<Issue> result = index.search(query.build(), new QueryContext());
-    assertThat(result.getHits().get(0).severity()).isEqualTo(Severity.INFO);
-    assertThat(result.getHits().get(1).severity()).isEqualTo(Severity.MINOR);
-    assertThat(result.getHits().get(2).severity()).isEqualTo(Severity.MAJOR);
-    assertThat(result.getHits().get(3).severity()).isEqualTo(Severity.CRITICAL);
-    assertThat(result.getHits().get(4).severity()).isEqualTo(Severity.BLOCKER);
+    SearchResult<IssueDoc> result = index.search(query.build(), new SearchOptions());
+    assertThat(result.getDocs().get(0).severity()).isEqualTo(Severity.INFO);
+    assertThat(result.getDocs().get(1).severity()).isEqualTo(Severity.MINOR);
+    assertThat(result.getDocs().get(2).severity()).isEqualTo(Severity.MAJOR);
+    assertThat(result.getDocs().get(3).severity()).isEqualTo(Severity.CRITICAL);
+    assertThat(result.getDocs().get(4).severity()).isEqualTo(Severity.BLOCKER);
 
     query = IssueQuery.builder().sort(IssueQuery.SORT_BY_SEVERITY).asc(false);
-    result = index.search(query.build(), new QueryContext());
-    assertThat(result.getHits().get(0).severity()).isEqualTo(Severity.BLOCKER);
-    assertThat(result.getHits().get(1).severity()).isEqualTo(Severity.CRITICAL);
-    assertThat(result.getHits().get(2).severity()).isEqualTo(Severity.MAJOR);
-    assertThat(result.getHits().get(3).severity()).isEqualTo(Severity.MINOR);
-    assertThat(result.getHits().get(4).severity()).isEqualTo(Severity.INFO);
+    result = index.search(query.build(), new SearchOptions());
+    assertThat(result.getDocs().get(0).severity()).isEqualTo(Severity.BLOCKER);
+    assertThat(result.getDocs().get(1).severity()).isEqualTo(Severity.CRITICAL);
+    assertThat(result.getDocs().get(2).severity()).isEqualTo(Severity.MAJOR);
+    assertThat(result.getDocs().get(3).severity()).isEqualTo(Severity.MINOR);
+    assertThat(result.getDocs().get(4).severity()).isEqualTo(Severity.INFO);
   }
 
   @Test
@@ -874,16 +868,16 @@ public class IssueIndexMediumTest {
       IssueTesting.newDoc("ISSUE2", file).setAssignee("simon"));
 
     IssueQuery.Builder query = IssueQuery.builder().sort(IssueQuery.SORT_BY_ASSIGNEE).asc(true);
-    Result<Issue> result = index.search(query.build(), new QueryContext());
-    assertThat(result.getHits()).hasSize(2);
-    assertThat(result.getHits().get(0).assignee()).isEqualTo("simon");
-    assertThat(result.getHits().get(1).assignee()).isEqualTo("steph");
+    SearchResult<IssueDoc> result = index.search(query.build(), new SearchOptions());
+    assertThat(result.getDocs()).hasSize(2);
+    assertThat(result.getDocs().get(0).assignee()).isEqualTo("simon");
+    assertThat(result.getDocs().get(1).assignee()).isEqualTo("steph");
 
     query = IssueQuery.builder().sort(IssueQuery.SORT_BY_ASSIGNEE).asc(false);
-    result = index.search(query.build(), new QueryContext());
-    assertThat(result.getHits()).hasSize(2);
-    assertThat(result.getHits().get(0).assignee()).isEqualTo("steph");
-    assertThat(result.getHits().get(1).assignee()).isEqualTo("simon");
+    result = index.search(query.build(), new SearchOptions());
+    assertThat(result.getDocs()).hasSize(2);
+    assertThat(result.getDocs().get(0).assignee()).isEqualTo("steph");
+    assertThat(result.getDocs().get(1).assignee()).isEqualTo("simon");
   }
 
   @Test
@@ -896,16 +890,16 @@ public class IssueIndexMediumTest {
       IssueTesting.newDoc("ISSUE2", file).setFuncCreationDate(DateUtils.parseDate("2014-09-24")));
 
     IssueQuery.Builder query = IssueQuery.builder().sort(IssueQuery.SORT_BY_CREATION_DATE).asc(true);
-    Result<Issue> result = index.search(query.build(), new QueryContext());
-    assertThat(result.getHits()).hasSize(2);
-    assertThat(result.getHits().get(0).creationDate()).isEqualTo(DateUtils.parseDate("2014-09-23"));
-    assertThat(result.getHits().get(1).creationDate()).isEqualTo(DateUtils.parseDate("2014-09-24"));
+    SearchResult<IssueDoc> result = index.search(query.build(), new SearchOptions());
+    assertThat(result.getDocs()).hasSize(2);
+    assertThat(result.getDocs().get(0).creationDate()).isEqualTo(DateUtils.parseDate("2014-09-23"));
+    assertThat(result.getDocs().get(1).creationDate()).isEqualTo(DateUtils.parseDate("2014-09-24"));
 
     query = IssueQuery.builder().sort(IssueQuery.SORT_BY_CREATION_DATE).asc(false);
-    result = index.search(query.build(), new QueryContext());
-    assertThat(result.getHits()).hasSize(2);
-    assertThat(result.getHits().get(0).creationDate()).isEqualTo(DateUtils.parseDate("2014-09-24"));
-    assertThat(result.getHits().get(1).creationDate()).isEqualTo(DateUtils.parseDate("2014-09-23"));
+    result = index.search(query.build(), new SearchOptions());
+    assertThat(result.getDocs()).hasSize(2);
+    assertThat(result.getDocs().get(0).creationDate()).isEqualTo(DateUtils.parseDate("2014-09-24"));
+    assertThat(result.getDocs().get(1).creationDate()).isEqualTo(DateUtils.parseDate("2014-09-23"));
   }
 
   @Test
@@ -918,16 +912,16 @@ public class IssueIndexMediumTest {
       IssueTesting.newDoc("ISSUE2", file).setFuncUpdateDate(DateUtils.parseDate("2014-09-24")));
 
     IssueQuery.Builder query = IssueQuery.builder().sort(IssueQuery.SORT_BY_UPDATE_DATE).asc(true);
-    Result<Issue> result = index.search(query.build(), new QueryContext());
-    assertThat(result.getHits()).hasSize(2);
-    assertThat(result.getHits().get(0).updateDate()).isEqualTo(DateUtils.parseDate("2014-09-23"));
-    assertThat(result.getHits().get(1).updateDate()).isEqualTo(DateUtils.parseDate("2014-09-24"));
+    SearchResult<IssueDoc> result = index.search(query.build(), new SearchOptions());
+    assertThat(result.getDocs()).hasSize(2);
+    assertThat(result.getDocs().get(0).updateDate()).isEqualTo(DateUtils.parseDate("2014-09-23"));
+    assertThat(result.getDocs().get(1).updateDate()).isEqualTo(DateUtils.parseDate("2014-09-24"));
 
     query = IssueQuery.builder().sort(IssueQuery.SORT_BY_UPDATE_DATE).asc(false);
-    result = index.search(query.build(), new QueryContext());
-    assertThat(result.getHits()).hasSize(2);
-    assertThat(result.getHits().get(0).updateDate()).isEqualTo(DateUtils.parseDate("2014-09-24"));
-    assertThat(result.getHits().get(1).updateDate()).isEqualTo(DateUtils.parseDate("2014-09-23"));
+    result = index.search(query.build(), new SearchOptions());
+    assertThat(result.getDocs()).hasSize(2);
+    assertThat(result.getDocs().get(0).updateDate()).isEqualTo(DateUtils.parseDate("2014-09-24"));
+    assertThat(result.getDocs().get(1).updateDate()).isEqualTo(DateUtils.parseDate("2014-09-23"));
   }
 
   @Test
@@ -941,18 +935,18 @@ public class IssueIndexMediumTest {
       IssueTesting.newDoc("ISSUE3", file).setFuncCloseDate(null));
 
     IssueQuery.Builder query = IssueQuery.builder().sort(IssueQuery.SORT_BY_CLOSE_DATE).asc(true);
-    Result<Issue> result = index.search(query.build(), new QueryContext());
-    assertThat(result.getHits()).hasSize(3);
-    assertThat(result.getHits().get(0).closeDate()).isNull();
-    assertThat(result.getHits().get(1).closeDate()).isEqualTo(DateUtils.parseDate("2014-09-23"));
-    assertThat(result.getHits().get(2).closeDate()).isEqualTo(DateUtils.parseDate("2014-09-24"));
+    SearchResult<IssueDoc> result = index.search(query.build(), new SearchOptions());
+    assertThat(result.getDocs()).hasSize(3);
+    assertThat(result.getDocs().get(0).closeDate()).isNull();
+    assertThat(result.getDocs().get(1).closeDate()).isEqualTo(DateUtils.parseDate("2014-09-23"));
+    assertThat(result.getDocs().get(2).closeDate()).isEqualTo(DateUtils.parseDate("2014-09-24"));
 
     query = IssueQuery.builder().sort(IssueQuery.SORT_BY_CLOSE_DATE).asc(false);
-    result = index.search(query.build(), new QueryContext());
-    assertThat(result.getHits()).hasSize(3);
-    assertThat(result.getHits().get(0).closeDate()).isEqualTo(DateUtils.parseDate("2014-09-24"));
-    assertThat(result.getHits().get(1).closeDate()).isEqualTo(DateUtils.parseDate("2014-09-23"));
-    assertThat(result.getHits().get(2).closeDate()).isNull();
+    result = index.search(query.build(), new SearchOptions());
+    assertThat(result.getDocs()).hasSize(3);
+    assertThat(result.getDocs().get(0).closeDate()).isEqualTo(DateUtils.parseDate("2014-09-24"));
+    assertThat(result.getDocs().get(1).closeDate()).isEqualTo(DateUtils.parseDate("2014-09-23"));
+    assertThat(result.getDocs().get(2).closeDate()).isNull();
   }
 
   @Test
@@ -975,25 +969,25 @@ public class IssueIndexMediumTest {
 
     // ascending sort -> F1 then F2. Line "0" first.
     IssueQuery.Builder query = IssueQuery.builder().sort(IssueQuery.SORT_BY_FILE_LINE).asc(true);
-    Result<Issue> result = index.search(query.build(), new QueryContext());
-    assertThat(result.getHits()).hasSize(6);
-    assertThat(result.getHits().get(0).key()).isEqualTo("F1_1");
-    assertThat(result.getHits().get(1).key()).isEqualTo("F1_2");
-    assertThat(result.getHits().get(2).key()).isEqualTo("F1_3");
-    assertThat(result.getHits().get(3).key()).isEqualTo("F2_1");
-    assertThat(result.getHits().get(4).key()).isEqualTo("F2_2");
-    assertThat(result.getHits().get(5).key()).isEqualTo("F2_3");
+    SearchResult<IssueDoc> result = index.search(query.build(), new SearchOptions());
+    assertThat(result.getDocs()).hasSize(6);
+    assertThat(result.getDocs().get(0).key()).isEqualTo("F1_1");
+    assertThat(result.getDocs().get(1).key()).isEqualTo("F1_2");
+    assertThat(result.getDocs().get(2).key()).isEqualTo("F1_3");
+    assertThat(result.getDocs().get(3).key()).isEqualTo("F2_1");
+    assertThat(result.getDocs().get(4).key()).isEqualTo("F2_2");
+    assertThat(result.getDocs().get(5).key()).isEqualTo("F2_3");
 
     // descending sort -> F2 then F1
     query = IssueQuery.builder().sort(IssueQuery.SORT_BY_FILE_LINE).asc(false);
-    result = index.search(query.build(), new QueryContext());
-    assertThat(result.getHits()).hasSize(6);
-    assertThat(result.getHits().get(0).key()).isEqualTo("F2_3");
-    assertThat(result.getHits().get(1).key()).isEqualTo("F2_2");
-    assertThat(result.getHits().get(2).key()).isEqualTo("F2_1");
-    assertThat(result.getHits().get(3).key()).isEqualTo("F1_3");
-    assertThat(result.getHits().get(4).key()).isEqualTo("F1_2");
-    assertThat(result.getHits().get(5).key()).isEqualTo("F1_1");
+    result = index.search(query.build(), new SearchOptions());
+    assertThat(result.getDocs()).hasSize(6);
+    assertThat(result.getDocs().get(0).key()).isEqualTo("F2_3");
+    assertThat(result.getDocs().get(1).key()).isEqualTo("F2_2");
+    assertThat(result.getDocs().get(2).key()).isEqualTo("F2_1");
+    assertThat(result.getDocs().get(3).key()).isEqualTo("F1_3");
+    assertThat(result.getDocs().get(4).key()).isEqualTo("F1_2");
+    assertThat(result.getDocs().get(5).key()).isEqualTo("F1_1");
   }
 
   @Test
@@ -1013,22 +1007,20 @@ public class IssueIndexMediumTest {
     // project3 can be seen by nobody
     indexIssue(IssueTesting.newDoc("ISSUE3", file3), null, null);
 
-    IssueQuery.Builder query = IssueQuery.builder();
-
     MockUserSession.set().setUserGroups("sonar-users");
-    assertThat(index.search(query.build(), new QueryContext()).getHits()).hasSize(1);
+    assertThat(index.search(IssueQuery.builder().build(), new SearchOptions()).getDocs()).hasSize(1);
 
     MockUserSession.set().setUserGroups("sonar-admins");
-    assertThat(index.search(query.build(), new QueryContext()).getHits()).hasSize(1);
+    assertThat(index.search(IssueQuery.builder().build(), new SearchOptions()).getDocs()).hasSize(1);
 
     MockUserSession.set().setUserGroups("sonar-users", "sonar-admins");
-    assertThat(index.search(query.build(), new QueryContext()).getHits()).hasSize(2);
+    assertThat(index.search(IssueQuery.builder().build(), new SearchOptions()).getDocs()).hasSize(2);
 
     MockUserSession.set().setUserGroups("another group");
-    assertThat(index.search(query.build(), new QueryContext()).getHits()).isEmpty();
+    assertThat(index.search(IssueQuery.builder().build(), new SearchOptions()).getDocs()).isEmpty();
 
     MockUserSession.set().setUserGroups("sonar-users", "sonar-admins");
-    assertThat(index.search(query.projectUuids(newArrayList(project3.uuid())).build(), new QueryContext()).getHits()).isEmpty();
+    assertThat(index.search(IssueQuery.builder().projectUuids(newArrayList(project3.uuid())).build(), new SearchOptions()).getDocs()).isEmpty();
   }
 
   @Test
@@ -1046,19 +1038,18 @@ public class IssueIndexMediumTest {
     indexIssue(IssueTesting.newDoc("ISSUE2", file2), null, "max");
     indexIssue(IssueTesting.newDoc("ISSUE3", file3), null, null);
 
-    IssueQuery.Builder query = IssueQuery.builder();
-
     MockUserSession.set().setLogin("john");
-    assertThat(index.search(query.build(), new QueryContext()).getHits()).hasSize(1);
+
+    assertThat(index.search(IssueQuery.builder().build(), new SearchOptions()).getDocs()).hasSize(1);
 
     MockUserSession.set().setLogin("max");
-    assertThat(index.search(query.build(), new QueryContext()).getHits()).hasSize(1);
+    assertThat(index.search(IssueQuery.builder().build(), new SearchOptions()).getDocs()).hasSize(1);
 
     MockUserSession.set().setLogin("another guy");
-    assertThat(index.search(query.build(), new QueryContext()).getHits()).hasSize(0);
+    assertThat(index.search(IssueQuery.builder().build(), new SearchOptions()).getDocs()).hasSize(0);
 
     MockUserSession.set().setLogin("john");
-    assertThat(index.search(query.projectUuids(newArrayList(project3.key())).build(), new QueryContext()).getHits()).hasSize(0);
+    assertThat(index.search(IssueQuery.builder().projectUuids(newArrayList(project3.key())).build(), new SearchOptions()).getDocs()).hasSize(0);
   }
 
   @Test
@@ -1075,7 +1066,7 @@ public class IssueIndexMediumTest {
 
     IssueQuery.Builder query = IssueQuery.builder();
     MockUserSession.set().setLogin("john").setUserGroups("sonar-users");
-    assertThat(index.search(query.build(), new QueryContext()).getHits()).hasSize(1);
+    assertThat(index.search(query.build(), new SearchOptions()).getDocs()).hasSize(1);
   }
 
   @Test
@@ -1091,17 +1082,22 @@ public class IssueIndexMediumTest {
       // Issue assigned to julien should not be returned as the issue is closed
       IssueTesting.newDoc("ISSUE5", file).setAssignee("julien").setStatus(Issue.STATUS_CLOSED));
 
-    List<FacetValue> results = index.listAssignees(IssueQuery.builder().statuses(newArrayList(Issue.STATUS_OPEN)).build());
-
+    LinkedHashMap<String, Long> results = index.searchForAssignees(IssueQuery.builder().statuses(newArrayList(Issue.STATUS_OPEN)).build());
     assertThat(results).hasSize(3);
-    assertThat(results.get(0).getKey()).isEqualTo("steph");
-    assertThat(results.get(0).getValue()).isEqualTo(2);
 
-    assertThat(results.get(1).getKey()).isEqualTo("simon");
-    assertThat(results.get(1).getValue()).isEqualTo(1);
+    Iterator<Map.Entry<String, Long>> buckets = results.entrySet().iterator();
 
-    assertThat(results.get(2).getKey()).isEqualTo("_notAssigned_");
-    assertThat(results.get(2).getValue()).isEqualTo(1);
+    Map.Entry<String, Long> bucket = buckets.next();
+    assertThat(bucket.getKey()).isEqualTo("steph");
+    assertThat(bucket.getValue()).isEqualTo(2L);
+
+    bucket = buckets.next();
+    assertThat(bucket.getKey()).isEqualTo("simon");
+    assertThat(bucket.getValue()).isEqualTo(1L);
+
+    bucket = buckets.next();
+    assertThat(bucket.getKey()).isEqualTo("_notAssigned_");
+    assertThat(bucket.getValue()).isEqualTo(1L);
   }
 
   @Test
@@ -1123,9 +1119,9 @@ public class IssueIndexMediumTest {
     index.deleteClosedIssuesOfProjectBefore(project.uuid(), yesterday);
 
     // ASSERT
-    List<Issue> issues = index.search(IssueQuery.builder().projectUuids(newArrayList(project.uuid())).build(), new QueryContext()).getHits();
+    List<IssueDoc> issues = index.search(IssueQuery.builder().projectUuids(newArrayList(project.uuid())).build(), new SearchOptions()).getDocs();
     List<Date> dates = newArrayList();
-    for (Issue issue : issues) {
+    for (IssueDoc issue : issues) {
       dates.add(issue.closeDate());
     }
 
index 33acb9a906cf8dbf5f15e3f0d6dabda515092617..c10978bb6ee4362a690512be6b52d5b857986c7d 100644 (file)
@@ -31,7 +31,6 @@ import org.sonar.server.ws.WsTester;
 
 import java.util.Arrays;
 
-import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -47,9 +46,7 @@ public class AuthorsActionTest {
 
   @Before
   public void setUp() throws Exception {
-    tester = new WsTester(new IssuesWs(mock(IssueShowAction.class), new SearchAction(null, null, null, null, null, null, null, null, null, null, null),
-      mock(TagsAction.class), mock(SetTagsAction.class), mock(ComponentTagsAction.class),
-      new AuthorsAction(service)));
+    tester = new WsTester(new IssuesWs(new AuthorsAction(service)));
     controller = tester.controller("api/issues");
   }
 
index 31bb581a73061800da00299f94f27146e9a752ef..5ea8d46672dd3b6f767d2500578c03b80a313f36 100644 (file)
@@ -49,10 +49,7 @@ public class ComponentTagsActionTest {
   @Before
   public void setUp() {
     componentTagsAction = new ComponentTagsAction(service);
-    tester = new WsTester(
-      new IssuesWs(new IssueShowAction(null, null, null, null, null, null, null, null, null, null, null),
-        new SearchAction(null, null, null, null, null, null, null, null, null, null,null),
-        new TagsAction(null), new SetTagsAction(null), componentTagsAction, new AuthorsAction(null)));
+    tester = new WsTester(new IssuesWs(componentTagsAction));
   }
 
   @Test
index 5dd1de61d0bb3ae308ce8252841e8cd8c712fcdf..d730f222275e8fdfe088c3ce160e3164d7789624 100644 (file)
@@ -31,7 +31,6 @@ import org.sonar.api.issue.Issue;
 import org.sonar.api.issue.internal.DefaultIssue;
 import org.sonar.api.issue.internal.DefaultIssueComment;
 import org.sonar.api.issue.internal.FieldDiffs;
-import org.sonar.api.resources.Languages;
 import org.sonar.api.rule.RuleKey;
 import org.sonar.api.server.debt.internal.DefaultDebtCharacteristic;
 import org.sonar.api.user.User;
@@ -41,7 +40,6 @@ import org.sonar.api.utils.Duration;
 import org.sonar.api.utils.Durations;
 import org.sonar.core.component.ComponentDto;
 import org.sonar.core.issue.DefaultActionPlan;
-import org.sonar.core.issue.db.IssueChangeDao;
 import org.sonar.core.issue.workflow.Transition;
 import org.sonar.core.persistence.DbSession;
 import org.sonar.core.user.DefaultUser;
@@ -49,7 +47,11 @@ import org.sonar.server.component.ComponentTesting;
 import org.sonar.server.component.db.ComponentDao;
 import org.sonar.server.db.DbClient;
 import org.sonar.server.debt.DebtModelService;
-import org.sonar.server.issue.*;
+import org.sonar.server.issue.ActionService;
+import org.sonar.server.issue.IssueChangelog;
+import org.sonar.server.issue.IssueChangelogService;
+import org.sonar.server.issue.IssueCommentService;
+import org.sonar.server.issue.IssueService;
 import org.sonar.server.issue.actionplan.ActionPlanService;
 import org.sonar.server.rule.Rule;
 import org.sonar.server.rule.RuleService;
@@ -141,11 +143,7 @@ public class IssueShowActionTest {
 
     tester = new WsTester(new IssuesWs(
       new IssueShowAction(dbClient, issueService, issueChangelogService, commentService,
-        new IssueActionsWriter(issueService, actionService), actionPlanService, userFinder, debtModel, ruleService, i18n, durations),
-      new SearchAction(mock(DbClient.class), mock(IssueChangeDao.class), mock(IssueService.class), mock(IssueActionsWriter.class), mock(IssueQueryService.class),
-        mock(RuleService.class),
-        mock(ActionPlanService.class), mock(UserFinder.class), mock(I18n.class), mock(Durations.class), mock(Languages.class)),
-      new TagsAction(null), new SetTagsAction(null), new ComponentTagsAction(null), new AuthorsAction(null)
+        new IssueActionsWriter(issueService, actionService), actionPlanService, userFinder, debtModel, ruleService, i18n, durations)
       ));
   }
 
index 45f97790bde59e705119ebc110f09152abf6ab65..9519396292be2ee5d09e082d4fd4597cf95979b0 100644 (file)
@@ -47,10 +47,7 @@ public class IssueTagsActionTest {
   @Before
   public void setUp() {
     tagsAction = new TagsAction(service);
-    tester = new WsTester(
-      new IssuesWs(new IssueShowAction(null, null, null, null, null, null, null, null, null, null, null),
-        new SearchAction(null, null, null, null, null, null, null, null, null, null,null),
-        tagsAction, new SetTagsAction(null), new ComponentTagsAction(null), new AuthorsAction(null)));
+    tester = new WsTester(new IssuesWs(tagsAction));
   }
 
   @Test
index f3d0d3e73f9e879ffe78480bbbe0652952be0915..e47f7fac89437da97b0ebd31e5986c7b507090c2 100644 (file)
  */
 package org.sonar.server.issue.ws;
 
-import org.junit.Before;
 import org.junit.Test;
-import org.sonar.api.i18n.I18n;
-import org.sonar.api.resources.Languages;
-import org.sonar.api.server.ws.RailsHandler;
 import org.sonar.api.server.ws.WebService;
-import org.sonar.api.user.UserFinder;
-import org.sonar.api.utils.Durations;
-import org.sonar.core.issue.db.IssueChangeDao;
-import org.sonar.server.db.DbClient;
-import org.sonar.server.debt.DebtModelService;
-import org.sonar.server.issue.IssueChangelogService;
-import org.sonar.server.issue.IssueCommentService;
-import org.sonar.server.issue.IssueQueryService;
-import org.sonar.server.issue.IssueService;
-import org.sonar.server.issue.actionplan.ActionPlanService;
-import org.sonar.server.rule.RuleService;
-import org.sonar.server.ws.WsTester;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.any;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 
 public class IssuesWsTest {
 
-  IssueShowAction showAction;
-
-  WsTester tester;
-
-  @Before
-  public void setUp() throws Exception {
-    IssueChangelogService issueChangelogService = mock(IssueChangelogService.class);
-    IssueActionsWriter actionsWriter = mock(IssueActionsWriter.class);
-    DebtModelService debtModelService = mock(DebtModelService.class);
-    I18n i18n = mock(I18n.class);
-    Durations durations = mock(Durations.class);
-
-    showAction = new IssueShowAction(mock(DbClient.class), mock(IssueService.class), issueChangelogService, mock(IssueCommentService.class), actionsWriter,
-      mock(ActionPlanService.class), mock(UserFinder.class),
-      debtModelService, mock(RuleService.class), i18n, durations);
-    SearchAction searchAction = new SearchAction(mock(DbClient.class), mock(IssueChangeDao.class), mock(IssueService.class), mock(IssueActionsWriter.class),
-      mock(IssueQueryService.class), mock(RuleService.class),
-      mock(ActionPlanService.class), mock(UserFinder.class), mock(I18n.class), mock(Durations.class), mock(Languages.class));
-    tester = new WsTester(new IssuesWs(showAction, searchAction, new TagsAction(null), new SetTagsAction(null), new ComponentTagsAction(null), new AuthorsAction(null)));
-  }
-
   @Test
-  public void define_controller() throws Exception {
-    WebService.Controller controller = tester.controller("api/issues");
+  public void define_actions() throws Exception {
+    BaseIssuesWsAction action1 = mock(BaseIssuesWsAction.class);
+    BaseIssuesWsAction action2 = mock(BaseIssuesWsAction.class);
+    IssuesWs ws = new IssuesWs(action1, action2);
+    WebService.Context context = new WebService.Context();
+    ws.define(context);
+
+    WebService.Controller controller = context.controller("api/issues");
     assertThat(controller).isNotNull();
     assertThat(controller.description()).isNotEmpty();
     assertThat(controller.since()).isEqualTo("3.6");
-    assertThat(controller.actions()).hasSize(18);
-  }
-
-  @Test
-  public void define_show_action() throws Exception {
-    WebService.Controller controller = tester.controller("api/issues");
-
-    WebService.Action action = controller.action("show");
-    assertThat(action).isNotNull();
-    assertThat(action.handler()).isNotNull();
-    assertThat(action.since()).isEqualTo("4.2");
-    assertThat(action.isPost()).isFalse();
-    assertThat(action.isInternal()).isTrue();
-    assertThat(action.handler()).isSameAs(showAction);
-    assertThat(action.responseExampleAsString()).isNotEmpty();
-    assertThat(action.params()).hasSize(1);
-
-    WebService.Param key = action.param("key");
-    assertThat(key).isNotNull();
-    assertThat(key.description()).isNotNull();
-    assertThat(key.isRequired()).isFalse();
-  }
-
-  @Test
-  public void define_changelog_action() throws Exception {
-    WebService.Controller controller = tester.controller("api/issues");
-
-    WebService.Action action = controller.action("changelog");
-    assertThat(action).isNotNull();
-    assertThat(action.handler()).isNotNull();
-    assertThat(action.responseExampleAsString()).isNotEmpty();
-    assertThat(action.params()).hasSize(2);
-  }
-
-  @Test
-  public void define_assign_action() throws Exception {
-    WebService.Controller controller = tester.controller("api/issues");
-
-    WebService.Action action = controller.action("assign");
-    assertThat(action).isNotNull();
-    assertThat(action.handler()).isNotNull();
-    assertThat(action.since()).isEqualTo("3.6");
-    assertThat(action.isPost()).isTrue();
-    assertThat(action.isInternal()).isFalse();
-    assertThat(action.handler()).isInstanceOf(RailsHandler.class);
-    assertThat(action.params()).hasSize(3);
-  }
-
-  @Test
-  public void define_add_comment_action() throws Exception {
-    WebService.Controller controller = tester.controller("api/issues");
-
-    WebService.Action action = controller.action("add_comment");
-    assertThat(action).isNotNull();
-    assertThat(action.handler()).isNotNull();
-    assertThat(action.since()).isEqualTo("3.6");
-    assertThat(action.isPost()).isTrue();
-    assertThat(action.isInternal()).isFalse();
-    assertThat(action.handler()).isInstanceOf(RailsHandler.class);
-    assertThat(action.params()).hasSize(3);
-  }
-
-  @Test
-  public void define_delete_comment_action() throws Exception {
-    WebService.Controller controller = tester.controller("api/issues");
-
-    WebService.Action action = controller.action("delete_comment");
-    assertThat(action).isNotNull();
-    assertThat(action.handler()).isNotNull();
-    assertThat(action.since()).isEqualTo("3.6");
-    assertThat(action.isPost()).isTrue();
-    assertThat(action.isInternal()).isFalse();
-    assertThat(action.handler()).isInstanceOf(RailsHandler.class);
-    assertThat(action.params()).hasSize(1);
-  }
-
-  @Test
-  public void define_edit_comment_action() throws Exception {
-    WebService.Controller controller = tester.controller("api/issues");
-
-    WebService.Action action = controller.action("edit_comment");
-    assertThat(action).isNotNull();
-    assertThat(action.handler()).isNotNull();
-    assertThat(action.since()).isEqualTo("3.6");
-    assertThat(action.isPost()).isTrue();
-    assertThat(action.isInternal()).isFalse();
-    assertThat(action.handler()).isInstanceOf(RailsHandler.class);
-    assertThat(action.params()).hasSize(3);
-  }
-
-  @Test
-  public void define_change_severity_action() throws Exception {
-    WebService.Controller controller = tester.controller("api/issues");
-
-    WebService.Action action = controller.action("set_severity");
-    assertThat(action).isNotNull();
-    assertThat(action.handler()).isNotNull();
-    assertThat(action.since()).isEqualTo("3.6");
-    assertThat(action.isPost()).isTrue();
-    assertThat(action.isInternal()).isFalse();
-    assertThat(action.handler()).isInstanceOf(RailsHandler.class);
-    assertThat(action.params()).hasSize(3);
-  }
-
-  @Test
-  public void define_plan_action() throws Exception {
-    WebService.Controller controller = tester.controller("api/issues");
-
-    WebService.Action action = controller.action("plan");
-    assertThat(action).isNotNull();
-    assertThat(action.handler()).isNotNull();
-    assertThat(action.since()).isEqualTo("3.6");
-    assertThat(action.isPost()).isTrue();
-    assertThat(action.isInternal()).isFalse();
-    assertThat(action.handler()).isInstanceOf(RailsHandler.class);
-    assertThat(action.params()).hasSize(3);
-  }
-
-  @Test
-  public void define_do_transition_action() throws Exception {
-    WebService.Controller controller = tester.controller("api/issues");
-
-    WebService.Action action = controller.action("do_transition");
-    assertThat(action).isNotNull();
-    assertThat(action.handler()).isNotNull();
-    assertThat(action.since()).isEqualTo("3.6");
-    assertThat(action.isPost()).isTrue();
-    assertThat(action.isInternal()).isFalse();
-    assertThat(action.handler()).isInstanceOf(RailsHandler.class);
-    assertThat(action.params()).hasSize(3);
-  }
-
-  @Test
-  public void define_transitions_action() throws Exception {
-    WebService.Controller controller = tester.controller("api/issues");
-
-    WebService.Action action = controller.action("transitions");
-    assertThat(action).isNotNull();
-    assertThat(action.handler()).isNotNull();
-    assertThat(action.since()).isEqualTo("3.6");
-    assertThat(action.isPost()).isFalse();
-    assertThat(action.isInternal()).isFalse();
-    assertThat(action.handler()).isInstanceOf(RailsHandler.class);
-    assertThat(action.responseExampleAsString()).isNotEmpty();
-    assertThat(action.params()).hasSize(1);
-  }
-
-  @Test
-  public void define_create_action() throws Exception {
-    WebService.Controller controller = tester.controller("api/issues");
-
-    WebService.Action action = controller.action("create");
-    assertThat(action).isNotNull();
-    assertThat(action.handler()).isNotNull();
-    assertThat(action.since()).isEqualTo("3.6");
-    assertThat(action.isPost()).isTrue();
-    assertThat(action.isInternal()).isFalse();
-    assertThat(action.params()).hasSize(6);
-  }
-
-  @Test
-  public void define_do_action_action() throws Exception {
-    WebService.Controller controller = tester.controller("api/issues");
-
-    WebService.Action action = controller.action("do_action");
-    assertThat(action).isNotNull();
-    assertThat(action.handler()).isNotNull();
-    assertThat(action.since()).isEqualTo("3.6");
-    assertThat(action.isPost()).isTrue();
-    assertThat(action.isInternal()).isFalse();
-    assertThat(action.params()).hasSize(3);
-  }
-
-  @Test
-  public void define_bulk_change_action() throws Exception {
-    WebService.Controller controller = tester.controller("api/issues");
 
-    WebService.Action action = controller.action("bulk_change");
-    assertThat(action).isNotNull();
-    assertThat(action.handler()).isNotNull();
-    assertThat(action.since()).isEqualTo("3.7");
-    assertThat(action.isPost()).isTrue();
-    assertThat(action.isInternal()).isFalse();
-    assertThat(action.handler()).isInstanceOf(RailsHandler.class);
-    assertThat(action.params()).hasSize(9);
+    assertThat(controller.actions()).isNotEmpty();
+    verify(action1).define(any(WebService.NewController.class));
+    verify(action2).define(any(WebService.NewController.class));
   }
 
 }
index 5a50b56856c0575a268411f81467bf9d4fba599d..324210099d5f80079153ddf82f548f677310bc04 100644 (file)
@@ -27,6 +27,7 @@ import org.junit.Test;
 import org.sonar.api.resources.Qualifiers;
 import org.sonar.api.rule.RuleStatus;
 import org.sonar.api.security.DefaultGroups;
+import org.sonar.api.server.ws.WebService;
 import org.sonar.api.utils.DateUtils;
 import org.sonar.api.web.UserRole;
 import org.sonar.core.component.ComponentDto;
@@ -158,13 +159,13 @@ public class SearchActionComponentsMediumTest {
 
     wsTester.newGetRequest(IssuesWs.API_ENDPOINT, SearchAction.SEARCH_ACTION)
       .setParam(IssueFilterParameters.PROJECT_UUIDS, project1.uuid())
-      .setParam(SearchAction.PARAM_FACETS, "projectUuids")
+      .setParam(WebService.Param.FACETS, "projectUuids")
       .execute()
       .assertJson(this.getClass(), "display_sticky_project_facet.json", false);
 
     wsTester.newGetRequest(IssuesWs.API_ENDPOINT, SearchAction.SEARCH_ACTION)
       .setParam(IssueFilterParameters.COMPONENT_UUIDS, project1.uuid())
-      .setParam(SearchAction.PARAM_FACETS, "projectUuids")
+      .setParam(WebService.Param.FACETS, "projectUuids")
       .execute()
       .assertJson(this.getClass(), "display_non_sticky_project_facet.json", false);
   }
@@ -242,7 +243,7 @@ public class SearchActionComponentsMediumTest {
     wsTester.newGetRequest(IssuesWs.API_ENDPOINT, SearchAction.SEARCH_ACTION)
       .setParam(IssueFilterParameters.COMPONENT_UUIDS, project.uuid())
       .setParam(IssueFilterParameters.FILE_UUIDS, file1.uuid() + "," + file3.uuid())
-      .setParam(SearchAction.PARAM_FACETS, "fileUuids")
+      .setParam(WebService.Param.FACETS, "fileUuids")
       .execute()
       .assertJson(this.getClass(), "display_file_facet.json", false);
   }
@@ -349,7 +350,7 @@ public class SearchActionComponentsMediumTest {
     wsTester.newGetRequest(IssuesWs.API_ENDPOINT, SearchAction.SEARCH_ACTION)
       .setParam(IssueFilterParameters.COMPONENT_UUIDS, project.uuid())
       .setParam(IssueFilterParameters.MODULE_UUIDS, subModule1.uuid() + "," + subModule3.uuid())
-      .setParam(SearchAction.PARAM_FACETS, "moduleUuids")
+      .setParam(WebService.Param.FACETS, "moduleUuids")
       .execute()
       .assertJson(this.getClass(), "display_module_facet.json", false);
   }
@@ -368,7 +369,7 @@ public class SearchActionComponentsMediumTest {
     MockUserSession.set().setLogin("john");
     WsTester.Result result = wsTester.newGetRequest(IssuesWs.API_ENDPOINT, SearchAction.SEARCH_ACTION)
       .setParam("resolved", "false")
-      .setParam(SearchAction.PARAM_FACETS, "directories")
+      .setParam(WebService.Param.FACETS, "directories")
       .execute();
     result.assertJson(this.getClass(), "display_directory_facet.json", false);
   }
@@ -469,7 +470,7 @@ public class SearchActionComponentsMediumTest {
 
     wsTester.newGetRequest(IssuesWs.API_ENDPOINT, SearchAction.SEARCH_ACTION)
       .setParam(IssueFilterParameters.AUTHORS, "leia")
-      .setParam(SearchAction.PARAM_FACETS, "authors")
+      .setParam(WebService.Param.FACETS, "authors")
       .execute()
       .assertJson(this.getClass(), "search_by_authors.json", false);
 
index 8111464b58857642a08e439fef1fb597d75dd186..d1bf870bfdcf01333bc800c2b0602b6ec8096995 100644 (file)
@@ -88,7 +88,7 @@ public class SearchActionMediumTest {
     assertThat(show.isPost()).isFalse();
     assertThat(show.isInternal()).isFalse();
     assertThat(show.responseExampleAsString()).isNotEmpty();
-    assertThat(show.params()).hasSize(39);
+    assertThat(show.params()).hasSize(38);
   }
 
   @Test
@@ -367,7 +367,7 @@ public class SearchActionMediumTest {
     MockUserSession.set().setLogin("john");
     WsTester.Result result = wsTester.newGetRequest(IssuesWs.API_ENDPOINT, SearchAction.SEARCH_ACTION)
       .setParam("resolved", "false")
-      .setParam(SearchAction.PARAM_FACETS, "statuses,severities,resolutions,projectUuids,rules,fileUuids,assignees,languages,actionPlans")
+      .setParam(WebService.Param.FACETS, "statuses,severities,resolutions,projectUuids,rules,fileUuids,assignees,languages,actionPlans")
       .execute();
     result.assertJson(this.getClass(), "display_facets.json", false);
   }
@@ -393,7 +393,7 @@ public class SearchActionMediumTest {
       .setParam("resolved", "false")
       .setParam("severities", "MAJOR,MINOR")
       .setParam("languages", "xoo,polop,palap")
-      .setParam(SearchAction.PARAM_FACETS, "statuses,severities,resolutions,projectUuids,rules,fileUuids,assignees,languages,actionPlans")
+      .setParam(WebService.Param.FACETS, "statuses,severities,resolutions,projectUuids,rules,fileUuids,assignees,languages,actionPlans")
       .execute();
     result.assertJson(this.getClass(), "display_zero_facets.json", false);
   }
@@ -451,8 +451,8 @@ public class SearchActionMediumTest {
     tester.get(IssueIndexer.class).indexAll();
 
     WsTester.TestRequest request = wsTester.newGetRequest(IssuesWs.API_ENDPOINT, SearchAction.SEARCH_ACTION);
-    request.setParam(SearchAction.PARAM_PAGE, "2");
-    request.setParam(SearchAction.PARAM_PAGE_SIZE, "9");
+    request.setParam(WebService.Param.PAGE, "2");
+    request.setParam(WebService.Param.PAGE_SIZE, "9");
 
     WsTester.Result result = request.execute();
     result.assertJson(this.getClass(), "paging.json", false);
@@ -472,8 +472,8 @@ public class SearchActionMediumTest {
     tester.get(IssueIndexer.class).indexAll();
 
     WsTester.TestRequest request = wsTester.newGetRequest(IssuesWs.API_ENDPOINT, SearchAction.SEARCH_ACTION);
-    request.setParam(SearchAction.PARAM_PAGE, "1");
-    request.setParam(SearchAction.PARAM_PAGE_SIZE, "-1");
+    request.setParam(WebService.Param.PAGE, "1");
+    request.setParam(WebService.Param.PAGE_SIZE, "-1");
 
     WsTester.Result result = request.execute();
     result.assertJson(this.getClass(), "paging_with_page_size_to_minus_one.json", false);
index 2dd3a9f2bf34d1d08f37ab13ff045e6bbffeea32..7ccfb597930753da0d61964e9b3d9e6effe3b0e9 100644 (file)
@@ -19,6 +19,7 @@
  */
 package org.sonar.server.issue.ws;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import org.junit.Before;
 import org.junit.Test;
@@ -38,19 +39,14 @@ import static org.mockito.Mockito.when;
 public class SetTagsActionTest {
 
   @Mock
-  private IssueService service;
-
-  private SetTagsAction setTagsAction;
-
-  private WsTester tester;
+  IssueService service;
+  SetTagsAction sut;
+  WsTester tester;
 
   @Before
   public void setUp() {
-    setTagsAction = new SetTagsAction(service);
-    tester = new WsTester(
-      new IssuesWs(new IssueShowAction(null, null, null, null, null, null, null, null, null, null, null),
-        new SearchAction(null, null, null, null, null, null, null, null, null, null,null),
-        new TagsAction(null), setTagsAction, new ComponentTagsAction(null), new AuthorsAction(null)));
+    sut = new SetTagsAction(service);
+    tester = new WsTester(new IssuesWs(sut));
   }
 
   @Test
@@ -60,7 +56,7 @@ public class SetTagsActionTest {
     assertThat(action.responseExampleAsString()).isNull();
     assertThat(action.isPost()).isTrue();
     assertThat(action.isInternal()).isFalse();
-    assertThat(action.handler()).isEqualTo(setTagsAction);
+    assertThat(action.handler()).isEqualTo(sut);
     assertThat(action.params()).hasSize(2);
 
     Param query = action.param("key");
@@ -76,9 +72,9 @@ public class SetTagsActionTest {
 
   @Test
   public void should_set_tags() throws Exception {
-    when(service.setTags("polop", ImmutableSet.of("palap"))).thenReturn(ImmutableSet.of("palap"));
+    when(service.setTags("polop", ImmutableList.of("palap"))).thenReturn(ImmutableSet.of("palap"));
     tester.newPostRequest("api/issues", "set_tags").setParam("key", "polop").setParam("tags", "palap").execute()
       .assertJson("{\"tags\":[\"palap\"]}");
-    verify(service).setTags("polop", ImmutableSet.of("palap"));
+    verify(service).setTags("polop", ImmutableList.of("palap"));
   }
 }
index ff806db5e704cc8e43450b933c7a799d538eae68..3f80f02f69585a168553645c75607c088836de75 100644 (file)
@@ -34,16 +34,18 @@ import org.sonar.core.rule.RuleDto;
 import org.sonar.server.component.ComponentTesting;
 import org.sonar.server.component.db.ComponentDao;
 import org.sonar.server.db.DbClient;
+import org.sonar.server.es.SearchOptions;
+import org.sonar.server.es.SearchResult;
 import org.sonar.server.issue.IssueQuery;
 import org.sonar.server.issue.IssueTesting;
 import org.sonar.server.issue.db.IssueDao;
+import org.sonar.server.issue.index.IssueDoc;
 import org.sonar.server.issue.index.IssueIndex;
 import org.sonar.server.issue.index.IssueIndexer;
 import org.sonar.server.permission.InternalPermissionService;
 import org.sonar.server.permission.PermissionChange;
 import org.sonar.server.rule.RuleTesting;
 import org.sonar.server.rule.db.RuleDao;
-import org.sonar.server.search.QueryContext;
 import org.sonar.server.tester.ServerTester;
 import org.sonar.server.user.MockUserSession;
 
@@ -97,7 +99,9 @@ public class ViewIndexerMediumTest {
     indexer.index(viewUuid);
 
     // Execute issue query on view -> 1 issue on view
-    assertThat(tester.get(IssueIndex.class).search(IssueQuery.builder().viewUuids(newArrayList(viewUuid)).build(), new QueryContext()).getHits()).hasSize(1);
+    SearchResult<IssueDoc> docs = tester.get(IssueIndex.class).search(IssueQuery.builder().viewUuids(newArrayList(viewUuid)).build(),
+      new SearchOptions());
+    assertThat(docs.getDocs()).hasSize(1);
 
     // Add a project to the view and index it again
     ComponentDto project2 = addProjectWithIssue(rule);
@@ -107,7 +111,7 @@ public class ViewIndexerMediumTest {
     indexer.index(viewUuid);
 
     // Execute issue query on view -> issue of project2 are well taken into account : the cache has been cleared
-    assertThat(tester.get(IssueIndex.class).search(IssueQuery.builder().viewUuids(newArrayList(viewUuid)).build(), new QueryContext()).getHits()).hasSize(2);
+    assertThat(tester.get(IssueIndex.class).search(IssueQuery.builder().viewUuids(newArrayList(viewUuid)).build(), new SearchOptions()).getDocs()).hasSize(2);
   }
 
   private ComponentDto addProjectWithIssue(RuleDto rule) {
index 4d91f3e640f8beddd1d36321e1bf1b52cc8fa9e7..787b0028a298e2a084e72705690a0c4dfd1d44ae 100644 (file)
@@ -2,7 +2,6 @@
   "total": 0,
   "p": 1,
   "ps": 100,
-  "maxResultsReached": false,
   "paging": {
     "pageIndex": 1,
     "pageSize": 100,
index b236a18b366238a4d60282cda8a3d80b4b6fa8ba..a766d0245921fc66351f34992c70221499b84613 100644 (file)
@@ -26,6 +26,7 @@ import org.sonar.api.BatchComponent;
 import org.sonar.api.ServerComponent;
 import org.sonar.api.issue.internal.DefaultIssueComment;
 import org.sonar.api.issue.internal.FieldDiffs;
+import org.sonar.core.persistence.DaoComponent;
 import org.sonar.core.persistence.DbSession;
 import org.sonar.core.persistence.MyBatis;
 
@@ -43,7 +44,7 @@ import static com.google.common.collect.Maps.newHashMap;
 /**
  * @since 3.6
  */
-public class IssueChangeDao implements BatchComponent, ServerComponent {
+public class IssueChangeDao implements DaoComponent, BatchComponent, ServerComponent {
 
   private final MyBatis mybatis;
 
index ac41ee8639e3af15f89facea40d39d4d9e5bd32a..3d4690c2f0e1173d07a100b18b478b7a0e1b2cb4 100644 (file)
@@ -23,7 +23,6 @@ import org.apache.ibatis.annotations.Param;
 import org.sonar.core.rule.RuleDto;
 
 import javax.annotation.Nullable;
-import java.util.Collection;
 import java.util.Date;
 import java.util.List;
 
@@ -31,7 +30,7 @@ public interface IssueMapper {
 
   IssueDto selectByKey(String key);
 
-  List<IssueDto> selectByKeys(Collection<String> keys);
+  List<IssueDto> selectByKeys(List<String> keys);
 
   List<IssueDto> selectByActionPlan(String actionPlan);
 
index f8bffedc01977ad4686438942690fc27ed9f285f..faa0f05fb56f6f6241e0508f3d4b1661dc0495be 100644 (file)
@@ -35,7 +35,11 @@ import javax.annotation.concurrent.Immutable;
 
 import java.io.IOException;
 import java.net.URL;
-import java.util.*;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 /**
  * Defines a web service. Note that contrary to the deprecated {@link org.sonar.api.web.Webservice}
@@ -117,8 +121,7 @@ public interface WebService extends ServerExtension {
     private void register(NewController newController) {
       if (controllers.containsKey(newController.path)) {
         throw new IllegalStateException(
-          String.format("The web service '%s' is defined multiple times", newController.path)
-        );
+          String.format("The web service '%s' is defined multiple times", newController.path));
       }
       controllers.put(newController.path, new Controller(newController));
     }
@@ -177,8 +180,7 @@ public interface WebService extends ServerExtension {
     public NewAction createAction(String actionKey) {
       if (actions.containsKey(actionKey)) {
         throw new IllegalStateException(
-          String.format("The action '%s' is defined multiple times in the web service '%s'", actionKey, path)
-        );
+          String.format("The action '%s' is defined multiple times in the web service '%s'", actionKey, path));
       }
       NewAction action = new NewAction(actionKey);
       actions.put(actionKey, action);
@@ -194,8 +196,7 @@ public interface WebService extends ServerExtension {
     private Controller(NewController newController) {
       if (newController.actions.isEmpty()) {
         throw new IllegalStateException(
-          String.format("At least one action must be declared in the web service '%s'", newController.path)
-        );
+          String.format("At least one action must be declared in the web service '%s'", newController.path));
       }
       this.path = newController.path;
       this.description = newController.description;
@@ -306,8 +307,7 @@ public interface WebService extends ServerExtension {
     public NewParam createParam(String paramKey) {
       if (newParams.containsKey(paramKey)) {
         throw new IllegalStateException(
-          String.format("The parameter '%s' is defined multiple times in the action '%s'", paramKey, key)
-        );
+          String.format("The parameter '%s' is defined multiple times in the action '%s'", paramKey, key));
       }
       NewParam newParam = new NewParam(paramKey);
       newParams.put(paramKey, newParam);
@@ -322,6 +322,52 @@ public interface WebService extends ServerExtension {
       createParam(paramKey).setDescription(description);
       return this;
     }
+
+    /**
+     * Add predefined parameters related to pagination of results.
+     */
+    public NewAction addPagingParams(int defaultPageSize) {
+      createParam(Param.PAGE)
+        .setDescription("1-based page number")
+        .setExampleValue("42")
+        .setDeprecatedKey("pageIndex")
+        .setDefaultValue("1");
+
+      createParam(Param.PAGE_SIZE)
+        .setDescription("Page size. Must be greater than 0.")
+        .setExampleValue("20")
+        .setDeprecatedKey("pageSize")
+        .setDefaultValue(String.valueOf(defaultPageSize));
+      return this;
+    }
+
+    /**
+     * Creates the parameter {@link org.sonar.api.server.ws.WebService.Param#FIELDS}, which is
+     * used to restrict the number of fields returned in JSON response.
+     */
+    public NewAction addFieldsParam(Collection possibleValues) {
+      createParam(Param.FIELDS)
+        .setDescription("Comma-separated list of the fields to be returned in response. All the fields are returned by default.")
+        .setPossibleValues(possibleValues);
+      return this;
+    }
+
+    /**
+     * Add predefined parameters related to sorting of results.
+     */
+    public <V> NewAction addSortParams(Collection<V> possibleValues, @Nullable V defaultValue, boolean defaultAscending) {
+      createParam(Param.SORT)
+        .setDescription("Sort field")
+        .setDeprecatedKey("sort")
+        .setDefaultValue(defaultValue)
+        .setPossibleValues(possibleValues);
+
+      createParam(Param.ASCENDING)
+        .setDescription("Ascending sort")
+        .setBooleanPossibleValues()
+        .setDefaultValue(defaultAscending);
+      return this;
+    }
   }
 
   @Immutable
@@ -531,6 +577,14 @@ public interface WebService extends ServerExtension {
 
   @Immutable
   class Param {
+    public static final String TEXT_QUERY = "q";
+    public static final String PAGE = "p";
+    public static final String PAGE_SIZE = "ps";
+    public static final String FIELDS = "f";
+    public static final String SORT = "s";
+    public static final String ASCENDING = "asc";
+    public static final String FACETS = "facets";
+
     private final String key, deprecatedKey, description, exampleValue, defaultValue;
     private final boolean required;
     private final Set<String> possibleValues;
index 550b959019fa7944486802c9900452d0acadfacc..9226ea1af6053c0ae20a5af0669da05ef5a6712f 100644 (file)
@@ -25,6 +25,7 @@ import org.sonar.api.rule.RuleStatus;
 
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 
@@ -257,15 +258,18 @@ public class WebServiceTest {
       @Override
       public void define(Context context) {
         NewController newController = context.createController("api/rule");
-        NewAction create = newController.createAction("create").setHandler(mock(RequestHandler.class));
-        create.createParam("key").setDescription("Key of the new rule");
-        create.createParam("severity").setDefaultValue("MAJOR").setPossibleValues("INFO", "MAJOR", "BLOCKER");
+        NewAction newAction = newController.createAction("create").setHandler(mock(RequestHandler.class));
+        newAction.createParam("key").setDescription("Key of the new rule");
+        newAction.createParam("severity").setDefaultValue("MAJOR").setPossibleValues("INFO", "MAJOR", "BLOCKER");
+        newAction.addPagingParams(20);
+        newAction.addFieldsParam(Arrays.asList("name", "severity"));
+        newAction.addSortParams(Arrays.asList("name", "updatedAt", "severity"), "updatedAt", false);
         newController.done();
       }
     }.define(context);
 
     WebService.Action action = context.controller("api/rule").action("create");
-    assertThat(action.params()).hasSize(2);
+    assertThat(action.params()).hasSize(7);
 
     assertThat(action.param("key").key()).isEqualTo("key");
     assertThat(action.param("key").description()).isEqualTo("Key of the new rule");
@@ -275,6 +279,16 @@ public class WebServiceTest {
     assertThat(action.param("severity").description()).isNull();
     assertThat(action.param("severity").defaultValue()).isEqualTo("MAJOR");
     assertThat(action.param("severity").possibleValues()).containsOnly("INFO", "MAJOR", "BLOCKER");
+
+    // predefined fields
+    assertThat(action.param("p").defaultValue()).isEqualTo("1");
+    assertThat(action.param("p").description()).isNotEmpty();
+    assertThat(action.param("ps").defaultValue()).isEqualTo("20");
+    assertThat(action.param("ps").description()).isNotEmpty();
+    assertThat(action.param("f").possibleValues()).containsOnly("name", "severity");
+    assertThat(action.param("s").possibleValues()).containsOnly("name", "severity", "updatedAt");
+    assertThat(action.param("s").description()).isNotEmpty();
+    assertThat(action.param("asc").defaultValue()).isEqualTo("false");
   }
 
   @Test