diff options
7 files changed, 170 insertions, 32 deletions
diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/ws/SearchProjectsAction.java b/server/sonar-server/src/main/java/org/sonar/server/component/ws/SearchProjectsAction.java index 5c68ae9e95b..10b3c78a5ba 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/component/ws/SearchProjectsAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/component/ws/SearchProjectsAction.java @@ -38,6 +38,7 @@ import org.sonarqube.ws.WsComponents.Component; import org.sonarqube.ws.WsComponents.SearchProjectsWsResponse; import org.sonarqube.ws.client.component.SearchProjectsRequest; +import static com.google.common.base.MoreObjects.firstNonNull; import static org.sonar.server.component.ws.SearchProjectsQueryBuilder.SearchProjectsCriteriaQuery; import static org.sonar.server.component.ws.SearchProjectsQueryBuilder.build; import static org.sonar.server.ws.WsUtils.writeProtobuf; @@ -88,12 +89,11 @@ public class SearchProjectsAction implements ComponentsWsAction { } private SearchResults searchProjects(DbSession dbSession, SearchProjectsRequest request) { - String filter = request.getFilter(); - if (filter != null) { - SearchProjectsCriteriaQuery query = build(filter); - searchProjectsQueryBuilderValidator.validate(dbSession, query); - } - SearchIdResult<String> searchResult = index.search(new SearchOptions().setPage(request.getPage(), request.getPageSize())); + String filter = firstNonNull(request.getFilter(), ""); + SearchProjectsCriteriaQuery query = build(filter); + searchProjectsQueryBuilderValidator.validate(dbSession, query); + + SearchIdResult<String> searchResult = index.search(query, new SearchOptions().setPage(request.getPage(), request.getPageSize())); Ordering<ComponentDto> ordering = Ordering.explicit(searchResult.getIds()).onResultOf(ComponentDto::uuid); List<ComponentDto> projects = ordering.immutableSortedCopy(dbClient.componentDao().selectByUuids(dbSession, searchResult.getIds())); diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/ws/SearchProjectsQueryBuilder.java b/server/sonar-server/src/main/java/org/sonar/server/component/ws/SearchProjectsQueryBuilder.java index 381db467ea6..085ad841d1c 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/component/ws/SearchProjectsQueryBuilder.java +++ b/server/sonar-server/src/main/java/org/sonar/server/component/ws/SearchProjectsQueryBuilder.java @@ -25,6 +25,7 @@ import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.apache.commons.lang.StringUtils; import static com.google.common.base.Preconditions.checkArgument; import static java.lang.String.format; @@ -43,7 +44,12 @@ public class SearchProjectsQueryBuilder { } public static SearchProjectsCriteriaQuery build(String filter) { + if (StringUtils.isBlank(filter)) { + return new SearchProjectsCriteriaQuery(); + } + SearchProjectsCriteriaQuery query = new SearchProjectsCriteriaQuery(); + CRITERIA_SPLITTER.split(filter.toLowerCase(ENGLISH)) .forEach(criteria -> processCriteria(criteria, query)); return query; @@ -60,7 +66,7 @@ public class SearchProjectsQueryBuilder { public static class SearchProjectsCriteriaQuery { public enum Operator { - LT("<="), GT(">"), EQ("="); + LTE("<="), GT(">"), EQ("="); String value; @@ -82,7 +88,7 @@ public class SearchProjectsQueryBuilder { private List<MetricCriteria> metricCriterias = new ArrayList<>(); - SearchProjectsCriteriaQuery addMetricCriteria(MetricCriteria metricCriteria) { + public SearchProjectsCriteriaQuery addMetricCriteria(MetricCriteria metricCriteria) { metricCriterias.add(metricCriteria); return this; } @@ -92,11 +98,11 @@ public class SearchProjectsQueryBuilder { } public static class MetricCriteria { - private String metricKey; - private Operator operator; - private double value; + private final String metricKey; + private final Operator operator; + private final double value; - private MetricCriteria(String metricKey, Operator operator, double value) { + public MetricCriteria(String metricKey, Operator operator, double value) { this.metricKey = metricKey; this.operator = operator; this.value = value; diff --git a/server/sonar-server/src/main/java/org/sonar/server/project/es/ProjectMeasuresDoc.java b/server/sonar-server/src/main/java/org/sonar/server/project/es/ProjectMeasuresDoc.java index e650263e76d..284a8bec17c 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/project/es/ProjectMeasuresDoc.java +++ b/server/sonar-server/src/main/java/org/sonar/server/project/es/ProjectMeasuresDoc.java @@ -19,12 +19,16 @@ */ package org.sonar.server.project.es; +import java.util.Collection; import java.util.Date; import java.util.HashMap; +import java.util.Map; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonar.server.es.BaseDoc; +import static org.sonar.server.project.es.ProjectMeasuresIndexDefinition.FIELD_MEASURES; + public class ProjectMeasuresDoc extends BaseDoc { public ProjectMeasuresDoc() { @@ -78,4 +82,13 @@ public class ProjectMeasuresDoc extends BaseDoc { setField(ProjectMeasuresIndexDefinition.FIELD_ANALYSED_AT, d); return this; } + + public Collection<Map<String, Object>> getMeasures() { + return getField(FIELD_MEASURES); + } + + public ProjectMeasuresDoc setMeasures(Collection<Map<String, Object>> measures) { + setField(FIELD_MEASURES, measures); + return this; + } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/project/es/ProjectMeasuresIndex.java b/server/sonar-server/src/main/java/org/sonar/server/project/es/ProjectMeasuresIndex.java index 04486442f25..6eeef117d6f 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/project/es/ProjectMeasuresIndex.java +++ b/server/sonar-server/src/main/java/org/sonar/server/project/es/ProjectMeasuresIndex.java @@ -20,36 +20,71 @@ package org.sonar.server.project.es; import org.elasticsearch.action.search.SearchRequestBuilder; +import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; -import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.sort.SortOrder; +import org.sonar.server.component.ws.SearchProjectsQueryBuilder.SearchProjectsCriteriaQuery; +import org.sonar.server.component.ws.SearchProjectsQueryBuilder.SearchProjectsCriteriaQuery.MetricCriteria; import org.sonar.server.es.BaseIndex; import org.sonar.server.es.EsClient; import org.sonar.server.es.SearchIdResult; import org.sonar.server.es.SearchOptions; +import static org.elasticsearch.index.query.QueryBuilders.boolQuery; +import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; +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.sonar.server.project.es.ProjectMeasuresIndexDefinition.FIELD_MEASURES; +import static org.sonar.server.project.es.ProjectMeasuresIndexDefinition.FIELD_MEASURES_KEY; +import static org.sonar.server.project.es.ProjectMeasuresIndexDefinition.FIELD_MEASURES_VALUE; import static org.sonar.server.project.es.ProjectMeasuresIndexDefinition.FIELD_NAME; import static org.sonar.server.project.es.ProjectMeasuresIndexDefinition.INDEX_PROJECT_MEASURES; import static org.sonar.server.project.es.ProjectMeasuresIndexDefinition.TYPE_PROJECT_MEASURES; public class ProjectMeasuresIndex extends BaseIndex { + private static final String FIELD_KEY = FIELD_MEASURES + "." + FIELD_MEASURES_KEY; + private static final String FIELD_VALUE = FIELD_MEASURES + "." + FIELD_MEASURES_VALUE; + public ProjectMeasuresIndex(EsClient client) { super(client); } - public SearchIdResult<String> search(SearchOptions searchOptions) { - QueryBuilder condition = QueryBuilders.matchAllQuery(); + public SearchIdResult<String> search(SearchProjectsCriteriaQuery query, SearchOptions searchOptions) { + BoolQueryBuilder metricFilters = boolQuery(); + query.getMetricCriterias().stream() + .map(criteria -> nestedQuery(FIELD_MEASURES, boolQuery() + .filter(termQuery(FIELD_KEY, criteria.getMetricKey())) + .filter(toValueQuery(criteria)))) + .forEach(metricFilters::filter); + QueryBuilder esQuery = query.getMetricCriterias().isEmpty() ? matchAllQuery() : metricFilters; SearchRequestBuilder request = getClient() .prepareSearch(INDEX_PROJECT_MEASURES) .setTypes(TYPE_PROJECT_MEASURES) .setFetchSource(false) - .setQuery(condition) + .setQuery(esQuery) .setFrom(searchOptions.getOffset()) .setSize(searchOptions.getLimit()) .addSort(FIELD_NAME + "." + SORT_SUFFIX, SortOrder.ASC); return new SearchIdResult<>(request.get(), id -> id); } + + private static QueryBuilder toValueQuery(MetricCriteria criteria) { + String fieldName = FIELD_VALUE; + + switch (criteria.getOperator()) { + case EQ: + return termQuery(fieldName, criteria.getValue()); + case GT: + return rangeQuery(fieldName).gt(criteria.getValue()); + case LTE: + return rangeQuery(fieldName).lte(criteria.getValue()); + default: + throw new IllegalStateException("Metric criteria non supported: " + criteria.getOperator().name()); + } + + } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/project/es/ProjectMeasuresIndexDefinition.java b/server/sonar-server/src/main/java/org/sonar/server/project/es/ProjectMeasuresIndexDefinition.java index c2f75e8ab62..61eefd12a48 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/project/es/ProjectMeasuresIndexDefinition.java +++ b/server/sonar-server/src/main/java/org/sonar/server/project/es/ProjectMeasuresIndexDefinition.java @@ -30,6 +30,9 @@ public class ProjectMeasuresIndexDefinition implements IndexDefinition { public static final String FIELD_KEY = "key"; public static final String FIELD_NAME = "name"; public static final String FIELD_ANALYSED_AT = "analysedAt"; + public static final String FIELD_MEASURES = "measures"; + public static final String FIELD_MEASURES_KEY = "key"; + public static final String FIELD_MEASURES_VALUE = "value"; private final Settings settings; @@ -47,6 +50,11 @@ public class ProjectMeasuresIndexDefinition implements IndexDefinition { mapping.stringFieldBuilder(FIELD_KEY).disableNorms().build(); mapping.stringFieldBuilder(FIELD_NAME).enableSorting().enableGramSearch().build(); mapping.createDateTimeField(FIELD_ANALYSED_AT); + mapping.nestedFieldBuilder(FIELD_MEASURES) + .addStringFied(FIELD_MEASURES_KEY) + .addStringFied(FIELD_MEASURES_VALUE) + .build(); + // do not store document but only indexation of information mapping.setEnableSource(false); } diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/ws/SearchProjectsQueryBuilderTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/ws/SearchProjectsQueryBuilderTest.java index 1415769ddfe..36aaea35c70 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/component/ws/SearchProjectsQueryBuilderTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/component/ws/SearchProjectsQueryBuilderTest.java @@ -44,7 +44,7 @@ public class SearchProjectsQueryBuilderTest { .extracting(MetricCriteria::getMetricKey, MetricCriteria::getOperator, MetricCriteria::getValue) .containsOnly( tuple("ncloc", Operator.GT, 10d), - tuple("coverage", Operator.LT, 80d)); + tuple("coverage", Operator.LTE, 80d)); } @Test @@ -53,7 +53,7 @@ public class SearchProjectsQueryBuilderTest { .extracting(MetricCriteria::getMetricKey, MetricCriteria::getOperator, MetricCriteria::getValue) .containsOnly( tuple("ncloc", Operator.GT, 10d), - tuple("coverage", Operator.LT, 80d)); + tuple("coverage", Operator.LTE, 80d)); } @Test @@ -64,6 +64,13 @@ public class SearchProjectsQueryBuilderTest { } @Test + public void accept_empty_query() throws Exception { + SearchProjectsCriteriaQuery result = build(""); + + assertThat(result.getMetricCriterias()).isEmpty(); + } + + @Test public void fail_on_unknown_operator() throws Exception { expectedException.expect(IllegalArgumentException.class); expectedException.expectMessage("Unknown operator '>='"); @@ -97,11 +104,4 @@ public class SearchProjectsQueryBuilderTest { expectedException.expectMessage("Invalid criteria 'ncloc >='"); build("ncloc >="); } - - @Test - public void fail_when_no_criteria_provided() throws Exception { - expectedException.expect(IllegalArgumentException.class); - expectedException.expectMessage("Invalid criteria ''"); - build(""); - } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/project/es/ProjectMeasuresIndexTest.java b/server/sonar-server/src/test/java/org/sonar/server/project/es/ProjectMeasuresIndexTest.java index f67c6bbb43c..5dae8f3ce8f 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/project/es/ProjectMeasuresIndexTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/project/es/ProjectMeasuresIndexTest.java @@ -20,33 +20,48 @@ package org.sonar.server.project.es; import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableMap; import java.util.List; +import java.util.Map; import java.util.stream.IntStream; import org.junit.Rule; import org.junit.Test; import org.sonar.api.config.MapSettings; +import org.sonar.server.component.ws.SearchProjectsQueryBuilder.SearchProjectsCriteriaQuery; +import org.sonar.server.component.ws.SearchProjectsQueryBuilder.SearchProjectsCriteriaQuery.MetricCriteria; +import org.sonar.server.component.ws.SearchProjectsQueryBuilder.SearchProjectsCriteriaQuery.Operator; import org.sonar.server.es.EsTester; import org.sonar.server.es.SearchIdResult; import org.sonar.server.es.SearchOptions; +import static com.google.common.collect.Lists.newArrayList; import static org.assertj.core.api.Assertions.assertThat; import static org.sonar.server.project.es.ProjectMeasuresIndexDefinition.INDEX_PROJECT_MEASURES; import static org.sonar.server.project.es.ProjectMeasuresIndexDefinition.TYPE_PROJECT_MEASURES; public class ProjectMeasuresIndexTest { + private static final String COVERAGE = "coverage"; + private static final String NCLOC = "ncloc"; @Rule public EsTester es = new EsTester(new ProjectMeasuresIndexDefinition(new MapSettings())); private ProjectMeasuresIndex underTest = new ProjectMeasuresIndex(es.client()); @Test + public void empty_search() { + List<String> result = underTest.search(new SearchProjectsCriteriaQuery(), new SearchOptions()).getIds(); + + assertThat(result).isEmpty(); + } + + @Test public void search_sort_by_name_case_insensitive() { addDocs(newDoc("P1", "K1", "Windows"), newDoc("P3", "K3", "apachee"), newDoc("P2", "K2", "Apache")); - List<String> result = underTest.search(new SearchOptions()).getIds(); + List<String> result = underTest.search(new SearchProjectsCriteriaQuery(), new SearchOptions()).getIds(); assertThat(result).containsExactly("P2", "P3", "P1"); } @@ -56,17 +71,67 @@ public class ProjectMeasuresIndexTest { IntStream.rangeClosed(1, 9) .forEach(i -> addDocs(newDoc("P" + i, "K" + i, "P" + i))); - SearchIdResult<String> result = underTest.search(new SearchOptions().setPage(2, 3)); + SearchIdResult<String> result = underTest.search(new SearchProjectsCriteriaQuery(), new SearchOptions().setPage(2, 3)); assertThat(result.getIds()).containsExactly("P4", "P5", "P6"); assertThat(result.getTotal()).isEqualTo(9); } - private static ProjectMeasuresDoc newDoc(String uuid, String key, String name) { - return new ProjectMeasuresDoc() - .setId(uuid) - .setKey(key) - .setName(name); + @Test + public void filter_with_lower_than_or_equals() { + addDocs( + newDoc("P1", "K1", "N1").setMeasures(newArrayList(newMeasure(COVERAGE, 79d), newMeasure(NCLOC, 10_000d))), + newDoc("P2", "K2", "N2").setMeasures(newArrayList(newMeasure(COVERAGE, 80d), newMeasure(NCLOC, 10_000d))), + newDoc("P3", "K3", "N3").setMeasures(newArrayList(newMeasure(COVERAGE, 81d), newMeasure(NCLOC, 10_000d)))); + + SearchProjectsCriteriaQuery esQuery = new SearchProjectsCriteriaQuery() + .addMetricCriteria(new MetricCriteria(COVERAGE, Operator.LTE, 80d)); + List<String> result = underTest.search(esQuery, new SearchOptions()).getIds(); + + assertThat(result).containsExactly("P1", "P2"); + } + + @Test + public void filter_with_greater_than() { + addDocs( + newDoc("P1", "K1", "N1").setMeasures(newArrayList(newMeasure(COVERAGE, 80d), newMeasure(NCLOC, 10_000d))), + newDoc("P2", "K2", "N2").setMeasures(newArrayList(newMeasure(COVERAGE, 80d), newMeasure(NCLOC, 10_001d))), + newDoc("P3", "K3", "N3").setMeasures(newArrayList(newMeasure(COVERAGE, 80d), newMeasure(NCLOC, 10_001d)))); + + SearchProjectsCriteriaQuery esQuery = new SearchProjectsCriteriaQuery() + .addMetricCriteria(new MetricCriteria(NCLOC, Operator.GT, 10_000d)); + List<String> result = underTest.search(esQuery, new SearchOptions()).getIds(); + + assertThat(result).containsExactly("P2", "P3"); + } + + @Test + public void filter_with_equals() { + addDocs( + newDoc("P1", "K1", "N1").setMeasures(newArrayList(newMeasure(COVERAGE, 79d), newMeasure(NCLOC, 10_000d))), + newDoc("P2", "K2", "N2").setMeasures(newArrayList(newMeasure(COVERAGE, 80d), newMeasure(NCLOC, 10_000d))), + newDoc("P3", "K3", "N3").setMeasures(newArrayList(newMeasure(COVERAGE, 81d), newMeasure(NCLOC, 10_000d)))); + + SearchProjectsCriteriaQuery esQuery = new SearchProjectsCriteriaQuery() + .addMetricCriteria(new MetricCriteria(COVERAGE, Operator.EQ, 80d)); + List<String> result = underTest.search(esQuery, new SearchOptions()).getIds(); + + assertThat(result).containsExactly("P2"); + } + + @Test + public void filter_on_several_metrics() { + addDocs( + newDoc("P1", "K1", "N1").setMeasures(newArrayList(newMeasure(COVERAGE, 81d), newMeasure(NCLOC, 10_001d))), + newDoc("P2", "K2", "N2").setMeasures(newArrayList(newMeasure(COVERAGE, 80d), newMeasure(NCLOC, 10_001d))), + newDoc("P3", "K3", "N3").setMeasures(newArrayList(newMeasure(COVERAGE, 79d), newMeasure(NCLOC, 10_000d)))); + + SearchProjectsCriteriaQuery esQuery = new SearchProjectsCriteriaQuery() + .addMetricCriteria(new MetricCriteria(COVERAGE, Operator.LTE, 80d)) + .addMetricCriteria(new MetricCriteria(NCLOC, Operator.GT, 10_000d)); + List<String> result = underTest.search(esQuery, new SearchOptions()).getIds(); + + assertThat(result).containsExactly("P2"); } private void addDocs(ProjectMeasuresDoc... docs) { @@ -76,4 +141,15 @@ public class ProjectMeasuresIndexTest { Throwables.propagate(e); } } + + private static ProjectMeasuresDoc newDoc(String uuid, String key, String name) { + return new ProjectMeasuresDoc() + .setId(uuid) + .setKey(key) + .setName(name); + } + + private Map<String, Object> newMeasure(String key, double value) { + return ImmutableMap.of("key", key, "value", value); + } } |