From: Teryk Bellahsene Date: Mon, 6 Mar 2017 17:13:47 +0000 (+0100) Subject: SONAR-8840 Create WS api/project_tags/search to search for tags X-Git-Tag: 6.4-RC1~807 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=refs%2Fpull%2F1752%2Fhead;p=sonarqube.git SONAR-8840 Create WS api/project_tags/search to search for tags --- 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 f4ee4488274..3efd2707796 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 @@ -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 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 filters); diff --git a/server/sonar-server/src/main/java/org/sonar/server/projecttag/ws/ProjectTagsWsModule.java b/server/sonar-server/src/main/java/org/sonar/server/projecttag/ws/ProjectTagsWsModule.java index 9d1b106ad9d..e89d01e9e7d 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/projecttag/ws/ProjectTagsWsModule.java +++ b/server/sonar-server/src/main/java/org/sonar/server/projecttag/ws/ProjectTagsWsModule.java @@ -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 index 00000000000..ab915cd61c2 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/projecttag/ws/SearchAction.java @@ -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 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 index 00000000000..79656739879 --- /dev/null +++ b/server/sonar-server/src/main/resources/org/sonar/server/projecttag/ws/search-example.json @@ -0,0 +1,7 @@ +{ + "tags": [ + "official", + "offshore", + "playoff" + ] +} 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 bfbcbbfccb8..3d5a28e324d 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 @@ -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 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 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 result = underTest.searchTags(null, 10); + + assertThat(result).containsOnly("finance", "marketing"); + } + + @Test + public void search_tags_with_no_tags() { + List result = underTest.searchTags("whatever", 10); + + assertThat(result).isEmpty(); + } + + @Test + public void search_tags_with_page_size_at_0() { + index(newDoc().setTags(newArrayList("offshore"))); + + List 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) { diff --git a/server/sonar-server/src/test/java/org/sonar/server/projecttag/ws/ProjectTagsWsModuleTest.java b/server/sonar-server/src/test/java/org/sonar/server/projecttag/ws/ProjectTagsWsModuleTest.java index 35f38ba7d33..adef078f459 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/projecttag/ws/ProjectTagsWsModuleTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/projecttag/ws/ProjectTagsWsModuleTest.java @@ -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 index 00000000000..3d151e14117 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/projecttag/ws/SearchActionTest.java @@ -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 index 00000000000..99cc43c2e10 --- /dev/null +++ b/sonar-ws/src/main/protobuf/ws-project_tags.proto @@ -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; +}