Browse Source

Refactor issue search stack in order to remove dependency with

SearchRequestHandler
tags/5.1-RC1
Simon Brandhof 9 years ago
parent
commit
0ea7c0cedc
59 changed files with 2247 additions and 1655 deletions
  1. 3
    3
      server/sonar-server-benchmarks/src/test/java/org/sonar/server/benchmark/IssueIndexBenchmarkTest.java
  2. 9
    6
      server/sonar-server/src/main/java/org/sonar/core/computation/dbcleaner/ProjectCleaner.java
  3. 8
    2
      server/sonar-server/src/main/java/org/sonar/server/db/DbClient.java
  4. 35
    0
      server/sonar-server/src/main/java/org/sonar/server/es/BaseIndex.java
  5. 65
    0
      server/sonar-server/src/main/java/org/sonar/server/es/EsUtils.java
  6. 153
    0
      server/sonar-server/src/main/java/org/sonar/server/es/Facets.java
  7. 179
    0
      server/sonar-server/src/main/java/org/sonar/server/es/SearchOptions.java
  8. 58
    0
      server/sonar-server/src/main/java/org/sonar/server/es/SearchResult.java
  9. 2
    4
      server/sonar-server/src/main/java/org/sonar/server/issue/AbstractChangeTagsAction.java
  10. 2
    1
      server/sonar-server/src/main/java/org/sonar/server/issue/Action.java
  11. 2
    2
      server/sonar-server/src/main/java/org/sonar/server/issue/AssignAction.java
  12. 3
    4
      server/sonar-server/src/main/java/org/sonar/server/issue/CommentAction.java
  13. 7
    6
      server/sonar-server/src/main/java/org/sonar/server/issue/InternalRubyIssueService.java
  14. 33
    22
      server/sonar-server/src/main/java/org/sonar/server/issue/IssueBulkChangeService.java
  15. 41
    1
      server/sonar-server/src/main/java/org/sonar/server/issue/IssueQuery.java
  16. 45
    20
      server/sonar-server/src/main/java/org/sonar/server/issue/IssueService.java
  17. 4
    5
      server/sonar-server/src/main/java/org/sonar/server/issue/PlanAction.java
  18. 3
    3
      server/sonar-server/src/main/java/org/sonar/server/issue/SetSeverityAction.java
  19. 2
    2
      server/sonar-server/src/main/java/org/sonar/server/issue/TransitionAction.java
  20. 1
    3
      server/sonar-server/src/main/java/org/sonar/server/issue/db/IssueDao.java
  21. 15
    15
      server/sonar-server/src/main/java/org/sonar/server/issue/filter/IssueFilterService.java
  22. 319
    293
      server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndex.java
  23. 10
    9
      server/sonar-server/src/main/java/org/sonar/server/issue/ws/AuthorsAction.java
  24. 8
    7
      server/sonar-server/src/main/java/org/sonar/server/issue/ws/BaseIssuesWsAction.java
  25. 3
    3
      server/sonar-server/src/main/java/org/sonar/server/issue/ws/ComponentTagsAction.java
  26. 3
    4
      server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueShowAction.java
  27. 11
    25
      server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssuesWs.java
  28. 123
    125
      server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchAction.java
  29. 9
    12
      server/sonar-server/src/main/java/org/sonar/server/issue/ws/SetTagsAction.java
  30. 6
    3
      server/sonar-server/src/main/java/org/sonar/server/issue/ws/TagsAction.java
  31. 2
    4
      server/sonar-server/src/main/java/org/sonar/server/search/StickyFacetBuilder.java
  32. 5
    7
      server/sonar-server/src/main/java/org/sonar/server/source/index/SourceLineIndex.java
  33. 3
    8
      server/sonar-server/src/main/resources/org/sonar/server/issue/ws/example-search.json
  34. 17
    24
      server/sonar-server/src/test/java/org/sonar/core/computation/dbcleaner/ProjectCleanerTest.java
  35. 69
    0
      server/sonar-server/src/test/java/org/sonar/server/es/EsUtilsTest.java
  36. 156
    0
      server/sonar-server/src/test/java/org/sonar/server/es/SearchOptionsTest.java
  37. 5
    4
      server/sonar-server/src/test/java/org/sonar/server/issue/ActionTest.java
  38. 19
    21
      server/sonar-server/src/test/java/org/sonar/server/issue/InternalRubyIssueServiceTest.java
  39. 321
    361
      server/sonar-server/src/test/java/org/sonar/server/issue/IssueBulkChangeServiceTest.java
  40. 42
    39
      server/sonar-server/src/test/java/org/sonar/server/issue/IssueServiceMediumTest.java
  41. 8
    9
      server/sonar-server/src/test/java/org/sonar/server/issue/filter/IssueFilterServiceTest.java
  42. 302
    306
      server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexMediumTest.java
  43. 1
    4
      server/sonar-server/src/test/java/org/sonar/server/issue/ws/AuthorsActionTest.java
  44. 1
    4
      server/sonar-server/src/test/java/org/sonar/server/issue/ws/ComponentTagsActionTest.java
  45. 6
    8
      server/sonar-server/src/test/java/org/sonar/server/issue/ws/IssueShowActionTest.java
  46. 1
    4
      server/sonar-server/src/test/java/org/sonar/server/issue/ws/IssueTagsActionTest.java
  47. 13
    223
      server/sonar-server/src/test/java/org/sonar/server/issue/ws/IssuesWsTest.java
  48. 7
    6
      server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionComponentsMediumTest.java
  49. 7
    7
      server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionMediumTest.java
  50. 9
    13
      server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetTagsActionTest.java
  51. 7
    3
      server/sonar-server/src/test/java/org/sonar/server/view/index/ViewIndexerMediumTest.java
  52. 0
    1
      server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionMediumTest/default_page_size_is_100.json
  53. 0
    1
      server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionMediumTest/deprecated_paging.json
  54. 0
    1
      server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionMediumTest/empty_result.json
  55. 0
    1
      server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionMediumTest/issue.json
  56. 2
    1
      sonar-core/src/main/java/org/sonar/core/issue/db/IssueChangeDao.java
  57. 1
    2
      sonar-core/src/main/java/org/sonar/core/issue/db/IssueMapper.java
  58. 63
    9
      sonar-plugin-api/src/main/java/org/sonar/api/server/ws/WebService.java
  59. 18
    4
      sonar-plugin-api/src/test/java/org/sonar/api/server/ws/WebServiceTest.java

