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 a22131e18f33..9bc8519608e8 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.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> facetsByName = new LinkedHashMap<>(); public Facets(SearchResponse response) { @@ -59,6 +61,8 @@ private void processAggregation(Aggregation aggregation) { 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 @@ private void processAggregation(Aggregation aggregation) { private void processMissingAggregation(Missing aggregation) { long docCount = aggregation.getDocCount(); if (docCount > 0L) { - getOrCreateFacet(aggregation.getName().replace("_missing", "")).put("", docCount); + LinkedHashMap 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 @@ private void processTermsAggregation(Terms aggregation) { facetName = facetName.replace("_selected", ""); LinkedHashMap 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 @@ private void processSubAggregations(HasAggregations aggregation) { private void processDateHistogram(DateHistogram aggregation) { LinkedHashMap 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 bcb2c9654af2..ee4a1ce1d174 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 userGroups; @@ -121,6 +122,7 @@ private IssueQuery(Builder builder) { this.userLogin = builder.userLogin; this.userGroups = builder.userGroups; this.checkAuthorization = builder.checkAuthorization; + this.facetMode = builder.facetMode; } public Collection issueKeys() { @@ -269,6 +271,10 @@ public boolean checkAuthorization() { return checkAuthorization; } + public String facetMode() { + return facetMode; + } + @Override public String toString() { return ReflectionToStringBuilder.toString(this); @@ -311,6 +317,7 @@ public static class Builder { private String userLogin; private Set userGroups; private boolean checkAuthorization = true; + private String facetMode; private Builder(UserSession userSession) { this.userLogin = userSession.getLogin(); @@ -518,6 +525,10 @@ public Builder checkAuthorization(boolean checkAuthorization) { return this; } + public Builder facetMode(String facetMode) { + this.facetMode = facetMode; + return this; + } } private static Collection defaultCollection(@Nullable Collection 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 38f40ab43db1..134a9414a4bf 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 IssueQuery createFromMap(Map params) { 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 IssueQuery createFromRequest(Request request) { .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 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 96b5589eb160..3f1b221d4ec9 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 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 ALL_WITHOUT_PAGINATION = newArrayList(Iterables.filter(ALL, new Predicate() { 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 b73dc7a7a0b5..98650655de5b 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.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 @@ */ public class IssueIndex extends BaseIndex { + private static final String SUBSTRING_MATCH_REGEXP = ".*%s.*"; public static final List 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 @@ private void validateCreationDateBounds(Date createdBefore, Date createdAfter) { private void configureStickyFacets(IssueQuery query, SearchOptions options, Map 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 @@ private void configureStickyFacets(IssueQuery query, SearchOptions options, Map< } 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 @@ private void configureStickyFacets(IssueQuery query, SearchOptions options, Map< esSearch.addAggregation(getCreatedAtFacet(query, filters, esQuery)); } } + + if (IssueFilterParameters.FACET_MODE_DEBT.equals(query.facetMode())) { + esSearch.addAggregation(DEBT_AGGREGATION); + } + } + + private StickyFacetBuilder newStickyFacetBuilder(IssueQuery query, Map 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 @@ private void addSimpleStickyFacetIfNeeded(SearchOptions options, StickyFacetBuil } } + 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 filters, QueryBuilder esQuery) { long now = system.now(); @@ -445,7 +473,7 @@ private AggregationBuilder getCreatedAtFacet(IssueQuery query, Map filters, QueryBuilder esQuery) { @@ -490,7 +520,7 @@ private AggregationBuilder createAssigneesFacet(IssueQuery query, Map 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 @@ private AggregationBuilder createAssigneesFacet(IssueQuery query, Map filters, QueryBuilder esQuery) { + private AggregationBuilder createResolutionFacet(IssueQuery query, Map filters, QueryBuilder esQuery) { String fieldName = IssueIndexDefinition.FIELD_ISSUE_RESOLUTION; String facetName = IssueFilterParameters.RESOLUTIONS; @@ -552,16 +582,16 @@ private AggregationBuilder createResolutionFacet(Map filt Map 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 @@ private AggregationBuilder createActionPlansFacet(IssueQuery query, Map 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 512de33d3ef3..3cc0cbe5a7f3 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.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 void define(WebService.NewController controller) { 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 @@ private SearchResult execute(IssueQuery query, SearchOptions options) } private void writeResponse(Request request, SearchResult 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 issueKeys = newArrayList(); Set ruleKeys = newHashSet(); Set 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 589536f14844..5bbf441c7a4e 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 filters; + private final AbstractAggregationBuilder subAggregation; + private final Order order; public StickyFacetBuilder(QueryBuilder query, Map filters) { + this(query, filters, null, FACET_DEFAULT_ORDER); + } + + public StickyFacetBuilder(QueryBuilder query, Map 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 BoolFilterBuilder getStickyFacetFilter(String... fieldNames) { } 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 000000000000..d1962713ec35 --- /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 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 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 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 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 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 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 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 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 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 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 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 b921a2f7de28..85daaa6f4d9d 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 void define_action() { assertThat(show.isPost()).isFalse(); assertThat(show.isInternal()).isFalse(); assertThat(show.responseExampleAsString()).isNotEmpty(); - assertThat(show.params()).hasSize(39); + assertThat(show.params()).hasSize(40); } @Test @@ -371,6 +371,31 @@ public void display_facets() throws Exception { result.assertJson(this.getClass(), "display_facets.json"); } + @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")); 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 000000000000..1917038f24e1 --- /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 + } + ] + } + ]}