aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJean-Baptiste Lievremont <jean-baptiste.lievremont@sonarsource.com>2015-06-15 17:31:57 +0200
committerJean-Baptiste Lievremont <jean-baptiste.lievremont@sonarsource.com>2015-06-25 11:56:29 +0200
commit8f7068c61bbeab118d2b5f197963371ca3a92a2b (patch)
tree34eef758c4fbbaf5bc1d0a29253f9b20c0a2caf9
parenta77b83ff194ac3af5ad2d7cbdf2b6c01bcd12326 (diff)
downloadsonarqube-8f7068c61bbeab118d2b5f197963371ca3a92a2b.tar.gz
sonarqube-8f7068c61bbeab118d2b5f197963371ca3a92a2b.zip
SONAR-6078 Aggregate technical debt on issues search WS
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/es/Facets.java39
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/issue/IssueQuery.java11
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/issue/IssueQueryService.java9
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/issue/filter/IssueFilterParameters.java6
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndex.java60
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchAction.java9
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/search/StickyFacetBuilder.java42
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexDebtTest.java293
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionMediumTest.java27
-rw-r--r--server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionMediumTest/display_facets_debt.json144
10 files changed, 602 insertions, 38 deletions
diff --git a/server/sonar-server/src/main/java/org/sonar/server/es/Facets.java b/server/sonar-server/src/main/java/org/sonar/server/es/Facets.java
index a22131e18f3..9bc8519608e 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/es/Facets.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/es/Facets.java
@@ -19,6 +19,11 @@
*/
package org.sonar.server.es;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+import javax.annotation.CheckForNull;
import org.apache.commons.lang.builder.ReflectionToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.elasticsearch.action.search.SearchResponse;
@@ -27,15 +32,12 @@ 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;
+import org.elasticsearch.search.aggregations.metrics.sum.Sum;
public class Facets {
+ public static final String TOTAL = "total";
+
private final Map<String, LinkedHashMap<String, Long>> facetsByName = new LinkedHashMap<>();
public Facets(SearchResponse response) {
@@ -59,6 +61,8 @@ public class Facets {
processSubAggregations((HasAggregations) aggregation);
} else if (DateHistogram.class.isAssignableFrom(aggregation.getClass())) {
processDateHistogram((DateHistogram) aggregation);
+ } else if (Sum.class.isAssignableFrom(aggregation.getClass())) {
+ processSum((Sum) aggregation);
} else {
throw new IllegalArgumentException("Aggregation type not supported yet: " + aggregation.getClass());
}
@@ -67,7 +71,12 @@ public class Facets {
private void processMissingAggregation(Missing aggregation) {
long docCount = aggregation.getDocCount();
if (docCount > 0L) {
- getOrCreateFacet(aggregation.getName().replace("_missing", "")).put("", docCount);
+ LinkedHashMap<String, Long> facet = getOrCreateFacet(aggregation.getName().replace("_missing", ""));
+ if (aggregation.getAggregations().getAsMap().containsKey("debt")) {
+ facet.put("", Math.round(((Sum) aggregation.getAggregations().get("debt")).getValue()));
+ } else {
+ facet.put("", docCount);
+ }
}
}
@@ -80,7 +89,11 @@ public class Facets {
facetName = facetName.replace("_selected", "");
LinkedHashMap<String, Long> facet = getOrCreateFacet(facetName);
for (Terms.Bucket value : aggregation.getBuckets()) {
- facet.put(value.getKey(), value.getDocCount());
+ if (value.getAggregations().getAsMap().containsKey("debt")) {
+ facet.put(value.getKey(), Math.round(((Sum) value.getAggregations().get("debt")).getValue()));
+ } else {
+ facet.put(value.getKey(), value.getDocCount());
+ }
}
}
@@ -93,10 +106,18 @@ public class Facets {
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());
+ if (value.getAggregations().getAsMap().containsKey("debt")) {
+ facet.put(value.getKey(), Math.round(((Sum) value.getAggregations().get("debt")).getValue()));
+ } else {
+ facet.put(value.getKey(), value.getDocCount());
+ }
}
}
+ private void processSum(Sum aggregation) {
+ getOrCreateFacet(aggregation.getName()).put(TOTAL, Math.round(aggregation.getValue()));
+ }
+
public boolean contains(String facetName) {
return facetsByName.containsKey(facetName);
}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/IssueQuery.java b/server/sonar-server/src/main/java/org/sonar/server/issue/IssueQuery.java
index bcb2c9654af..ee4a1ce1d17 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/issue/IssueQuery.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/issue/IssueQuery.java
@@ -83,6 +83,7 @@ public class IssueQuery {
private final Date createdBefore;
private final String sort;
private final Boolean asc;
+ private final String facetMode;
private final String userLogin;
private final Set<String> userGroups;
@@ -121,6 +122,7 @@ public class IssueQuery {
this.userLogin = builder.userLogin;
this.userGroups = builder.userGroups;
this.checkAuthorization = builder.checkAuthorization;
+ this.facetMode = builder.facetMode;
}
public Collection<String> issueKeys() {
@@ -269,6 +271,10 @@ public class IssueQuery {
return checkAuthorization;
}
+ public String facetMode() {
+ return facetMode;
+ }
+
@Override
public String toString() {
return ReflectionToStringBuilder.toString(this);
@@ -311,6 +317,7 @@ public class IssueQuery {
private String userLogin;
private Set<String> userGroups;
private boolean checkAuthorization = true;
+ private String facetMode;
private Builder(UserSession userSession) {
this.userLogin = userSession.getLogin();
@@ -518,6 +525,10 @@ public class IssueQuery {
return this;
}
+ public Builder facetMode(String facetMode) {
+ this.facetMode = facetMode;
+ return this;
+ }
}
private static <T> Collection<T> defaultCollection(@Nullable Collection<T> c) {
diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/IssueQueryService.java b/server/sonar-server/src/main/java/org/sonar/server/issue/IssueQueryService.java
index 38f40ab43db..134a9414a4b 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/issue/IssueQueryService.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/issue/IssueQueryService.java
@@ -132,6 +132,12 @@ public class IssueQueryService {
builder.sort(sort);
builder.asc(RubyUtils.toBoolean(params.get(IssueFilterParameters.ASC)));
}
+ String facetMode = (String) params.get(IssueFilterParameters.FACET_MODE);
+ if (!Strings.isNullOrEmpty(facetMode)) {
+ builder.facetMode(facetMode);
+ } else {
+ builder.facetMode(IssueFilterParameters.FACET_MODE_COUNT);
+ }
return builder.build();
} finally {
@@ -168,7 +174,8 @@ public class IssueQueryService {
.planned(request.paramAsBoolean(IssueFilterParameters.PLANNED))
.createdAt(request.paramAsDateTime(IssueFilterParameters.CREATED_AT))
.createdAfter(buildCreatedAfter(request.paramAsDateTime(IssueFilterParameters.CREATED_AFTER), request.param(IssueFilterParameters.CREATED_IN_LAST)))
- .createdBefore(request.paramAsDateTime(IssueFilterParameters.CREATED_BEFORE));
+ .createdBefore(request.paramAsDateTime(IssueFilterParameters.CREATED_BEFORE))
+ .facetMode(request.mandatoryParam(IssueFilterParameters.FACET_MODE));
Set<String> allComponentUuids = Sets.newHashSet();
boolean effectiveOnComponentOnly = mergeDeprecatedComponentParameters(session,
diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/filter/IssueFilterParameters.java b/server/sonar-server/src/main/java/org/sonar/server/issue/filter/IssueFilterParameters.java
index 96b5589eb16..3f1b221d4ec 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/issue/filter/IssueFilterParameters.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/issue/filter/IssueFilterParameters.java
@@ -68,11 +68,15 @@ public class IssueFilterParameters {
public static final String PAGE_INDEX = "pageIndex";
public static final String SORT = "sort";
public static final String ASC = "asc";
+ public static final String FACET_MODE = "facetMode";
+
+ public static final String FACET_MODE_COUNT = "count";
+ public static final String FACET_MODE_DEBT = "debt";
public static final String FACET_ASSIGNED_TO_ME = "assigned_to_me";
public static final List<String> ALL = ImmutableList.of(ISSUES, SEVERITIES, STATUSES, RESOLUTIONS, RESOLVED, COMPONENTS, COMPONENT_ROOTS, RULES, ACTION_PLANS, REPORTERS, TAGS,
- ASSIGNEES, LANGUAGES, ASSIGNED, PLANNED, HIDE_RULES, CREATED_AT, CREATED_AFTER, CREATED_BEFORE, CREATED_IN_LAST, COMPONENT_UUIDS, COMPONENT_ROOT_UUIDS,
+ ASSIGNEES, LANGUAGES, ASSIGNED, PLANNED, HIDE_RULES, CREATED_AT, CREATED_AFTER, CREATED_BEFORE, CREATED_IN_LAST, COMPONENT_UUIDS, COMPONENT_ROOT_UUIDS, FACET_MODE,
PROJECTS, PROJECT_UUIDS, PROJECT_KEYS, COMPONENT_KEYS, MODULE_UUIDS, DIRECTORIES, FILE_UUIDS, AUTHORS, HIDE_COMMENTS, PAGE_SIZE, PAGE_INDEX, SORT, ASC);
public static final List<String> ALL_WITHOUT_PAGINATION = newArrayList(Iterables.filter(ALL, new Predicate<String>() {
diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndex.java b/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndex.java
index b73dc7a7a0b..98650655de5 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndex.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndex.java
@@ -57,8 +57,10 @@ import org.elasticsearch.search.aggregations.bucket.global.Global;
import org.elasticsearch.search.aggregations.bucket.global.GlobalBuilder;
import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogram;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
+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.elasticsearch.search.aggregations.metrics.sum.SumBuilder;
import org.joda.time.Duration;
import org.sonar.api.issue.Issue;
import org.sonar.api.resources.Scopes;
@@ -90,6 +92,7 @@ import static com.google.common.collect.Lists.newArrayList;
*/
public class IssueIndex extends BaseIndex {
+
private static final String SUBSTRING_MATCH_REGEXP = ".*%s.*";
public static final List<String> SUPPORTED_FACETS = ImmutableList.of(
@@ -116,6 +119,10 @@ public class IssueIndex extends BaseIndex {
private static final String IS_ASSIGNED_FILTER = "__isAssigned";
+ public static final String DEBT_AGGREGATION_NAME = "debt";
+ private static final SumBuilder DEBT_AGGREGATION = AggregationBuilders.sum(DEBT_AGGREGATION_NAME).field(IssueIndexDefinition.FIELD_ISSUE_DEBT);
+ private static final Order DEBT_AGGREGATION_ORDER = Order.aggregation(DEBT_AGGREGATION_NAME, false);
+
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);
@@ -373,7 +380,7 @@ public class IssueIndex extends BaseIndex {
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);
+ StickyFacetBuilder stickyFacetBuilder = newStickyFacetBuilder(query, filters, esQuery);
// Execute Term aggregations
addSimpleStickyFacetIfNeeded(options, stickyFacetBuilder, esSearch,
IssueFilterParameters.SEVERITIES, IssueIndexDefinition.FIELD_ISSUE_SEVERITY, Severity.ALL.toArray());
@@ -402,7 +409,7 @@ public class IssueIndex extends BaseIndex {
}
if (options.getFacets().contains(IssueFilterParameters.RESOLUTIONS)) {
- esSearch.addAggregation(createResolutionFacet(filters, esQuery));
+ esSearch.addAggregation(createResolutionFacet(query, filters, esQuery));
}
if (options.getFacets().contains(IssueFilterParameters.ASSIGNEES)) {
esSearch.addAggregation(createAssigneesFacet(query, filters, esQuery));
@@ -415,6 +422,20 @@ public class IssueIndex extends BaseIndex {
esSearch.addAggregation(getCreatedAtFacet(query, filters, esQuery));
}
}
+
+ if (IssueFilterParameters.FACET_MODE_DEBT.equals(query.facetMode())) {
+ esSearch.addAggregation(DEBT_AGGREGATION);
+ }
+ }
+
+ private StickyFacetBuilder newStickyFacetBuilder(IssueQuery query, Map<String, FilterBuilder> filters, QueryBuilder esQuery) {
+ StickyFacetBuilder stickyFacetBuilder;
+ if (IssueFilterParameters.FACET_MODE_DEBT.equals(query.facetMode())) {
+ stickyFacetBuilder = new StickyFacetBuilder(esQuery, filters, DEBT_AGGREGATION, DEBT_AGGREGATION_ORDER);
+ } else {
+ stickyFacetBuilder = new StickyFacetBuilder(esQuery, filters);
+ }
+ return stickyFacetBuilder;
}
private void addSimpleStickyFacetIfNeeded(SearchOptions options, StickyFacetBuilder stickyFacetBuilder, SearchRequestBuilder esSearch,
@@ -424,6 +445,13 @@ public class IssueIndex extends BaseIndex {
}
}
+ private AggregationBuilder addDebtAggregationIfNeeded(IssueQuery query, AggregationBuilder aggregation) {
+ if (IssueFilterParameters.FACET_MODE_DEBT.equals(query.facetMode())) {
+ aggregation.subAggregation(DEBT_AGGREGATION);
+ }
+ return aggregation;
+ }
+
private AggregationBuilder getCreatedAtFacet(IssueQuery query, Map<String, FilterBuilder> filters, QueryBuilder esQuery) {
long now = system.now();
@@ -445,7 +473,7 @@ public class IssueIndex extends BaseIndex {
bucketSize = DateHistogram.Interval.MONTH;
}
- return AggregationBuilders.dateHistogram(IssueFilterParameters.CREATED_AT)
+ AggregationBuilder dateHistogram = AggregationBuilders.dateHistogram(IssueFilterParameters.CREATED_AT)
.field(IssueIndexDefinition.FIELD_ISSUE_FUNC_CREATED_AT)
.interval(bucketSize)
.minDocCount(0L)
@@ -453,6 +481,8 @@ public class IssueIndex extends BaseIndex {
.preZone(timeZoneString)
.postZone(timeZoneString)
.extendedBounds(startTime, endTime);
+ dateHistogram = addDebtAggregationIfNeeded(query, dateHistogram);
+ return dateHistogram;
}
private long getMinCreatedAt(Map<String, FilterBuilder> filters, QueryBuilder esQuery) {
@@ -490,7 +520,7 @@ public class IssueIndex extends BaseIndex {
Map<String, FilterBuilder> assigneeFilters = Maps.newHashMap(filters);
assigneeFilters.remove(IS_ASSIGNED_FILTER);
assigneeFilters.remove(fieldName);
- StickyFacetBuilder assigneeFacetBuilder = new StickyFacetBuilder(queryBuilder, assigneeFilters);
+ StickyFacetBuilder assigneeFacetBuilder = newStickyFacetBuilder(query, assigneeFilters, queryBuilder);
BoolFilterBuilder facetFilter = assigneeFacetBuilder.getStickyFacetFilter(fieldName);
FilterAggregationBuilder facetTopAggregation = assigneeFacetBuilder.buildTopFacetAggregation(fieldName, facetName, facetFilter, DEFAULT_FACET_SIZE);
@@ -501,9 +531,9 @@ public class IssueIndex extends BaseIndex {
// Add missing facet for unassigned issues
facetTopAggregation.subAggregation(
- AggregationBuilders
+ addDebtAggregationIfNeeded(query, AggregationBuilders
.missing(facetName + FACET_SUFFIX_MISSING)
- .field(fieldName)
+ .field(fieldName))
);
return AggregationBuilders
@@ -531,20 +561,20 @@ public class IssueIndex extends BaseIndex {
String facetName = IssueFilterParameters.FACET_ASSIGNED_TO_ME;
// Same as in super.stickyFacetBuilder
- StickyFacetBuilder assignedToMeFacetBuilder = new StickyFacetBuilder(queryBuilder, filters);
+ StickyFacetBuilder assignedToMeFacetBuilder = newStickyFacetBuilder(query, filters, queryBuilder);
BoolFilterBuilder facetFilter = assignedToMeFacetBuilder.getStickyFacetFilter(IS_ASSIGNED_FILTER, fieldName);
FilterAggregationBuilder facetTopAggregation = AggregationBuilders
.filter(facetName + "__filter")
.filter(facetFilter)
- .subAggregation(AggregationBuilders.terms(facetName + "__terms").field(fieldName).include(login));
+ .subAggregation(addDebtAggregationIfNeeded(query, AggregationBuilders.terms(facetName + "__terms").field(fieldName).include(login)));
builder.addAggregation(
AggregationBuilders.global(facetName)
.subAggregation(facetTopAggregation));
}
- private AggregationBuilder createResolutionFacet(Map<String, FilterBuilder> filters, QueryBuilder esQuery) {
+ private AggregationBuilder createResolutionFacet(IssueQuery query, Map<String, FilterBuilder> filters, QueryBuilder esQuery) {
String fieldName = IssueIndexDefinition.FIELD_ISSUE_RESOLUTION;
String facetName = IssueFilterParameters.RESOLUTIONS;
@@ -552,16 +582,16 @@ public class IssueIndex extends BaseIndex {
Map<String, FilterBuilder> resolutionFilters = Maps.newHashMap(filters);
resolutionFilters.remove("__isResolved");
resolutionFilters.remove(fieldName);
- StickyFacetBuilder assigneeFacetBuilder = new StickyFacetBuilder(esQuery, resolutionFilters);
+ StickyFacetBuilder assigneeFacetBuilder = newStickyFacetBuilder(query, resolutionFilters, esQuery);
BoolFilterBuilder facetFilter = assigneeFacetBuilder.getStickyFacetFilter(fieldName);
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
facetTopAggregation.subAggregation(
- AggregationBuilders
+ addDebtAggregationIfNeeded(query, AggregationBuilders
.missing(facetName + FACET_SUFFIX_MISSING)
- .field(fieldName)
+ .field(fieldName))
);
return AggregationBuilders
@@ -577,16 +607,16 @@ public class IssueIndex extends BaseIndex {
Map<String, FilterBuilder> actionPlanFilters = Maps.newHashMap(filters);
actionPlanFilters.remove("__isPlanned");
actionPlanFilters.remove(fieldName);
- StickyFacetBuilder actionPlanFacetBuilder = new StickyFacetBuilder(esQuery, actionPlanFilters);
+ StickyFacetBuilder actionPlanFacetBuilder = newStickyFacetBuilder(query, actionPlanFilters, esQuery);
BoolFilterBuilder facetFilter = actionPlanFacetBuilder.getStickyFacetFilter(fieldName);
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
facetTopAggregation.subAggregation(
- AggregationBuilders
+ addDebtAggregationIfNeeded(query, AggregationBuilders
.missing(facetName + FACET_SUFFIX_MISSING)
- .field(fieldName)
+ .field(fieldName))
);
return AggregationBuilders
diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchAction.java b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchAction.java
index 512de33d3ef..3cc0cbe5a7f 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchAction.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchAction.java
@@ -55,6 +55,7 @@ import org.sonar.core.component.ComponentDto;
import org.sonar.core.persistence.DbSession;
import org.sonar.server.component.ws.ComponentJsonWriter;
import org.sonar.server.db.DbClient;
+import org.sonar.server.es.Facets;
import org.sonar.server.es.SearchOptions;
import org.sonar.server.es.SearchResult;
import org.sonar.server.issue.IssueQuery;
@@ -126,6 +127,10 @@ public class SearchAction implements IssuesWsAction {
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.createParam(IssueFilterParameters.FACET_MODE)
+ .setDefaultValue(IssueFilterParameters.FACET_MODE_COUNT)
+ .setDescription("Choose the returned value for facet items, either count of issues or sum of debt.")
+ .setPossibleValues(IssueFilterParameters.FACET_MODE_COUNT, IssueFilterParameters.FACET_MODE_DEBT);
action.addSortParams(IssueQuery.SORTS, null, true);
action.addFieldsParam(IssueJsonWriter.SELECTABLE_FIELDS);
@@ -286,6 +291,10 @@ public class SearchAction implements IssuesWsAction {
}
private void writeResponse(Request request, SearchResult<IssueDoc> result, JsonWriter json) {
+ if (result.getFacets().contains(IssueIndex.DEBT_AGGREGATION_NAME)) {
+ json.prop("debtTotal", result.getFacets().get(IssueIndex.DEBT_AGGREGATION_NAME).get(Facets.TOTAL));
+ }
+
List<String> issueKeys = newArrayList();
Set<RuleKey> ruleKeys = newHashSet();
Set<String> projectUuids = newHashSet();
diff --git a/server/sonar-server/src/main/java/org/sonar/server/search/StickyFacetBuilder.java b/server/sonar-server/src/main/java/org/sonar/server/search/StickyFacetBuilder.java
index 589536f1484..5bbf441c7a4 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/search/StickyFacetBuilder.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/search/StickyFacetBuilder.java
@@ -20,29 +20,41 @@
package org.sonar.server.search;
import com.google.common.base.Joiner;
+import java.util.Map;
+import javax.annotation.Nullable;
import org.apache.commons.lang.ArrayUtils;
import org.elasticsearch.index.query.BoolFilterBuilder;
import org.elasticsearch.index.query.FilterBuilder;
import org.elasticsearch.index.query.FilterBuilders;
import org.elasticsearch.index.query.QueryBuilder;
+import org.elasticsearch.search.aggregations.AbstractAggregationBuilder;
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.terms.Terms;
-
-import java.util.Map;
+import org.elasticsearch.search.aggregations.bucket.terms.Terms.Order;
+import org.elasticsearch.search.aggregations.bucket.terms.TermsBuilder;
public class StickyFacetBuilder {
private static final int FACET_DEFAULT_MIN_DOC_COUNT = 1;
private static final int FACET_DEFAULT_SIZE = 10;
+ private static final Order FACET_DEFAULT_ORDER = Terms.Order.count(false);
private final QueryBuilder query;
private final Map<String, FilterBuilder> filters;
+ private final AbstractAggregationBuilder subAggregation;
+ private final Order order;
public StickyFacetBuilder(QueryBuilder query, Map<String, FilterBuilder> filters) {
+ this(query, filters, null, FACET_DEFAULT_ORDER);
+ }
+
+ public StickyFacetBuilder(QueryBuilder query, Map<String, FilterBuilder> filters, @Nullable AbstractAggregationBuilder subAggregation, @Nullable Order order) {
this.query = query;
this.filters = filters;
+ this.subAggregation = subAggregation;
+ this.order = order;
}
public QueryBuilder query() {
@@ -78,23 +90,31 @@ public class StickyFacetBuilder {
}
public FilterAggregationBuilder buildTopFacetAggregation(String fieldName, String facetName, BoolFilterBuilder facetFilter, int size) {
+ TermsBuilder termsAggregation = AggregationBuilders.terms(facetName)
+ .field(fieldName)
+ .order(order)
+ // .order(Terms.Order.aggregation("debt", false))
+ .size(size)
+ .minDocCount(FACET_DEFAULT_MIN_DOC_COUNT);
+ if (subAggregation != null) {
+ termsAggregation = termsAggregation.subAggregation(subAggregation);
+ }
return AggregationBuilders
.filter(facetName + "_filter")
.filter(facetFilter)
- .subAggregation(
- AggregationBuilders.terms(facetName)
- .field(fieldName)
- .order(Terms.Order.count(false))
- .size(size)
- .minDocCount(FACET_DEFAULT_MIN_DOC_COUNT));
+ .subAggregation(termsAggregation);
}
public FilterAggregationBuilder addSelectedItemsToFacet(String fieldName, String facetName, FilterAggregationBuilder facetTopAggregation, Object... selected) {
if (selected.length > 0) {
+ TermsBuilder selectedTerms = AggregationBuilders.terms(facetName + "_selected")
+ .field(fieldName)
+ .include(Joiner.on('|').join(selected));
+ if (subAggregation != null) {
+ selectedTerms = selectedTerms.subAggregation(subAggregation);
+ }
facetTopAggregation.subAggregation(
- AggregationBuilders.terms(facetName + "_selected")
- .field(fieldName)
- .include(Joiner.on('|').join(selected)));
+ selectedTerms);
}
return facetTopAggregation;
}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexDebtTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexDebtTest.java
new file mode 100644
index 00000000000..d1962713ec3
--- /dev/null
+++ b/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexDebtTest.java
@@ -0,0 +1,293 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.issue.index;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.TimeZone;
+import javax.annotation.Nullable;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.config.Settings;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rule.Severity;
+import org.sonar.api.security.DefaultGroups;
+import org.sonar.api.utils.DateUtils;
+import org.sonar.api.utils.System2;
+import org.sonar.core.component.ComponentDto;
+import org.sonar.server.component.ComponentTesting;
+import org.sonar.server.es.EsTester;
+import org.sonar.server.es.SearchOptions;
+import org.sonar.server.es.SearchResult;
+import org.sonar.server.issue.IssueQuery;
+import org.sonar.server.issue.IssueQuery.Builder;
+import org.sonar.server.issue.IssueTesting;
+import org.sonar.server.issue.filter.IssueFilterParameters;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.view.index.ViewIndexDefinition;
+import org.sonar.server.view.index.ViewIndexer;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.entry;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class IssueIndexDebtTest {
+
+ @ClassRule
+ public static EsTester tester = new EsTester().addDefinitions(new IssueIndexDefinition(new Settings()), new ViewIndexDefinition(new Settings()));
+ @Rule
+ public UserSessionRule userSessionRule = UserSessionRule.standalone();
+
+ IssueIndex index;
+
+ IssueIndexer issueIndexer;
+ IssueAuthorizationIndexer issueAuthorizationIndexer;
+ ViewIndexer viewIndexer;
+
+ @Before
+ public void setUp() {
+ tester.truncateIndices();
+ issueIndexer = new IssueIndexer(null, tester.client());
+ issueAuthorizationIndexer = new IssueAuthorizationIndexer(null, tester.client());
+ viewIndexer = new ViewIndexer(null, tester.client());
+ System2 system = mock(System2.class);
+ when(system.getDefaultTimeZone()).thenReturn(TimeZone.getTimeZone("+01:00"));
+ when(system.now()).thenReturn(System.currentTimeMillis());
+
+ index = new IssueIndex(tester.client(), system, userSessionRule);
+
+ }
+
+ @Test
+ public void facets_on_projects() {
+ ComponentDto project = ComponentTesting.newProjectDto("ABCD");
+ ComponentDto project2 = ComponentTesting.newProjectDto("EFGH");
+
+ indexIssues(
+ IssueTesting.newDoc("ISSUE1", ComponentTesting.newFileDto(project)),
+ IssueTesting.newDoc("ISSUE2", ComponentTesting.newFileDto(project)),
+ IssueTesting.newDoc("ISSUE3", ComponentTesting.newFileDto(project2)));
+
+ SearchResult<IssueDoc> result = index.search(newQueryBuilder().build(), new SearchOptions().addFacets(newArrayList("projectUuids")));
+ assertThat(result.getFacets().getNames()).containsOnly("projectUuids", "debt");
+ assertThat(result.getFacets().get("projectUuids")).containsOnly(entry("ABCD", 20L), entry("EFGH", 10L));
+ assertThat(result.getFacets().get("debt")).containsOnly(entry("total", 30L));
+ }
+
+ @Test
+ public void facets_on_components() {
+ ComponentDto project = ComponentTesting.newProjectDto("A");
+ ComponentDto file1 = ComponentTesting.newFileDto(project, "ABCD");
+ ComponentDto file2 = ComponentTesting.newFileDto(project, "BCDE");
+ ComponentDto file3 = ComponentTesting.newFileDto(project, "CDEF");
+
+ indexIssues(
+ IssueTesting.newDoc("ISSUE1", project),
+ IssueTesting.newDoc("ISSUE2", file1),
+ IssueTesting.newDoc("ISSUE3", file2),
+ IssueTesting.newDoc("ISSUE4", file2),
+ IssueTesting.newDoc("ISSUE5", file3));
+
+ SearchResult<IssueDoc> result = index.search(newQueryBuilder().build(), new SearchOptions().addFacets(newArrayList("fileUuids")));
+ assertThat(result.getFacets().getNames()).containsOnly("fileUuids", "debt");
+ assertThat(result.getFacets().get("fileUuids"))
+ .containsOnly(entry("A", 10L), entry("ABCD", 10L), entry("BCDE", 20L), entry("CDEF", 10L));
+ assertThat(result.getFacets().get("debt")).containsOnly(entry("total", 50L));
+ }
+
+ @Test
+ public void facets_on_directories() {
+ ComponentDto project = ComponentTesting.newProjectDto();
+ ComponentDto file1 = ComponentTesting.newFileDto(project).setPath("src/main/xoo/F1.xoo");
+ ComponentDto file2 = ComponentTesting.newFileDto(project).setPath("F2.xoo");
+
+ indexIssues(
+ IssueTesting.newDoc("ISSUE1", file1).setDirectoryPath("/src/main/xoo"),
+ IssueTesting.newDoc("ISSUE2", file2).setDirectoryPath("/"));
+
+ SearchResult<IssueDoc> result = index.search(newQueryBuilder().build(), new SearchOptions().addFacets(newArrayList("directories")));
+ assertThat(result.getFacets().getNames()).containsOnly("directories", "debt");
+ assertThat(result.getFacets().get("directories")).containsOnly(entry("/src/main/xoo", 10L), entry("/", 10L));
+ assertThat(result.getFacets().get("debt")).containsOnly(entry("total", 20L));
+ }
+
+ @Test
+ public void facets_on_severities() {
+ ComponentDto project = ComponentTesting.newProjectDto();
+ ComponentDto file = ComponentTesting.newFileDto(project);
+
+ indexIssues(
+ IssueTesting.newDoc("ISSUE1", file).setSeverity(Severity.INFO),
+ IssueTesting.newDoc("ISSUE2", file).setSeverity(Severity.INFO),
+ IssueTesting.newDoc("ISSUE3", file).setSeverity(Severity.MAJOR));
+
+ SearchResult<IssueDoc> result = index.search(newQueryBuilder().build(), new SearchOptions().addFacets(newArrayList("severities")));
+ assertThat(result.getFacets().getNames()).containsOnly("severities", "debt");
+ assertThat(result.getFacets().get("severities")).containsOnly(entry("INFO", 20L), entry("MAJOR", 10L));
+ assertThat(result.getFacets().get("debt")).containsOnly(entry("total", 30L));
+ }
+
+ @Test
+ public void facets_on_statuses() {
+ ComponentDto project = ComponentTesting.newProjectDto();
+ ComponentDto file = ComponentTesting.newFileDto(project);
+
+ indexIssues(
+ IssueTesting.newDoc("ISSUE1", file).setStatus(Issue.STATUS_CLOSED),
+ IssueTesting.newDoc("ISSUE2", file).setStatus(Issue.STATUS_CLOSED),
+ IssueTesting.newDoc("ISSUE3", file).setStatus(Issue.STATUS_OPEN));
+
+ SearchResult<IssueDoc> result = index.search(newQueryBuilder().build(), new SearchOptions().addFacets(newArrayList("statuses")));
+ assertThat(result.getFacets().getNames()).containsOnly("statuses", "debt");
+ assertThat(result.getFacets().get("statuses")).containsOnly(entry("CLOSED", 20L), entry("OPEN", 10L));
+ assertThat(result.getFacets().get("debt")).containsOnly(entry("total", 30L));
+ }
+
+ @Test
+ public void facets_on_resolutions() {
+ ComponentDto project = ComponentTesting.newProjectDto();
+ ComponentDto file = ComponentTesting.newFileDto(project);
+
+ indexIssues(
+ IssueTesting.newDoc("ISSUE1", file).setResolution(Issue.RESOLUTION_FALSE_POSITIVE),
+ IssueTesting.newDoc("ISSUE2", file).setResolution(Issue.RESOLUTION_FALSE_POSITIVE),
+ IssueTesting.newDoc("ISSUE3", file).setResolution(Issue.RESOLUTION_FIXED));
+
+ SearchResult<IssueDoc> result = index.search(newQueryBuilder().build(), new SearchOptions().addFacets(newArrayList("resolutions")));
+ assertThat(result.getFacets().getNames()).containsOnly("resolutions", "debt");
+ assertThat(result.getFacets().get("resolutions")).containsOnly(entry("FALSE-POSITIVE", 20L), entry("FIXED", 10L));
+ assertThat(result.getFacets().get("debt")).containsOnly(entry("total", 30L));
+ }
+
+ @Test
+ public void facets_on_action_plans() {
+ ComponentDto project = ComponentTesting.newProjectDto();
+ ComponentDto file = ComponentTesting.newFileDto(project);
+
+ indexIssues(
+ IssueTesting.newDoc("ISSUE1", file).setActionPlanKey("plan1"),
+ IssueTesting.newDoc("ISSUE2", file).setActionPlanKey("plan2"));
+
+ SearchResult<IssueDoc> result = index.search(newQueryBuilder().build(), new SearchOptions().addFacets(newArrayList("actionPlans")));
+ assertThat(result.getFacets().getNames()).containsOnly("actionPlans", "debt");
+ assertThat(result.getFacets().get("actionPlans")).containsOnly(entry("plan1", 10L), entry("plan2", 10L));
+ assertThat(result.getFacets().get("debt")).containsOnly(entry("total", 20L));
+ }
+
+ @Test
+ public void facets_on_languages() {
+ ComponentDto project = ComponentTesting.newProjectDto();
+ ComponentDto file = ComponentTesting.newFileDto(project);
+ RuleKey ruleKey = RuleKey.of("repo", "X1");
+
+ indexIssues(IssueTesting.newDoc("ISSUE1", file).setRuleKey(ruleKey.toString()).setLanguage("xoo"));
+
+ SearchResult<IssueDoc> result = index.search(newQueryBuilder().build(), new SearchOptions().addFacets(newArrayList("languages")));
+ assertThat(result.getFacets().getNames()).containsOnly("languages", "debt");
+ assertThat(result.getFacets().get("languages")).containsOnly(entry("xoo", 10L));
+ assertThat(result.getFacets().get("debt")).containsOnly(entry("total", 10L));
+ }
+
+ @Test
+ public void facets_on_assignees() {
+ ComponentDto project = ComponentTesting.newProjectDto();
+ ComponentDto file = ComponentTesting.newFileDto(project);
+
+ indexIssues(
+ IssueTesting.newDoc("ISSUE1", file).setAssignee("steph"),
+ IssueTesting.newDoc("ISSUE2", file).setAssignee("simon"),
+ IssueTesting.newDoc("ISSUE3", file).setAssignee("simon"),
+ IssueTesting.newDoc("ISSUE4", file).setAssignee(null));
+
+ SearchResult<IssueDoc> result = index.search(newQueryBuilder().build(), new SearchOptions().addFacets(newArrayList("assignees")));
+ assertThat(result.getFacets().getNames()).containsOnly("assignees", "debt");
+ assertThat(result.getFacets().get("assignees")).containsOnly(entry("steph", 10L), entry("simon", 20L), entry("", 10L));
+ assertThat(result.getFacets().get("debt")).containsOnly(entry("total", 40L));
+ }
+
+ @Test
+ public void facets_on_authors() {
+ ComponentDto project = ComponentTesting.newProjectDto();
+ ComponentDto file = ComponentTesting.newFileDto(project);
+
+ indexIssues(
+ IssueTesting.newDoc("ISSUE1", file).setAuthorLogin("steph"),
+ IssueTesting.newDoc("ISSUE2", file).setAuthorLogin("simon"),
+ IssueTesting.newDoc("ISSUE3", file).setAuthorLogin("simon"),
+ IssueTesting.newDoc("ISSUE4", file).setAuthorLogin(null));
+
+ SearchResult<IssueDoc> result = index.search(newQueryBuilder().build(), new SearchOptions().addFacets(newArrayList("authors")));
+ assertThat(result.getFacets().getNames()).containsOnly("authors", "debt");
+ assertThat(result.getFacets().get("authors")).containsOnly(entry("steph", 10L), entry("simon", 20L));
+ assertThat(result.getFacets().get("debt")).containsOnly(entry("total", 40L));
+ }
+
+ @Test
+ public void facet_on_created_at() {
+ SearchOptions SearchOptions = fixtureForCreatedAtFacet();
+
+ Map<String, Long> createdAt = index.search(newQueryBuilder()
+ .createdBefore(DateUtils.parseDateTime("2016-01-01T00:00:00+0100")).build(),
+ SearchOptions).getFacets().get("createdAt");
+ assertThat(createdAt).containsOnly(
+ entry("2011-01-01T00:00:00+0000", 10L),
+ entry("2012-01-01T00:00:00+0000", 0L),
+ entry("2013-01-01T00:00:00+0000", 0L),
+ entry("2014-01-01T00:00:00+0000", 50L),
+ entry("2015-01-01T00:00:00+0000", 10L));
+ }
+
+ protected SearchOptions fixtureForCreatedAtFacet() {
+ ComponentDto project = ComponentTesting.newProjectDto();
+ ComponentDto file = ComponentTesting.newFileDto(project);
+
+ IssueDoc issue0 = IssueTesting.newDoc("ISSUE0", file).setFuncCreationDate(DateUtils.parseDateTime("2011-04-25T01:05:13+0100"));
+ IssueDoc issue1 = IssueTesting.newDoc("ISSUE1", file).setFuncCreationDate(DateUtils.parseDateTime("2014-09-01T12:34:56+0100"));
+ IssueDoc issue2 = IssueTesting.newDoc("ISSUE2", file).setFuncCreationDate(DateUtils.parseDateTime("2014-09-01T23:45:60+0100"));
+ IssueDoc issue3 = IssueTesting.newDoc("ISSUE3", file).setFuncCreationDate(DateUtils.parseDateTime("2014-09-02T12:34:56+0100"));
+ IssueDoc issue4 = IssueTesting.newDoc("ISSUE4", file).setFuncCreationDate(DateUtils.parseDateTime("2014-09-05T12:34:56+0100"));
+ IssueDoc issue5 = IssueTesting.newDoc("ISSUE5", file).setFuncCreationDate(DateUtils.parseDateTime("2014-09-20T12:34:56+0100"));
+ IssueDoc issue6 = IssueTesting.newDoc("ISSUE6", file).setFuncCreationDate(DateUtils.parseDateTime("2015-01-18T12:34:56+0100"));
+
+ indexIssues(issue0, issue1, issue2, issue3, issue4, issue5, issue6);
+
+ return new SearchOptions().addFacets("createdAt");
+ }
+
+ private void indexIssues(IssueDoc... issues) {
+ issueIndexer.index(Arrays.asList(issues).iterator());
+ for (IssueDoc issue : issues) {
+ addIssueAuthorization(issue.projectUuid(), DefaultGroups.ANYONE, null);
+ }
+ }
+
+ private void addIssueAuthorization(String projectUuid, @Nullable String group, @Nullable String user) {
+ issueAuthorizationIndexer.index(newArrayList(new IssueAuthorizationDao.Dto(projectUuid, 1).addGroup(group).addUser(user)));
+ }
+
+ private Builder newQueryBuilder() {
+ return IssueQuery.builder(userSessionRule).facetMode(IssueFilterParameters.FACET_MODE_DEBT);
+ }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionMediumTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionMediumTest.java
index b921a2f7de2..85daaa6f4d9 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionMediumTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionMediumTest.java
@@ -95,7 +95,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(40);
}
@Test
@@ -372,6 +372,31 @@ public class SearchActionMediumTest {
}
@Test
+ public void display_facets_in_debt_mode() throws Exception {
+ ComponentDto project = insertComponent(ComponentTesting.newProjectDto("ABCD").setKey("MyProject"));
+ setDefaultProjectPermission(project);
+ ComponentDto file = insertComponent(ComponentTesting.newFileDto(project, "BCDE").setKey("MyComponent"));
+ IssueDto issue = IssueTesting.newDto(newRule(), file, project)
+ .setIssueCreationDate(DateUtils.parseDate("2014-09-04"))
+ .setIssueUpdateDate(DateUtils.parseDate("2017-12-04"))
+ .setDebt(10L)
+ .setStatus("OPEN")
+ .setKee("82fd47d4-b650-4037-80bc-7b112bd4eac2")
+ .setSeverity("MAJOR");
+ db.issueDao().insert(session, issue);
+ session.commit();
+ tester.get(IssueIndexer.class).indexAll();
+
+ userSessionRule.login("john");
+ WsTester.Result result = wsTester.newGetRequest(IssuesWs.API_ENDPOINT, SearchAction.SEARCH_ACTION)
+ .setParam("resolved", "false")
+ .setParam(WebService.Param.FACETS, "statuses,severities,resolutions,projectUuids,rules,fileUuids,assignees,languages,actionPlans")
+ .setParam("facetMode", "debt")
+ .execute();
+ result.assertJson(this.getClass(), "display_facets_debt.json");
+ }
+
+ @Test
public void display_zero_valued_facets_for_selected_items() throws Exception {
ComponentDto project = insertComponent(ComponentTesting.newProjectDto("ABCD").setKey("MyProject"));
setDefaultProjectPermission(project);
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionMediumTest/display_facets_debt.json b/server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionMediumTest/display_facets_debt.json
new file mode 100644
index 00000000000..1917038f24e
--- /dev/null
+++ b/server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionMediumTest/display_facets_debt.json
@@ -0,0 +1,144 @@
+{
+ "debtTotal": 10,
+ "issues": [
+ {
+ "key": "82fd47d4-b650-4037-80bc-7b112bd4eac2",
+ "component": "MyComponent",
+ "project": "MyProject",
+ "rule": "xoo:x1",
+ "status": "OPEN",
+ "severity": "MAJOR",
+ "debt": "10min"
+ }
+ ],
+ "facets": [
+ {
+ "property": "statuses",
+ "values": [
+ {
+ "val": "OPEN",
+ "count": 10
+ },
+ {
+ "val": "CONFIRMED",
+ "count": 0
+ },
+ {
+ "val": "REOPENED",
+ "count": 0
+ },
+ {
+ "val": "RESOLVED",
+ "count": 0
+ },
+ {
+ "val": "CLOSED",
+ "count": 0
+ }
+ ]
+ },
+ {
+ "property": "severities",
+ "values": [
+ {
+ "val": "INFO",
+ "count": 0
+ },
+ {
+ "val": "MINOR",
+ "count": 0
+ },
+ {
+ "val": "MAJOR",
+ "count": 10
+ },
+ {
+ "val": "CRITICAL",
+ "count": 0
+ },
+ {
+ "val": "BLOCKER",
+ "count": 0
+ }
+ ]
+ },
+ {
+ "property": "resolutions",
+ "values": [
+ {
+ "val": "",
+ "count": 10
+ },
+ {
+ "val": "FALSE-POSITIVE",
+ "count": 0
+ },
+ {
+ "val": "FIXED",
+ "count": 0
+ },
+ {
+ "val": "REMOVED",
+ "count": 0
+ },
+ {
+ "val": "WONTFIX",
+ "count": 0
+ }
+ ]
+ },
+ {
+ "property": "projectUuids",
+ "values": [
+ {
+ "val": "ABCD",
+ "count": 10
+ }
+ ]
+ },
+ {
+ "property": "rules",
+ "values": [
+ {
+ "val": "xoo:x1",
+ "count": 10
+ }
+ ]
+ },
+ {
+ "property": "fileUuids",
+ "values": [
+ {
+ "val": "BCDE",
+ "count": 10
+ }
+ ]
+ },
+ {
+ "property": "assignees",
+ "values": [
+ {
+ "val": "",
+ "count": 10
+ }
+ ]
+ },
+ {
+ "property": "languages",
+ "values": [
+ {
+ "val": "xoo",
+ "count": 10
+ }
+ ]
+ },
+ {
+ "property": "actionPlans",
+ "values": [
+ {
+ "val": "",
+ "count": 10
+ }
+ ]
+ }
+ ]}