From 29fa51a5b900a29a5f7384c265d1ffe9c9140455 Mon Sep 17 00:00:00 2001 From: Julien Lancelot Date: Tue, 21 Feb 2017 10:01:15 +0100 Subject: [PATCH] SONAR-8795 Search by text query --- .../server/component/ws/FilterParser.java | 21 ++++++++++- .../ws/ProjectMeasuresQueryFactory.java | 23 ++++++++++-- .../measure/index/ProjectMeasuresIndex.java | 19 +++++++++- .../index/ProjectMeasuresIndexDefinition.java | 5 ++- .../measure/index/ProjectMeasuresQuery.java | 10 +++++ .../server/component/ws/FilterParserTest.java | 25 ++++++++++++- .../ws/ProjectMeasuresQueryFactoryTest.java | 37 +++++++++++++++++++ .../ws/SearchProjectsActionTest.java | 13 +++++++ .../index/ProjectMeasuresIndexTest.java | 13 +++++++ 9 files changed, 156 insertions(+), 10 deletions(-) diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/ws/FilterParser.java b/server/sonar-server/src/main/java/org/sonar/server/component/ws/FilterParser.java index caa60c55b5a..f65bfeff006 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/component/ws/FilterParser.java +++ b/server/sonar-server/src/main/java/org/sonar/server/component/ws/FilterParser.java @@ -37,12 +37,18 @@ import static java.util.Objects.requireNonNull; public class FilterParser { + private static final String DOUBLE_QUOTES = "\""; + private static final Splitter CRITERIA_SPLITTER = Splitter.on(Pattern.compile("and", Pattern.CASE_INSENSITIVE)); private static final Splitter IN_VALUES_SPLITTER = Splitter.on(",").omitEmptyStrings().trimResults(); - private static final Pattern PATTERN = Pattern.compile("(\\w+)\\s*([<>]?[=]?)\\s*(\\S*)", Pattern.CASE_INSENSITIVE); + private static final Pattern PATTERN = Pattern.compile("(\\w+)\\s*([<>]?[=]?)\\s*(.*)", Pattern.CASE_INSENSITIVE); private static final Pattern PATTERN_HAVING_VALUES = Pattern.compile("(\\w+)\\s+(in)\\s+\\((.*)\\)", Pattern.CASE_INSENSITIVE); + private FilterParser(){ + // Only static methods + } + public static List parse(String filter) { return StreamSupport.stream(CRITERIA_SPLITTER.split(filter.trim()).spliterator(), false) .filter(Objects::nonNull) @@ -80,7 +86,7 @@ public class FilterParser { String value = matcher.group(3); if (!isNullOrEmpty(operatorValue) && !isNullOrEmpty(value)) { builder.setOperator(Operator.getByValue(operatorValue)); - builder.setValue(value); + builder.setValue(sanitizeValue(value)); } return builder.build(); } @@ -98,6 +104,17 @@ public class FilterParser { return builder.build(); } + @CheckForNull + private static String sanitizeValue(@Nullable String value) { + if (value == null) { + return null; + } + if (value.length() > 2 && value.startsWith(DOUBLE_QUOTES) && value.endsWith(DOUBLE_QUOTES)) { + return value.substring(1, value.length() - 1); + } + return value; + } + public static class Criterion { private final String key; private final Operator operator; diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/ws/ProjectMeasuresQueryFactory.java b/server/sonar-server/src/main/java/org/sonar/server/component/ws/ProjectMeasuresQueryFactory.java index aff52a9f99a..dc2e9330b1e 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/component/ws/ProjectMeasuresQueryFactory.java +++ b/server/sonar-server/src/main/java/org/sonar/server/component/ws/ProjectMeasuresQueryFactory.java @@ -42,6 +42,7 @@ import static org.sonarqube.ws.client.project.ProjectsWsParameters.FILTER_LANGUA class ProjectMeasuresQueryFactory { public static final String IS_FAVORITE_CRITERION = "isFavorite"; + public static final String QUERY_KEY = "query"; private ProjectMeasuresQueryFactory() { // prevent instantiation @@ -67,6 +68,11 @@ class ProjectMeasuresQueryFactory { return; } + if (QUERY_KEY.equalsIgnoreCase(key)) { + processQuery(criterion, query); + return; + } + String value = criterion.getValue(); checkArgument(value != null, "Value cannot be null for '%s'", key); if (ALERT_STATUS_KEY.equals(key)) { @@ -82,11 +88,21 @@ class ProjectMeasuresQueryFactory { List values = criterion.getValues(); if (value != null && EQ.equals(operator)) { query.setLanguages(singleton(value)); - } else if (!values.isEmpty() && IN.equals(operator)) { + return; + } + if (!values.isEmpty() && IN.equals(operator)) { query.setLanguages(new HashSet<>(values)); - } else { - throw new IllegalArgumentException("Language should be set either by using 'language = java' or 'language IN (java, js)'"); + return; } + throw new IllegalArgumentException("Language should be set either by using 'language = java' or 'language IN (java, js)'"); + } + + private static void processQuery(FilterParser.Criterion criterion, ProjectMeasuresQuery query) { + Operator operatorValue = criterion.getOperator(); + String value = criterion.getValue(); + checkArgument(value != null, "Query is invalid"); + checkArgument(EQ.equals(operatorValue), "Query should only be used with equals operator"); + query.setQueryText(value); } private static void processQualityGateStatus(FilterParser.Criterion criterion, ProjectMeasuresQuery query) { @@ -106,5 +122,4 @@ class ProjectMeasuresQueryFactory { } } - } diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndex.java b/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndex.java index 8571ab16f17..8938eabc7d2 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndex.java +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndex.java @@ -26,6 +26,7 @@ import com.google.common.collect.Multimap; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.IntStream; import org.elasticsearch.action.search.SearchRequestBuilder; import org.elasticsearch.index.query.BoolQueryBuilder; @@ -41,6 +42,8 @@ import org.sonar.server.es.EsClient; import org.sonar.server.es.SearchIdResult; import org.sonar.server.es.SearchOptions; import org.sonar.server.es.StickyFacetBuilder; +import org.sonar.server.es.textsearch.ComponentTextSearchFeature; +import org.sonar.server.es.textsearch.ComponentTextSearchQueryFactory; import org.sonar.server.measure.index.ProjectMeasuresQuery.MetricCriterion; import org.sonar.server.permission.index.AuthorizationTypeSupport; @@ -242,7 +245,6 @@ public class ProjectMeasuresIndex extends BaseIndex { .filter(toValueQuery(criterion)))) .forEach(metricFilters::must); filters.put(entry.getKey(), metricFilters); - }); query.getQualityGateStatus() @@ -257,9 +259,24 @@ public class ProjectMeasuresIndex extends BaseIndex { query.getOrganizationUuid() .ifPresent(organizationUuid -> filters.put(FIELD_ORGANIZATION_UUID, termQuery(FIELD_ORGANIZATION_UUID, organizationUuid))); + + createTextQueryFilter(query).ifPresent(queryBuilder -> filters.put("textQuery", queryBuilder)); return filters; } + private static Optional createTextQueryFilter(ProjectMeasuresQuery query) { + Optional queryText = query.getQueryText(); + if (!queryText.isPresent()) { + return Optional.empty(); + } + ComponentTextSearchQueryFactory.ComponentTextSearchQuery componentTextSearchQuery = ComponentTextSearchQueryFactory.ComponentTextSearchQuery.builder() + .setQueryText(queryText.get()) + .setFieldKey(FIELD_KEY) + .setFieldName(FIELD_NAME) + .build(); + return Optional.of(ComponentTextSearchQueryFactory.createQuery(componentTextSearchQuery, ComponentTextSearchFeature.values())); + } + private static QueryBuilder toValueQuery(MetricCriterion criterion) { String fieldName = FIELD_MEASURES_VALUE; diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndexDefinition.java b/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndexDefinition.java index 5a3558b127a..9e85ab83a0e 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndexDefinition.java +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndexDefinition.java @@ -23,6 +23,7 @@ import org.sonar.api.config.Settings; import org.sonar.server.es.IndexDefinition; import org.sonar.server.es.NewIndex; +import static org.sonar.server.es.DefaultIndexSettingsElement.SEARCH_GRAMS_ANALYZER; import static org.sonar.server.es.DefaultIndexSettingsElement.SORTABLE_ANALYZER; public class ProjectMeasuresIndexDefinition implements IndexDefinition { @@ -58,8 +59,8 @@ public class ProjectMeasuresIndexDefinition implements IndexDefinition { .requireProjectAuthorization(); mapping.stringFieldBuilder(FIELD_ORGANIZATION_UUID).build(); - mapping.stringFieldBuilder(FIELD_KEY).disableNorms().build(); - mapping.stringFieldBuilder(FIELD_NAME).addSubFields(SORTABLE_ANALYZER).build(); + mapping.stringFieldBuilder(FIELD_KEY).disableNorms().addSubFields(SORTABLE_ANALYZER).build(); + mapping.stringFieldBuilder(FIELD_NAME).addSubFields(SORTABLE_ANALYZER, SEARCH_GRAMS_ANALYZER).build(); mapping.stringFieldBuilder(FIELD_QUALITY_GATE_STATUS).build(); mapping.createDateTimeField(FIELD_ANALYSED_AT); mapping.nestedFieldBuilder(FIELD_MEASURES) diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresQuery.java b/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresQuery.java index 2bc12465beb..0b484c78ce2 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresQuery.java +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresQuery.java @@ -40,6 +40,7 @@ public class ProjectMeasuresQuery { private Set languages; private String sort = SORT_BY_NAME; private boolean asc = true; + private String queryText; public ProjectMeasuresQuery addMetricCriterion(MetricCriterion metricCriterion) { this.metricCriteria.add(metricCriterion); @@ -86,6 +87,15 @@ public class ProjectMeasuresQuery { return Optional.ofNullable(languages); } + public Optional getQueryText() { + return Optional.ofNullable(queryText); + } + + public ProjectMeasuresQuery setQueryText(@Nullable String queryText) { + this.queryText = queryText; + return this; + } + public String getSort() { return sort; } diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/ws/FilterParserTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/ws/FilterParserTest.java index 09c8e90120f..27a429f8fe4 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/component/ws/FilterParserTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/component/ws/FilterParserTest.java @@ -133,7 +133,7 @@ public class FilterParserTest { .containsOnly( tuple("ncloc", GT, "10", emptyList()), tuple("coverage", LTE, "80", emptyList()), - tuple("language", IN, null, asList("java", "js"))); + tuple("language", IN, null, asList("java", "js"))); } @Test @@ -151,6 +151,29 @@ public class FilterParserTest { tuple("language", IN, null, asList("java", "js"))); } + @Test + public void parse_filter_starting_and_ending_with_double_quotes() throws Exception { + assertThat(FilterParser.parse("q = \"Sonar Qube\"")) + .extracting(Criterion::getKey, Criterion::getOperator, Criterion::getValue) + .containsOnly( + tuple("q", EQ, "Sonar Qube")); + + assertThat(FilterParser.parse("q = \"Sonar\"Qube\"")) + .extracting(Criterion::getKey, Criterion::getOperator, Criterion::getValue) + .containsOnly( + tuple("q", EQ, "Sonar\"Qube")); + + assertThat(FilterParser.parse("q = Sonar\"Qube")) + .extracting(Criterion::getKey, Criterion::getOperator, Criterion::getValue) + .containsOnly( + tuple("q", EQ, "Sonar\"Qube")); + + assertThat(FilterParser.parse("q=\"Sonar Qube\"")) + .extracting(Criterion::getKey, Criterion::getOperator, Criterion::getValue) + .containsOnly( + tuple("q", EQ, "Sonar Qube")); + } + @Test public void accept_empty_query() throws Exception { List criterion = FilterParser.parse(""); diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/ws/ProjectMeasuresQueryFactoryTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/ws/ProjectMeasuresQueryFactoryTest.java index 79fd62ac2e6..215949fba57 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/component/ws/ProjectMeasuresQueryFactoryTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/component/ws/ProjectMeasuresQueryFactoryTest.java @@ -39,6 +39,7 @@ import static org.sonar.server.component.ws.FilterParser.Operator; import static org.sonar.server.component.ws.FilterParser.Operator.EQ; import static org.sonar.server.component.ws.FilterParser.Operator.GT; import static org.sonar.server.component.ws.FilterParser.Operator.IN; +import static org.sonar.server.component.ws.FilterParser.Operator.LT; import static org.sonar.server.component.ws.FilterParser.Operator.LTE; import static org.sonar.server.component.ws.ProjectMeasuresQueryFactory.newProjectMeasuresQuery; import static org.sonar.server.computation.task.projectanalysis.measure.Measure.Level.OK; @@ -152,6 +153,42 @@ public class ProjectMeasuresQueryFactoryTest { emptySet()); } + @Test + public void create_query_having_q() throws Exception { + List criteria = singletonList(Criterion.builder().setKey("query").setOperator(EQ).setValue("Sonar Qube").build()); + + ProjectMeasuresQuery underTest = newProjectMeasuresQuery(criteria, emptySet()); + + assertThat(underTest.getQueryText().get()).isEqualTo("Sonar Qube"); + } + + @Test + public void create_query_having_q_ignore_case_sensitive() throws Exception { + List criteria = singletonList(Criterion.builder().setKey("query").setOperator(EQ).setValue("Sonar Qube").build()); + + ProjectMeasuresQuery underTest = newProjectMeasuresQuery(criteria, emptySet()); + + assertThat(underTest.getQueryText().get()).isEqualTo("Sonar Qube"); + } + + @Test + public void fail_to_create_query_having_q_with_no_value() throws Exception { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Query is invalid"); + + newProjectMeasuresQuery(singletonList(Criterion.builder().setKey("query").setOperator(EQ).build()), + emptySet()); + } + + @Test + public void fail_to_create_query_having_q_with_other_operator_than_equals() throws Exception { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Query should only be used with equals operator"); + + newProjectMeasuresQuery(singletonList(Criterion.builder().setKey("query").setOperator(LT).setValue("java").build()), + emptySet()); + } + @Test public void do_not_filter_on_projectUuids_if_criteria_non_empty_and_projectUuid_is_null() { ProjectMeasuresQuery query = newProjectMeasuresQuery(singletonList(Criterion.builder().setKey("ncloc").setOperator(EQ).setValue("10").build()), diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/ws/SearchProjectsActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/ws/SearchProjectsActionTest.java index d5a5a7a3036..82024b61db0 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/component/ws/SearchProjectsActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/component/ws/SearchProjectsActionTest.java @@ -294,6 +294,19 @@ public class SearchProjectsActionTest { assertThat(result.getComponentsList()).extracting(Component::getName).containsOnly("Sonar Java", "Sonar Groovy", "Sonar Qube"); } + @Test + public void filter_projects_by_text_query() { + OrganizationDto organizationDto = db.organizations().insertForKey("my-org-key-1"); + insertProjectInDbAndEs(newProjectDto(organizationDto).setKey("sonar-java").setName("Sonar Java")); + insertProjectInDbAndEs(newProjectDto(organizationDto).setKey("sonar-groovy").setName("Sonar Groovy")); + insertProjectInDbAndEs(newProjectDto(organizationDto).setKey("sonar-markdown").setName("Sonar Markdown")); + insertProjectInDbAndEs(newProjectDto(organizationDto).setKey("sonarqube").setName("Sonar Qube")); + + assertThat(call(request.setFilter("query = \"Groovy\"")).getComponentsList()).extracting(Component::getName).containsOnly("Sonar Groovy"); + assertThat(call(request.setFilter("query = \"oNar\"")).getComponentsList()).extracting(Component::getName).containsOnly("Sonar Java", "Sonar Groovy", "Sonar Markdown", "Sonar Qube"); + assertThat(call(request.setFilter("query = \"sonar-java\"")).getComponentsList()).extracting(Component::getName).containsOnly("Sonar Java"); + } + @Test public void filter_favourite_projects_with_query_with_or_without_a_specified_organization() { userSession.logIn(); diff --git a/server/sonar-server/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTest.java b/server/sonar-server/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTest.java index 56708d2b8eb..5e027fcbe17 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTest.java @@ -290,6 +290,19 @@ public class ProjectMeasuresIndexTest { assertResults(new ProjectMeasuresQuery().setLanguages(newHashSet("unknown"))); } + @Test + public void filter_on_query_text() { + ComponentDto windows = newProjectDto(ORG).setUuid("windows").setName("Windows").setKey("project1"); + ComponentDto apachee = newProjectDto(ORG).setUuid("apachee").setName("apachee").setKey("project2"); + ComponentDto apache1 = newProjectDto(ORG).setUuid("apache-1").setName("Apache").setKey("project3"); + ComponentDto apache2 = newProjectDto(ORG).setUuid("apache-2").setName("Apache").setKey("project4"); + index(newDoc(windows), newDoc(apachee), newDoc(apache1), newDoc(apache2)); + + assertResults(new ProjectMeasuresQuery().setQueryText("windows"), windows); + assertResults(new ProjectMeasuresQuery().setQueryText("project2"), apachee); + assertResults(new ProjectMeasuresQuery().setQueryText("pAch"), apache1, apache2, apachee); + } + @Test public void filter_on_ids() { index( -- 2.39.5