]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-8795 Search by text query 1696/head
authorJulien Lancelot <julien.lancelot@sonarsource.com>
Tue, 21 Feb 2017 09:01:15 +0000 (10:01 +0100)
committerJulien Lancelot <julien.lancelot@sonarsource.com>
Wed, 22 Feb 2017 14:33:57 +0000 (15:33 +0100)
server/sonar-server/src/main/java/org/sonar/server/component/ws/FilterParser.java
server/sonar-server/src/main/java/org/sonar/server/component/ws/ProjectMeasuresQueryFactory.java
server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndex.java
server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndexDefinition.java
server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresQuery.java
server/sonar-server/src/test/java/org/sonar/server/component/ws/FilterParserTest.java
server/sonar-server/src/test/java/org/sonar/server/component/ws/ProjectMeasuresQueryFactoryTest.java
server/sonar-server/src/test/java/org/sonar/server/component/ws/SearchProjectsActionTest.java
server/sonar-server/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTest.java

index caa60c55b5aff8ea1419a0b0f5296260c9832db6..f65bfeff0065a17442ee468aea81fe2e844747dc 100644 (file)
@@ -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<Criterion> 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;
index aff52a9f99a4290c74d6ac465604ff88bbc2f0c0..dc2e9330b1e679ae9ff5792ea1a2af4603b53833 100644 (file)
@@ -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<String> 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 {
     }
   }
 
-
 }
index 8571ab16f17c609351eb881953f82caab218a126..8938eabc7d2cf3e1b86a91758291457156bdaa86 100644 (file)
@@ -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<QueryBuilder> createTextQueryFilter(ProjectMeasuresQuery query) {
+    Optional<String> 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;
 
index 5a3558b127a32f19a7aa8af7a0db024d1627ef1e..9e85ab83a0e41fd5fcb18dc0c2ea86b87531d13a 100644 (file)
@@ -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)
index 2bc12465beb39045a3cb9abfc5b934c7d4b5d1c3..0b484c78ce20418d950aa6ccbf4a4f0b0aae02f7 100644 (file)
@@ -40,6 +40,7 @@ public class ProjectMeasuresQuery {
   private Set<String> 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<String> getQueryText() {
+    return Optional.ofNullable(queryText);
+  }
+
+  public ProjectMeasuresQuery setQueryText(@Nullable String queryText) {
+    this.queryText = queryText;
+    return this;
+  }
+
   public String getSort() {
     return sort;
   }
index 09c8e90120f8f0bad81ead45894c87862d45635a..27a429f8fe4cd990ef613f001b3c15ef210310c3 100644 (file)
@@ -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> criterion = FilterParser.parse("");
index 79fd62ac2e671c253f1388d2af4f4e22f53b08dc..215949fba57776607bb59337fada4c62b0676ea9 100644 (file)
@@ -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<Criterion> 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<Criterion> 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()),
index d5a5a7a3036ce61bca62f2da54767372c13f68e2..82024b61db0b27e8c770112ae3c30d78e51cab74 100644 (file)
@@ -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();
index 56708d2b8eb93f039149b93fc300248dd30cd9a5..5e027fcbe1764be7c8605e01c073567a591b65c0 100644 (file)
@@ -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(