import org.elasticsearch.action.search.SearchRequestBuilder;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
+import org.elasticsearch.search.aggregations.AggregationBuilder;
+import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.sonar.server.component.es.ProjectMeasuresQuery.MetricCriterion;
import static org.elasticsearch.index.query.QueryBuilders.nestedQuery;
import static org.elasticsearch.index.query.QueryBuilders.rangeQuery;
import static org.elasticsearch.index.query.QueryBuilders.termQuery;
+import static org.elasticsearch.index.query.QueryBuilders.termsQuery;
+import static org.sonar.api.measures.CoreMetrics.NCLOC_KEY;
import static org.sonar.server.component.es.ProjectMeasuresIndexDefinition.FIELD_MEASURES;
import static org.sonar.server.component.es.ProjectMeasuresIndexDefinition.FIELD_MEASURES_KEY;
import static org.sonar.server.component.es.ProjectMeasuresIndexDefinition.FIELD_MEASURES_VALUE;
public SearchIdResult<String> search(ProjectMeasuresQuery query, SearchOptions searchOptions) {
QueryBuilder esQuery = createEsQuery(query);
+ AggregationBuilder locAggregation = AggregationBuilders.nested("nested_" + NCLOC_KEY)
+ .path("measures")
+ .subAggregation(
+ AggregationBuilders.filter("filter_" + NCLOC_KEY)
+ .filter(termsQuery("measures.key", NCLOC_KEY))
+ .subAggregation(AggregationBuilders.range(NCLOC_KEY)
+ .field("measures.value")
+ .addUnboundedTo(1_000d)
+ .addRange(1_000d, 10_000d)
+ .addRange(10_000d, 100_000d)
+ .addRange(100_000d, 500_000d)
+ .addUnboundedFrom(500_000)));
+
SearchRequestBuilder request = getClient()
.prepareSearch(INDEX_PROJECT_MEASURES)
.setTypes(TYPE_PROJECT_MEASURES)
.setQuery(esQuery)
.setFrom(searchOptions.getOffset())
.setSize(searchOptions.getLimit())
+ .addAggregation(locAggregation)
.addSort(FIELD_NAME + "." + SORT_SUFFIX, SortOrder.ASC);
return new SearchIdResult<>(request.get(), id -> id);
package org.sonar.server.component.ws;
import com.google.common.collect.Ordering;
+import java.util.Collections;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
import java.util.function.Function;
import java.util.stream.Stream;
import org.sonar.api.server.ws.Request;
import org.sonar.db.component.ComponentDto;
import org.sonar.server.component.es.ProjectMeasuresIndex;
import org.sonar.server.component.es.ProjectMeasuresQuery;
+import org.sonar.server.es.Facets;
import org.sonar.server.es.SearchIdResult;
import org.sonar.server.es.SearchOptions;
import org.sonarqube.ws.Common;
Ordering<ComponentDto> ordering = Ordering.explicit(searchResult.getIds()).onResultOf(ComponentDto::uuid);
List<ComponentDto> projects = ordering.immutableSortedCopy(dbClient.componentDao().selectByUuids(dbSession, searchResult.getIds()));
- return new SearchResults(projects, searchResult.getTotal());
+ return new SearchResults(projects, searchResult.getFacets(), searchResult.getTotal());
}
private static SearchProjectsRequest toRequest(Request httpRequest) {
.forEach(response::addComponents);
return response;
})
+ .map(response -> addFacets(searchResults.facets, response))
.map(SearchProjectsWsResponse.Builder::build)
.findFirst()
.orElseThrow(() -> new IllegalStateException("SearchProjectsWsResponse not built"));
}
+ private static SearchProjectsWsResponse.Builder addFacets(Facets facets, SearchProjectsWsResponse.Builder wsResponse) {
+ Common.Facets.Builder wsFacets = Common.Facets.newBuilder();
+ Common.Facet.Builder wsFacet = Common.Facet.newBuilder();
+ for (Map.Entry<String, LinkedHashMap<String, Long>> facet : facets.getAll().entrySet()) {
+ wsFacet.clear();
+ wsFacet.setProperty(facet.getKey());
+ LinkedHashMap<String, Long> buckets = facet.getValue();
+ if (buckets != null) {
+ for (Map.Entry<String, Long> bucket : buckets.entrySet()) {
+ Common.FacetValue.Builder valueBuilder = wsFacet.addValuesBuilder();
+ valueBuilder.setVal(bucket.getKey());
+ valueBuilder.setCount(bucket.getValue());
+ valueBuilder.build();
+ }
+ } else {
+ wsFacet.addAllValues(Collections.<Common.FacetValue>emptyList());
+ }
+ wsFacets.addFacets(wsFacet);
+ }
+ wsResponse.setFacets(wsFacets);
+
+ return wsResponse;
+ }
+
private static class DbToWsComponent implements Function<ComponentDto, Component> {
private final Component.Builder wsComponent;
private static class SearchResults {
private final List<ComponentDto> projects;
+ private final Facets facets;
private final int total;
- private SearchResults(List<ComponentDto> projects, long total) {
+ private SearchResults(List<ComponentDto> projects, Facets facets, long total) {
this.projects = projects;
this.total = (int) total;
+ this.facets = facets;
}
}
}
import org.elasticsearch.search.aggregations.HasAggregations;
import org.elasticsearch.search.aggregations.bucket.histogram.Histogram;
import org.elasticsearch.search.aggregations.bucket.missing.Missing;
+import org.elasticsearch.search.aggregations.bucket.range.Range;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.metrics.sum.Sum;
processDateHistogram((Histogram) aggregation);
} else if (Sum.class.isAssignableFrom(aggregation.getClass())) {
processSum((Sum) aggregation);
+ } else if (Range.class.isAssignableFrom(aggregation.getClass())) {
+ processRange((Range) aggregation);
} else {
throw new IllegalArgumentException("Aggregation type not supported yet: " + aggregation.getClass());
}
getOrCreateFacet(aggregation.getName()).put(TOTAL, Math.round(aggregation.getValue()));
}
+ private void processRange(Range aggregation) {
+ LinkedHashMap<String, Long> facet = getOrCreateFacet(aggregation.getName());
+ aggregation.getBuckets().forEach(bucket -> facet.put(bucket.getKeyAsString(), bucket.getDocCount()));
+ }
+
public boolean contains(String facetName) {
return facetsByName.containsKey(facetName);
}
import org.sonar.server.component.es.ProjectMeasuresQuery.MetricCriterion;
import org.sonar.server.component.es.ProjectMeasuresQuery.Operator;
import org.sonar.server.es.EsTester;
+import org.sonar.server.es.Facets;
import org.sonar.server.es.SearchIdResult;
import org.sonar.server.es.SearchOptions;
import org.sonar.server.permission.index.PermissionIndexerTester;
import static com.google.common.collect.Lists.newArrayList;
import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.entry;
import static org.sonar.api.measures.Metric.Level.OK;
import static org.sonar.api.security.DefaultGroups.ANYONE;
import static org.sonar.server.component.es.ProjectMeasuresIndexDefinition.INDEX_PROJECT_MEASURES;
assertThat(result).containsOnly("P1");
}
+ @Test
+ public void facet_ncloc() {
+ addDocs(
+ // 3 docs with ncloc<1K
+ newDoc("P11", "K1", "N1").setMeasures(newArrayList(newMeasure(NCLOC, 0d))),
+ newDoc("P12", "K1", "N1").setMeasures(newArrayList(newMeasure(NCLOC, 0d))),
+ newDoc("P13", "K1", "N1").setMeasures(newArrayList(newMeasure(NCLOC, 999d))),
+ // 2 docs with ncloc>=1K and ncloc<10K
+ newDoc("P21", "K2", "N2").setMeasures(newArrayList(newMeasure(NCLOC, 1_000d))),
+ newDoc("P22", "K2", "N2").setMeasures(newArrayList(newMeasure(NCLOC, 9_999d))),
+ // 4 docs with ncloc>=10K and ncloc<100K
+ newDoc("P31", "K3", "N3").setMeasures(newArrayList(newMeasure(NCLOC, 10_000d))),
+ newDoc("P32", "K3", "N3").setMeasures(newArrayList(newMeasure(NCLOC, 10_000d))),
+ newDoc("P33", "K3", "N3").setMeasures(newArrayList(newMeasure(NCLOC, 11_000d))),
+ newDoc("P34", "K3", "N3").setMeasures(newArrayList(newMeasure(NCLOC, 99_000d))),
+ // 2 docs with ncloc>=100K and ncloc<500K
+ newDoc("P41", "K3", "N3").setMeasures(newArrayList(newMeasure(NCLOC, 100_000d))),
+ newDoc("P42", "K3", "N3").setMeasures(newArrayList(newMeasure(NCLOC, 499_000d))),
+ // 5 docs with ncloc>= 500K
+ newDoc("P51", "K3", "N3").setMeasures(newArrayList(newMeasure(NCLOC, 500_000d))),
+ newDoc("P52", "K3", "N3").setMeasures(newArrayList(newMeasure(NCLOC, 100_000_000d))),
+ newDoc("P53", "K3", "N3").setMeasures(newArrayList(newMeasure(NCLOC, 500_000d))),
+ newDoc("P54", "K3", "N3").setMeasures(newArrayList(newMeasure(NCLOC, 1_000_000d))),
+ newDoc("P55", "K3", "N3").setMeasures(newArrayList(newMeasure(NCLOC, 100_000_000_000d)))
+ );
+
+ Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions()).getFacets();
+
+ assertThat(facets.get(NCLOC)).containsExactly(
+ entry("*-1000.0", 3L),
+ entry("1000.0-10000.0", 2L),
+ entry("10000.0-100000.0", 4L),
+ entry("100000.0-500000.0", 2L),
+ entry("500000.0-*", 5L)
+ );
+ }
+
private void addDocs(ProjectMeasuresDoc... docs) {
addDocs(null, ANYONE, docs);
}
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
import static org.sonar.db.component.ComponentTesting.newDeveloper;
import static org.sonar.db.component.ComponentTesting.newDirectory;
import static org.sonar.db.component.ComponentTesting.newFileDto;
import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_FILTER;
public class SearchProjectsActionTest {
+ private static final String NCLOC = "ncloc";
+ private static final String COVERAGE = "coverage";
@Rule
public ExpectedException expectedException = ExpectedException.none();
@Test
public void filter_projects_with_query() {
- insertProjectInDbAndEs(newProjectDto().setName("Sonar Java"), newArrayList(newMeasure("coverage", 81), newMeasure("ncloc", 10_000d)));
- insertProjectInDbAndEs(newProjectDto().setName("Sonar Markdown"), newArrayList(newMeasure("coverage", 80d), newMeasure("ncloc", 10_000d)));
- insertProjectInDbAndEs(newProjectDto().setName("Sonar Qube"), newArrayList(newMeasure("coverage", 80d), newMeasure("ncloc", 10_001d)));
+ insertProjectInDbAndEs(newProjectDto().setName("Sonar Java"), newArrayList(newMeasure(COVERAGE, 81), newMeasure(NCLOC, 10_000d)));
+ insertProjectInDbAndEs(newProjectDto().setName("Sonar Markdown"), newArrayList(newMeasure(COVERAGE, 80d), newMeasure(NCLOC, 10_000d)));
+ insertProjectInDbAndEs(newProjectDto().setName("Sonar Qube"), newArrayList(newMeasure(COVERAGE, 80d), newMeasure(NCLOC, 10_001d)));
request.setFilter("coverage <= 80 and ncloc <= 10000");
- dbClient.metricDao().insert(dbSession, newMetricDto().setKey("coverage").setValueType(Metric.ValueType.FLOAT.name()));
- dbClient.metricDao().insert(dbSession, newMetricDto().setKey("ncloc").setValueType(Metric.ValueType.FLOAT.name()));
+ dbClient.metricDao().insert(dbSession, newMetricDto().setKey(COVERAGE).setValueType(Metric.ValueType.FLOAT.name()));
+ dbClient.metricDao().insert(dbSession, newMetricDto().setKey(NCLOC).setValueType(Metric.ValueType.FLOAT.name()));
db.commit();
SearchProjectsWsResponse result = call(request);
assertThat(result.getComponents(0).getName()).isEqualTo("Sonar Markdown");
}
+ @Test
+ public void return_loc_facet() {
+ insertProjectInDbAndEs(newProjectDto().setName("Sonar Java"), newArrayList(newMeasure(COVERAGE, 81), newMeasure(NCLOC, 5d)));
+ insertProjectInDbAndEs(newProjectDto().setName("Sonar Groovy"), newArrayList(newMeasure(COVERAGE, 81), newMeasure(NCLOC, 5d)));
+ insertProjectInDbAndEs(newProjectDto().setName("Sonar Markdown"), newArrayList(newMeasure(COVERAGE, 80d), newMeasure(NCLOC, 10_000d)));
+ insertProjectInDbAndEs(newProjectDto().setName("Sonar Qube"), newArrayList(newMeasure(COVERAGE, 80d), newMeasure(NCLOC, 500_001d)));
+ dbClient.metricDao().insert(dbSession, newMetricDto().setKey(COVERAGE).setValueType(Metric.ValueType.FLOAT.name()));
+ dbClient.metricDao().insert(dbSession, newMetricDto().setKey(NCLOC).setValueType(Metric.ValueType.FLOAT.name()));
+ db.commit();
+
+ SearchProjectsWsResponse result = call(request);
+
+ assertThat(result.getFacets().getFacetsCount()).isEqualTo(1);
+ Common.Facet facet = result.getFacets().getFacets(0);
+ assertThat(facet.getProperty()).isEqualTo(NCLOC);
+ assertThat(facet.getValuesCount()).isEqualTo(5);
+ assertThat(facet.getValuesList())
+ .extracting(Common.FacetValue::getVal, Common.FacetValue::getCount)
+ .containsExactly(
+ tuple("*-1000.0", 2L),
+ tuple("1000.0-10000.0", 0L),
+ tuple("10000.0-100000.0", 1L),
+ tuple("100000.0-500000.0", 0L),
+ tuple("500000.0-*", 1L));
+ }
+
@Test
public void fail_if_metric_is_unknown() {
expectedException.expect(IllegalArgumentException.class);
message SearchProjectsWsResponse {
optional sonarqube.ws.commons.Paging paging = 1;
repeated Component components = 2;
+ optional sonarqube.ws.commons.Facets facets = 3;
}
message Component {