]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-8840 Create WS api/project_tags/search to search for tags 1752/head
authorTeryk Bellahsene <teryk.bellahsene@sonarsource.com>
Mon, 6 Mar 2017 17:13:47 +0000 (18:13 +0100)
committerTeryk Bellahsene <teryk.bellahsene@sonarsource.com>
Tue, 7 Mar 2017 10:42:07 +0000 (11:42 +0100)
server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndex.java
server/sonar-server/src/main/java/org/sonar/server/projecttag/ws/ProjectTagsWsModule.java
server/sonar-server/src/main/java/org/sonar/server/projecttag/ws/SearchAction.java [new file with mode: 0644]
server/sonar-server/src/main/resources/org/sonar/server/projecttag/ws/search-example.json [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTest.java
server/sonar-server/src/test/java/org/sonar/server/projecttag/ws/ProjectTagsWsModuleTest.java
server/sonar-server/src/test/java/org/sonar/server/projecttag/ws/SearchActionTest.java [new file with mode: 0644]
sonar-ws/src/main/protobuf/ws-project_tags.proto [new file with mode: 0644]

index f4ee4488274304dae73a5dad0c2d4f96b92ead11..3efd2707796b36c3cba0cb733c2f7d0fd08e660b 100644 (file)
@@ -28,14 +28,19 @@ import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.stream.IntStream;
+import javax.annotation.Nullable;
 import org.elasticsearch.action.search.SearchRequestBuilder;
 import org.elasticsearch.index.query.BoolQueryBuilder;
 import org.elasticsearch.index.query.QueryBuilder;
 import org.elasticsearch.search.aggregations.AbstractAggregationBuilder;
 import org.elasticsearch.search.aggregations.AggregationBuilders;
+import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation.Bucket;
 import org.elasticsearch.search.aggregations.bucket.filters.FiltersAggregationBuilder;
 import org.elasticsearch.search.aggregations.bucket.range.RangeBuilder;
+import org.elasticsearch.search.aggregations.bucket.terms.Terms;
+import org.elasticsearch.search.aggregations.bucket.terms.TermsBuilder;
 import org.elasticsearch.search.sort.FieldSortBuilder;
+import org.sonar.core.util.stream.Collectors;
 import org.sonar.server.es.BaseIndex;
 import org.sonar.server.es.DefaultIndexSettingsElement;
 import org.sonar.server.es.EsClient;
@@ -47,6 +52,8 @@ import org.sonar.server.es.textsearch.ComponentTextSearchQueryFactory;
 import org.sonar.server.measure.index.ProjectMeasuresQuery.MetricCriterion;
 import org.sonar.server.permission.index.AuthorizationTypeSupport;
 
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.util.Collections.emptyList;
 import static org.elasticsearch.index.query.QueryBuilders.boolQuery;
 import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery;
 import static org.elasticsearch.index.query.QueryBuilders.nestedQuery;
@@ -63,6 +70,7 @@ import static org.sonar.api.measures.CoreMetrics.NCLOC_KEY;
 import static org.sonar.api.measures.CoreMetrics.RELIABILITY_RATING_KEY;
 import static org.sonar.api.measures.CoreMetrics.SECURITY_RATING_KEY;
 import static org.sonar.api.measures.CoreMetrics.SQALE_RATING_KEY;
+import static org.sonar.server.es.EsUtils.escapeSpecialRegexChars;
 import static org.sonar.server.measure.index.ProjectMeasuresDoc.QUALITY_GATE_STATUS;
 import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_KEY;
 import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_LANGUAGES;
@@ -304,6 +312,34 @@ public class ProjectMeasuresIndex extends BaseIndex {
     }
   }
 