+ 3
- 3
server/sonar-server-benchmarks/src/test/java/org/sonar/server/benchmark/IssueIndexBenchmarkTest.java View 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);
}

+ 9
- 6
server/sonar-server/src/main/java/org/sonar/core/computation/dbcleaner/ProjectCleaner.java View 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);
}
}


+ 8
- 2
server/sonar-server/src/main/java/org/sonar/server/db/DbClient.java View File

@@ -19,10 +19,9 @@
*/
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;
}

+ 35
- 0
server/sonar-server/src/main/java/org/sonar/server/es/BaseIndex.java View File

@@ -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;
}

}

+ 65
- 0
server/sonar-server/src/main/java/org/sonar/server/es/EsUtils.java View File

@@ -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();
}
});
}
}

+ 153
- 0
server/sonar-server/src/main/java/org/sonar/server/es/Facets.java View File

@@ -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;
}
}

+ 179
- 0
server/sonar-server/src/main/java/org/sonar/server/es/SearchOptions.java View File

@@ -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;
}
}

+ 58
- 0
server/sonar-server/src/main/java/org/sonar/server/es/SearchResult.java View File

@@ -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);
}
}

+ 2
- 4
server/sonar-server/src/main/java/org/sonar/server/issue/AbstractChangeTagsAction.java View 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);
}

+ 2
- 1
server/sonar-server/src/main/java/org/sonar/server/issue/Action.java View 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);


+ 2
- 2
server/sonar-server/src/main/java/org/sonar/server/issue/AssignAction.java View 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);

+ 3
- 4
server/sonar-server/src/main/java/org/sonar/server/issue/CommentAction.java View 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;
}

+ 7
- 6
server/sonar-server/src/main/java/org/sonar/server/issue/InternalRubyIssueService.java View 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() {

+ 33
- 22
server/sonar-server/src/main/java/org/sonar/server/issue/IssueBulkChangeService.java View 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();

+ 41
- 1
server/sonar-server/src/main/java/org/sonar/server/issue/IssueQuery.java View 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;

+ 45
- 20
server/sonar-server/src/main/java/org/sonar/server/issue/IssueService.java View 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

+ 4
- 5
server/sonar-server/src/main/java/org/sonar/server/issue/PlanAction.java View 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;

+ 3
- 3
server/sonar-server/src/main/java/org/sonar/server/issue/SetSeverityAction.java View 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;
}

+ 2
- 2
server/sonar-server/src/main/java/org/sonar/server/issue/TransitionAction.java View 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;
}

+ 1
- 3
server/sonar-server/src/main/java/org/sonar/server/issue/db/IssueDao.java View 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);
}


+ 15
- 15
server/sonar-server/src/main/java/org/sonar/server/issue/filter/IssueFilterService.java View 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;
}
}
}
}

+ 319
- 293
server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndex.java View File

@@ -20,24 +20,25 @@
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();
}
}

+ 10
- 9
server/sonar-server/src/main/java/org/sonar/server/issue/ws/AuthorsAction.java View File

@@ -20,15 +20,15 @@
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");

server/sonar-server/src/main/java/org/sonar/server/issue/index/FakeIssueDto.java → server/sonar-server/src/main/java/org/sonar/server/issue/ws/BaseIssuesWsAction.java View File

@@ -17,13 +17,14 @@
* 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;
package org.sonar.server.issue.ws;

import org.sonar.core.persistence.Dto;
import org.sonar.api.server.ws.RequestHandler;
import org.sonar.api.server.ws.WebService;

