From 31e88758527886fa7b66508c0c8d6afd57db4868 Mon Sep 17 00:00:00 2001 From: Teryk Bellahsene Date: Thu, 2 Mar 2017 15:39:08 +0100 Subject: [PATCH] SONAR-8838 Filter by tags in WS api/components/search_projects --- .../server/component/ws/FilterParser.java | 2 +- .../ws/ProjectMeasuresQueryFactory.java | 34 +++++++++++++--- .../component/ws/SearchProjectsAction.java | 17 +++++--- .../server/component/ws/FilterParserTest.java | 10 ++++- .../ws/ProjectMeasuresQueryFactoryTest.java | 39 +++++++++++++++++-- .../ws/SearchProjectsActionTest.java | 26 ++++++++++--- 6 files changed, 104 insertions(+), 24 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 c352c622f7e..9a4a3f0219b 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 @@ -39,7 +39,7 @@ public class FilterParser { private static final String DOUBLE_QUOTES = "\""; private static final Splitter CRITERIA_SPLITTER = Splitter.on(Pattern.compile(" and ", Pattern.CASE_INSENSITIVE)).trimResults().omitEmptyStrings(); - private static final Splitter IN_VALUES_SPLITTER = Splitter.on(",").omitEmptyStrings().trimResults(); + private static final Splitter IN_VALUES_SPLITTER = Splitter.on(",").trimResults().omitEmptyStrings(); 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); 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 dc2e9330b1e..e05cb940b37 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 @@ -26,6 +26,7 @@ import java.util.Optional; import java.util.Set; import javax.annotation.Nullable; import org.sonar.api.measures.Metric.Level; +import org.sonar.server.component.ws.FilterParser.Criterion; import org.sonar.server.component.ws.FilterParser.Operator; import org.sonar.server.measure.index.ProjectMeasuresQuery; @@ -42,20 +43,21 @@ import static org.sonarqube.ws.client.project.ProjectsWsParameters.FILTER_LANGUA class ProjectMeasuresQueryFactory { public static final String IS_FAVORITE_CRITERION = "isFavorite"; + public static final String CRITERION_TAG = "tag"; public static final String QUERY_KEY = "query"; private ProjectMeasuresQueryFactory() { // prevent instantiation } - static ProjectMeasuresQuery newProjectMeasuresQuery(List criteria, @Nullable Set projectUuids) { + static ProjectMeasuresQuery newProjectMeasuresQuery(List criteria, @Nullable Set projectUuids) { ProjectMeasuresQuery query = new ProjectMeasuresQuery(); Optional.ofNullable(projectUuids).ifPresent(query::setProjectUuids); criteria.forEach(criterion -> processCriterion(criterion, query)); return query; } - private static void processCriterion(FilterParser.Criterion criterion, ProjectMeasuresQuery query) { + private static void processCriterion(Criterion criterion, ProjectMeasuresQuery query) { String key = criterion.getKey().toLowerCase(ENGLISH); if (IS_FAVORITE_CRITERION.equalsIgnoreCase(key)) { return; @@ -63,11 +65,17 @@ class ProjectMeasuresQueryFactory { Operator operator = criterion.getOperator(); checkArgument(operator != null, "Operator cannot be null for '%s'", key); + if (FILTER_LANGUAGE.equalsIgnoreCase(key)) { processLanguages(criterion, query); return; } + if (CRITERION_TAG.equalsIgnoreCase(key)) { + processTags(criterion, query); + return; + } + if (QUERY_KEY.equalsIgnoreCase(key)) { processQuery(criterion, query); return; @@ -82,7 +90,7 @@ class ProjectMeasuresQueryFactory { } } - private static void processLanguages(FilterParser.Criterion criterion, ProjectMeasuresQuery query) { + private static void processLanguages(Criterion criterion, ProjectMeasuresQuery query) { Operator operator = criterion.getOperator(); String value = criterion.getValue(); List values = criterion.getValues(); @@ -97,7 +105,22 @@ class ProjectMeasuresQueryFactory { 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) { + private static void processTags(Criterion criterion, ProjectMeasuresQuery query) { + Operator operator = criterion.getOperator(); + String value = criterion.getValue(); + List values = criterion.getValues(); + if (value != null && EQ.equals(operator)) { + query.setTags(singleton(value)); + return; + } + if (!values.isEmpty() && IN.equals(operator)) { + query.setTags(new HashSet<>(values)); + return; + } + throw new IllegalArgumentException("Tag should be set either by using 'tag = java' or 'tag IN (finance, platform)'"); + } + + private static void processQuery(Criterion criterion, ProjectMeasuresQuery query) { Operator operatorValue = criterion.getOperator(); String value = criterion.getValue(); checkArgument(value != null, "Query is invalid"); @@ -105,7 +128,7 @@ class ProjectMeasuresQueryFactory { query.setQueryText(value); } - private static void processQualityGateStatus(FilterParser.Criterion criterion, ProjectMeasuresQuery query) { + private static void processQualityGateStatus(Criterion criterion, ProjectMeasuresQuery query) { Operator operator = criterion.getOperator(); String value = criterion.getValue(); checkArgument(EQ.equals(operator), "Only equals operator is available for quality gate criteria"); @@ -121,5 +144,4 @@ class ProjectMeasuresQueryFactory { throw new IllegalArgumentException(format("Value '%s' is not a number", value)); } } - } 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 b5749087c02..493060331d7 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 @@ -119,7 +119,7 @@ public class SearchProjectsAction implements ComponentsWsAction { .setPossibleValues(SUPPORTED_FACETS); action .createParam(PARAM_FILTER) - .setDescription("Filter of projects on name, key, measure value, quality gate, language, or whether a project is a favorite or not.
" + + .setDescription("Filter of projects on name, key, measure value, quality gate, language, tag or whether a project is a favorite or not.
" + "The filter must be encoded to form a valid URL (for example '=' must be replaced by '%3D').
" + "Examples of use:" + "
    " + @@ -149,12 +149,17 @@ public class SearchProjectsAction implements ComponentsWsAction { "
  • 'WARN' for Warning
  • " + "
  • 'ERROR' for Failed
  • " + "
" + - "To filter on language keys use language key' : " + + "To filter on language keys use the language key: " + "
    " + - "
  • To filter on a single language you can use 'language = java'
  • " + - "
  • To filter on a many language you must use 'language IN (java, js)'
  • " + - "
      " + - "Use the WS api/languages/list to find the key of a language."); + "
    • to filter on a single language you can use 'language = java'
    • " + + "
    • to filter on several languages you must use 'language IN (java, js)'
    • " + + "
    " + + "Use the WS api/languages/list to find the key of a language." + + "To filter on tags use the 'tag' keyword:" + + "
      " + + "
    • to filter on one tag you can use tag = finance
    • " + + "
    • to filter on several tags you must use tag in (offshore, java)
    • " + + "
    "); action.createParam(Param.SORT) .setDescription("Sort projects by numeric metric key, quality gate status (using '%s'), or by project name.
    " + "See '%s' parameter description for the possible metric values", ALERT_STATUS_KEY, PARAM_FILTER) 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 27a429f8fe4..fa83fae3169 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 @@ -126,14 +126,14 @@ public class FilterParserTest { @Test public void parse_filter_without_any_space_in_criteria() throws Exception { - List criterion = FilterParser.parse("ncloc>10 and coverage<=80 and language in (java,js)"); + List criterion = FilterParser.parse("ncloc>10 and coverage<=80 and tags in (java,platform)"); assertThat(criterion) .extracting(Criterion::getKey, Criterion::getOperator, Criterion::getValue, Criterion::getValues) .containsOnly( tuple("ncloc", GT, "10", emptyList()), tuple("coverage", LTE, "80", emptyList()), - tuple("language", IN, null, asList("java", "js"))); + tuple("tags", IN, null, asList("java", "platform"))); } @Test @@ -198,4 +198,10 @@ public class FilterParserTest { assertThat(criterion).hasSize(4); } + @Test + public void metric_key_with_and_string() { + List criterion = FilterParser.parse("ncloc > 10 and operand = 5"); + + assertThat(criterion).hasSize(2).extracting(Criterion::getKey, Criterion::getValue).containsExactly(tuple("ncloc", "10"), tuple("operand", "5")); + } } 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 215949fba57..46ec80c2d7d 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 @@ -140,8 +140,7 @@ public class ProjectMeasuresQueryFactoryTest { expectedException.expect(IllegalArgumentException.class); expectedException.expectMessage("Language should be set either by using 'language = java' or 'language IN (java, js)"); - newProjectMeasuresQuery(singletonList(Criterion.builder().setKey("language").setOperator(IN).setValue("java").build()), - emptySet()); + newProjectMeasuresQuery(singletonList(Criterion.builder().setKey("language").setOperator(IN).setValue("java").build()), emptySet()); } @Test @@ -149,8 +148,42 @@ public class ProjectMeasuresQueryFactoryTest { expectedException.expect(IllegalArgumentException.class); expectedException.expectMessage("Language should be set either by using 'language = java' or 'language IN (java, js)"); - newProjectMeasuresQuery(singletonList(Criterion.builder().setKey("language").setOperator(EQ).setValues(asList("java")).build()), + newProjectMeasuresQuery(singletonList(Criterion.builder().setKey("language").setOperator(EQ).setValues(asList("java")).build()), emptySet()); + } + + + @Test + public void create_query_on_tag_using_in_operator() throws Exception { + ProjectMeasuresQuery query = newProjectMeasuresQuery( + singletonList(Criterion.builder().setKey("tag").setOperator(IN).setValues(asList("java", "js")).build()), + emptySet()); + + assertThat(query.getTags().get()).containsOnly("java", "js"); + } + + @Test + public void create_query_on_tag_using_equals_operator() throws Exception { + ProjectMeasuresQuery query = newProjectMeasuresQuery( + singletonList(Criterion.builder().setKey("tag").setOperator(EQ).setValue("java").build()), emptySet()); + + assertThat(query.getTags().get()).containsOnly("java"); + } + + @Test + public void fail_to_create_query_on_tag_using_in_operator_and_value() throws Exception { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Tag should be set either by using 'tag = java' or 'tag IN (finance, platform)"); + + newProjectMeasuresQuery(singletonList(Criterion.builder().setKey("tag").setOperator(IN).setValue("java").build()), emptySet()); + } + + @Test + public void fail_to_create_query_on_tag_using_eq_operator_and_values() throws Exception { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Tag should be set either by using 'tag = java' or 'tag IN (finance, platform)"); + + newProjectMeasuresQuery(singletonList(Criterion.builder().setKey("tag").setOperator(EQ).setValues(asList("java")).build()), emptySet()); } @Test 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 48f9e835d0f..605442cd4a4 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 @@ -160,8 +160,7 @@ public class SearchProjectsActionTest { .setUuid(Uuids.UUID_EXAMPLE_01) .setKey(KeyExamples.KEY_PROJECT_EXAMPLE_001) .setName("My Project 1") - .setTagsString("finance, java") - ); + .setTagsString("finance, java")); insertProjectInDbAndEs(newProjectDto(organization1Dto) .setUuid(Uuids.UUID_EXAMPLE_02) .setKey(KeyExamples.KEY_PROJECT_EXAMPLE_002) @@ -170,8 +169,7 @@ public class SearchProjectsActionTest { .setUuid(Uuids.UUID_EXAMPLE_03) .setKey(KeyExamples.KEY_PROJECT_EXAMPLE_003) .setName("My Project 3") - .setTagsString("sales, offshore, java") - ); + .setTagsString("sales, offshore, java")); userSession.logIn().setUserId(23); addFavourite(project1); dbSession.commit(); @@ -297,6 +295,20 @@ public class SearchProjectsActionTest { assertThat(result.getComponentsList()).extracting(Component::getName).containsOnly("Sonar Java", "Sonar Groovy", "Sonar Qube"); } + @Test + public void filter_projects_by_tags() { + OrganizationDto organizationDto = db.organizations().insertForKey("my-org-key-1"); + insertProjectInDbAndEs(newProjectDto(organizationDto).setName("Sonar Java").setTags(newArrayList("finance", "platform"))); + insertProjectInDbAndEs(newProjectDto(organizationDto).setName("Sonar Markdown").setTags(singletonList("marketing"))); + insertProjectInDbAndEs(newProjectDto(organizationDto).setName("Sonar Qube").setTags(newArrayList("offshore"))); + insertMetrics(COVERAGE, NCLOC); + request.setFilter("tag in (finance, offshore)"); + + SearchProjectsWsResponse result = call(request); + + assertThat(result.getComponentsList()).extracting(Component::getName).containsOnly("Sonar Java", "Sonar Qube"); + } + @Test public void filter_projects_by_text_query() { OrganizationDto organizationDto = db.organizations().insertForKey("my-org-key-1"); @@ -306,7 +318,8 @@ public class SearchProjectsActionTest { 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 = \"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"); } @@ -601,7 +614,8 @@ public class SearchProjectsActionTest { .setName(project.name()) .setMeasures(measures) .setQualityGateStatus(qualityGateStatus) - .setLanguages(languagesDistribution)); + .setLanguages(languagesDistribution) + .setTags(project.getTags())); authorizationIndexerTester.allowOnlyAnyone(project); } catch (Exception e) { Throwables.propagate(e); -- 2.39.5