+  public List<String> searchTags(@Nullable String textQuery, int pageSize) {
+    checkArgument(pageSize <= 100, "Page size must be lower than or equals to " + 100);
+    if (pageSize == 0) {
+      return emptyList();
+    }
+
+    TermsBuilder tagFacet = AggregationBuilders.terms(FIELD_TAGS)
+      .field(FIELD_TAGS)
+      .size(pageSize)
+      .minDocCount(1);
+    if (textQuery != null) {
+      tagFacet.include(".*" + escapeSpecialRegexChars(textQuery) + ".*");
+    }
+
+    SearchRequestBuilder searchQuery = getClient()
+      .prepareSearch(INDEX_TYPE_PROJECT_MEASURES)
+      .setQuery(authorizationTypeSupport.createQueryFilter())
+      .setFetchSource(false)
+      .setSize(0)
+      .addAggregation(tagFacet);
+
+    Terms aggregation = searchQuery.get().getAggregations().get(FIELD_TAGS);
+
+    return aggregation.getBuckets().stream()
+      .map(Bucket::getKeyAsString)
+      .collect(Collectors.toList());
+  }
+
   @FunctionalInterface
   private interface FacetSetter {
     void addFacet(SearchRequestBuilder esSearch, Map<String, QueryBuilder> filters);
index 9d1b106ad9daacd9af859cd4f9f44b104d018ce9..e89d01e9e7da5f512da19b31c560fe43e74b488c 100644 (file)
@@ -27,7 +27,8 @@ public class ProjectTagsWsModule extends Module {
   protected void configureModule() {
     add(
       ProjectTagsWs.class,
-      SetAction.class
+      SetAction.class,
+      SearchAction.class
     );
   }
 }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/projecttag/ws/SearchAction.java b/server/sonar-server/src/main/java/org/sonar/server/projecttag/ws/SearchAction.java
new file mode 100644 (file)
index 0000000..ab915cd
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.projecttag.ws;
+
+import java.util.List;
+import org.sonar.api.server.ws.Request;
+import org.sonar.api.server.ws.Response;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.server.measure.index.ProjectMeasuresIndex;
+import org.sonarqube.ws.WsProjectTags;
+
+import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE;
+import static org.sonar.api.server.ws.WebService.Param.TEXT_QUERY;
+import static org.sonar.server.ws.WsUtils.writeProtobuf;
+
+public class SearchAction implements ProjectTagsWsAction {
+  private final ProjectMeasuresIndex index;
+
+  public SearchAction(ProjectMeasuresIndex index) {
+    this.index = index;
+  }
+
+  @Override
+  public void define(WebService.NewController context) {
+    WebService.NewAction action = context.createAction("search")
+      .setDescription("Search tags")
+      .setSince("6.4")
+      .setResponseExample(getClass().getResource("search-example.json"))
+      .setHandler(this);
+
+    action.addSearchQuery("off", "tags");
+    action.addPageSize(10, 100);
+  }
+
+  @Override
+  public void handle(Request request, Response response) throws Exception {
+    WsProjectTags.SearchResponse wsResponse = doHandle(request);
+    writeProtobuf(wsResponse, request, response);
+  }
+
+  private WsProjectTags.SearchResponse doHandle(Request request) {
+    List<String> tags = index.searchTags(request.param(TEXT_QUERY), request.mandatoryParamAsInt(PAGE_SIZE));
+    return WsProjectTags.SearchResponse.newBuilder().addAllTags(tags).build();
+  }
+}
diff --git a/server/sonar-server/src/main/resources/org/sonar/server/projecttag/ws/search-example.json b/server/sonar-server/src/main/resources/org/sonar/server/projecttag/ws/search-example.json
new file mode 100644 (file)
index 0000000..7965673
--- /dev/null
@@ -0,0 +1,7 @@
+{
+  "tags": [
+    "official",
+    "offshore",
+    "playoff"
+  ]
+}
index bfbcbbfccb897e3e1072c5840ab34163366bb2f0..3d5a28e324df01024e00dce6fb4ac3c6344b459b 100644 (file)
@@ -27,6 +27,7 @@ import java.util.Map;
 import java.util.stream.IntStream;
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExpectedException;
 import org.sonar.api.config.MapSettings;
 import org.sonar.api.resources.Qualifiers;
 import org.sonar.db.component.ComponentDto;
@@ -47,6 +48,7 @@ import org.sonar.server.tester.UserSessionRule;
 
 import static com.google.common.collect.Lists.newArrayList;
 import static com.google.common.collect.Sets.newHashSet;
+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.CoreMetrics.ALERT_STATUS_KEY;
@@ -82,6 +84,9 @@ public class ProjectMeasuresIndexTest {
   @Rule
   public EsTester es = new EsTester(new ProjectMeasuresIndexDefinition(new MapSettings()));
 
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
   @Rule
   public UserSessionRule userSession = UserSessionRule.standalone();
 
@@ -1083,6 +1088,75 @@ public class ProjectMeasuresIndexTest {
     assertThat(result).hasSize(10).containsOnlyKeys("finance1", "finance2", "finance3", "finance4", "finance5", "finance6", "finance7", "finance8", "finance9", "finance10");
   }
 
+  @Test
+  public void search_tags() {
+    index(
+      newDoc().setTags(newArrayList("finance", "offshore", "java")),
+      newDoc().setTags(newArrayList("official", "javascript")),
+      newDoc().setTags(newArrayList("marketing", "official")),
+      newDoc().setTags(newArrayList("marketing", "Madhoff")),
+      newDoc().setTags(newArrayList("finance", "offshore")),
+      newDoc().setTags(newArrayList("offshore")));
+
+    List<String> result = underTest.searchTags("off", 10);
+
+    assertThat(result).containsExactly("offshore", "official", "Madhoff");
+  }
+
+  @Test
+  public void search_tags_return_all_tags() {
+    index(
+      newDoc().setTags(newArrayList("finance", "offshore", "java")),
+      newDoc().setTags(newArrayList("official", "javascript")),
+      newDoc().setTags(newArrayList("marketing", "official")),
+      newDoc().setTags(newArrayList("marketing", "Madhoff")),
+      newDoc().setTags(newArrayList("finance", "offshore")),
+      newDoc().setTags(newArrayList("offshore")));
+
+    List<String> result = underTest.searchTags(null, 10);
+
+    assertThat(result).containsOnly("offshore", "official", "Madhoff", "finance", "marketing", "java", "javascript");
+  }
+
+  @Test
+  public void search_tags_only_of_authorized_projects() {
+    indexForUser(USER1,
+      newDoc(PROJECT1).setTags(singletonList("finance")),
+      newDoc(PROJECT2).setTags(singletonList("marketing")));
+    indexForUser(USER2,
+      newDoc(PROJECT3).setTags(singletonList("offshore")));
+
+    userSession.logIn(USER1);
+
+    List<String> result = underTest.searchTags(null, 10);
+
+    assertThat(result).containsOnly("finance", "marketing");
+  }
+
+  @Test
+  public void search_tags_with_no_tags() {
+    List<String> result = underTest.searchTags("whatever", 10);
+
+    assertThat(result).isEmpty();
+  }
+
+  @Test
+  public void search_tags_with_page_size_at_0() {
+    index(newDoc().setTags(newArrayList("offshore")));
+
+    List<String> result = underTest.searchTags(null, 0);
+
+    assertThat(result).isEmpty();
+  }
+
+  @Test
+  public void fail_if_page_size_greater_than_100() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Page size must be lower than or equals to 100");
+
+    underTest.searchTags("whatever", 101);
+  }
+
   private void index(ProjectMeasuresDoc... docs) {
     es.putDocuments(INDEX_TYPE_PROJECT_MEASURES, docs);
     for (ProjectMeasuresDoc doc : docs) {
index 35f38ba7d337e4a5e698de6958b98a27a03a1162..adef078f459746370dc8685dc60e23a25741b3d3 100644 (file)
@@ -31,6 +31,6 @@ public class ProjectTagsWsModuleTest {
   public void verify_count_of_added_components() {
     ComponentContainer container = new ComponentContainer();
     new ProjectTagsWsModule().configure(container);
-    assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 2);
+    assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 3);
   }
 }
diff --git a/server/sonar-server/src/test/java/org/sonar/server/projecttag/ws/SearchActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/projecttag/ws/SearchActionTest.java
new file mode 100644 (file)
index 0000000..3d151e1
--- /dev/null
@@ -0,0 +1,138 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.projecttag.ws;
+
+import com.google.common.base.Throwables;
+import java.io.IOException;
+import javax.annotation.Nullable;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.config.MapSettings;
+import org.sonar.api.resources.Qualifiers;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.organization.OrganizationDto;
+import org.sonar.db.organization.OrganizationTesting;
+import org.sonar.server.es.EsTester;
+import org.sonar.server.measure.index.ProjectMeasuresDoc;
+import org.sonar.server.measure.index.ProjectMeasuresIndex;
+import org.sonar.server.measure.index.ProjectMeasuresIndexDefinition;
+import org.sonar.server.measure.index.ProjectMeasuresIndexer;
+import org.sonar.server.permission.index.AuthorizationTypeSupport;
+import org.sonar.server.permission.index.PermissionIndexerDao;
+import org.sonar.server.permission.index.PermissionIndexerTester;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.ws.TestRequest;
+import org.sonar.server.ws.WsActionTester;
+import org.sonarqube.ws.WsProjectTags.SearchResponse;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.core.util.Protobuf.setNullable;
+import static org.sonar.db.component.ComponentTesting.newProjectDto;
+import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.INDEX_TYPE_PROJECT_MEASURES;
+import static org.sonar.test.JsonAssert.assertJson;
+import static org.sonarqube.ws.MediaTypes.PROTOBUF;
+
+public class SearchActionTest {
+  private static final OrganizationDto ORG = OrganizationTesting.newOrganizationDto();
+
+  @Rule
+  public EsTester es = new EsTester(new ProjectMeasuresIndexDefinition(new MapSettings()));
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  @Rule
+  public UserSessionRule userSession = UserSessionRule.standalone();
+
+  private ProjectMeasuresIndexer projectMeasureIndexer = new ProjectMeasuresIndexer(null, es.client());
+  private PermissionIndexerTester authorizationIndexerTester = new PermissionIndexerTester(es, projectMeasureIndexer);
+  private ProjectMeasuresIndex index = new ProjectMeasuresIndex(es.client(), new AuthorizationTypeSupport(userSession));
+
+  private WsActionTester ws = new WsActionTester(new SearchAction(index));
+
+  @Test
+  public void json_example() throws IOException {
+    index(newDoc().setTags(newArrayList("official", "offshore", "playoff")));
+
+    String result = ws.newRequest().execute().getInput();
+
+    assertJson(ws.getDef().responseExampleAsString()).isSimilarTo(result);
+  }
+
+  @Test
+  public void search_by_query_and_page_size() {
+    index(
+      newDoc().setTags(newArrayList("whatever-tag", "official", "offshore", "yet-another-tag", "playoff")),
+      newDoc().setTags(newArrayList("offshore", "playoff")));
+
+    SearchResponse result = call("off", 2);
+
+    assertThat(result.getTagsList()).containsOnly("offshore", "playoff");
+  }
+
+  @Test
+  public void definition() {
+    WebService.Action definition = ws.getDef();
+
+    assertThat(definition.key()).isEqualTo("search");
+    assertThat(definition.isInternal()).isFalse();
+    assertThat(definition.isPost()).isFalse();
+    assertThat(definition.responseExampleAsString()).isNotEmpty();
+    assertThat(definition.since()).isEqualTo("6.4");
+    assertThat(definition.params()).extracting(WebService.Param::key).containsOnly("q", "ps");
+  }
+
+  private void index(ProjectMeasuresDoc... docs) {
+    es.putDocuments(INDEX_TYPE_PROJECT_MEASURES, docs);
+    for (ProjectMeasuresDoc doc : docs) {
+      PermissionIndexerDao.Dto access = new PermissionIndexerDao.Dto(doc.getId(), System.currentTimeMillis(), Qualifiers.PROJECT);
+      access.allowAnyone();
+      authorizationIndexerTester.allow(access);
+    }
+  }
+
+  private static ProjectMeasuresDoc newDoc(ComponentDto project) {
+    return new ProjectMeasuresDoc()
+      .setOrganizationUuid(project.getOrganizationUuid())
+      .setId(project.uuid())
+      .setKey(project.key())
+      .setName(project.name());
+  }
+
+  private static ProjectMeasuresDoc newDoc() {
+    return newDoc(newProjectDto(ORG));
+  }
+
+  private SearchResponse call(@Nullable String textQuery, @Nullable Integer pageSize) {
+    TestRequest request = ws.newRequest().setMediaType(PROTOBUF);
+    setNullable(textQuery, s -> request.setParam("q", s));
+    setNullable(pageSize, ps -> request.setParam("ps", ps.toString()));
+
+    try {
+      return SearchResponse.parseFrom(request.execute().getInputStream());
+    } catch (IOException e) {
+      throw Throwables.propagate(e);
+    }
+  }
+}
diff --git a/sonar-ws/src/main/protobuf/ws-project_tags.proto b/sonar-ws/src/main/protobuf/ws-project_tags.proto
new file mode 100644 (file)
index 0000000..99cc43c
--- /dev/null
@@ -0,0 +1,30 @@
+// SonarQube, open source software quality management tool.
+// Copyright (C) 2008-2016 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.
+
+syntax = "proto3";
+
+package sonarqube.ws.projects;
+
+option java_package = "org.sonarqube.ws";
+option java_outer_classname = "WsProjectTags";
+option optimize_for = SPEED;
+
+// Response for api/project_tags/search
+message SearchResponse {
+  repeated string tags = 1;
+}