interface BaseIssuesWsAction extends RequestHandler {

void define(WebService.NewController controller);

public class FakeIssueDto extends Dto<String> {
@Override
public String getKey() {
throw new UnsupportedOperationException();
}
}


+ 3
- 3
server/sonar-server/src/main/java/org/sonar/server/issue/ws/ComponentTagsAction.java View 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")

+ 3
- 4
server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueShowAction.java View 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")

+ 11
- 25
server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssuesWs.java View 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) {

+ 123
- 125
server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchAction.java View 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();
}
}
}
}
}

+ 9
- 12
server/sonar-server/src/main/java/org/sonar/server/issue/ws/SetTagsAction.java View File

@@ -19,11 +19,8 @@
*/
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);

+ 6
- 3
server/sonar-server/src/main/java/org/sonar/server/issue/ws/TagsAction.java View 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")

+ 2
- 4
server/sonar-server/src/main/java/org/sonar/server/search/StickyFacetBuilder.java View 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;

+ 5
- 7
server/sonar-server/src/main/java/org/sonar/server/source/index/SourceLineIndex.java View 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()

+ 3
- 8
server/sonar-server/src/main/resources/org/sonar/server/issue/ws/example-search.json View 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",

+ 17
- 24
server/sonar-server/src/test/java/org/sonar/core/computation/dbcleaner/ProjectCleanerTest.java View 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));


+ 69
- 0
server/sonar-server/src/test/java/org/sonar/server/es/EsUtilsTest.java View File

@@ -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));
}
}

+ 156
- 0
server/sonar-server/src/test/java/org/sonar/server/es/SearchOptionsTest.java View File

@@ -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);
}
}

+ 5
- 4
server/sonar-server/src/test/java/org/sonar/server/issue/ActionTest.java View 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;

+ 19
- 21
server/sonar-server/src/test/java/org/sonar/server/issue/InternalRubyIssueServiceTest.java View 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);
}

+ 321
- 361
server/sonar-server/src/test/java/org/sonar/server/issue/IssueBulkChangeServiceTest.java View File

@@ -20,367 +20,327 @@

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;
// }
// }
}

+ 42
- 39
server/sonar-server/src/test/java/org/sonar/server/issue/IssueServiceMediumTest.java View File

@@ -19,7 +19,11 @@
*/
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();
}

+ 8
- 9
server/sonar-server/src/test/java/org/sonar/server/issue/filter/IssueFilterServiceTest.java View 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);

+ 302
- 306
server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexMediumTest.java
File diff suppressed because it is too large
View File


+ 1
- 4
server/sonar-server/src/test/java/org/sonar/server/issue/ws/AuthorsActionTest.java View 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");
}


+ 1
- 4
server/sonar-server/src/test/java/org/sonar/server/issue/ws/ComponentTagsActionTest.java View 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

+ 6
- 8
server/sonar-server/src/test/java/org/sonar/server/issue/ws/IssueShowActionTest.java View 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)
));
}


+ 1
- 4
server/sonar-server/src/test/java/org/sonar/server/issue/ws/IssueTagsActionTest.java View 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

+ 13
- 223
server/sonar-server/src/test/java/org/sonar/server/issue/ws/IssuesWsTest.java View File

@@ -19,242 +19,32 @@
*/
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));
}

}

+ 7
- 6
server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionComponentsMediumTest.java View 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);


+ 7
- 7
server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionMediumTest.java View 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);

+ 9
- 13
server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetTagsActionTest.java View 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"));
}
}

+ 7
- 3
server/sonar-server/src/test/java/org/sonar/server/view/index/ViewIndexerMediumTest.java View 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) {

+ 0
- 1
server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionMediumTest/default_page_size_is_100.json View File

@@ -2,7 +2,6 @@
"total": 0,
"p": 1,
"ps": 100,
"maxResultsReached": false,
"paging": {
"pageIndex": 1,
"pageSize": 100,

+ 0
- 1
server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionMediumTest/deprecated_paging.json View File

@@ -1,5 +1,4 @@
{
"maxResultsReached": false,
"paging": {
"pageIndex": 2,
"pageSize": 9,

+ 0
- 1
server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionMediumTest/empty_result.json View File

@@ -2,7 +2,6 @@
"total": 0,
"p": 1,
"ps": 100,
"maxResultsReached": false,
"paging": {
"pageIndex": 1,
"pageSize": 100,

+ 0
- 1
server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionMediumTest/issue.json View File

@@ -1,5 +1,4 @@
{
"maxResultsReached": false,
"total": 1,
"p": 1,
"ps": 100,

+ 2
- 1
sonar-core/src/main/java/org/sonar/core/issue/db/IssueChangeDao.java View 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;


+ 1
- 2
sonar-core/src/main/java/org/sonar/core/issue/db/IssueMapper.java View 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);


+ 63
- 9
sonar-plugin-api/src/main/java/org/sonar/api/server/ws/WebService.java View 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;

+ 18
- 4
sonar-plugin-api/src/test/java/org/sonar/api/server/ws/WebServiceTest.java View 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

Loading…
Cancel
Save