diff options
author | Teryk Bellahsene <teryk.bellahsene@sonarsource.com> | 2017-03-02 15:39:08 +0100 |
---|---|---|
committer | Teryk Bellahsene <teryk.bellahsene@sonarsource.com> | 2017-03-03 11:07:45 +0100 |
commit | 31e88758527886fa7b66508c0c8d6afd57db4868 (patch) | |
tree | 7eac382dc952dff435219e93228cce899c554fd1 | |
parent | 3c513b0d4e033ad8837ffdef8896c0f86931ed98 (diff) | |
download | sonarqube-31e88758527886fa7b66508c0c8d6afd57db4868.tar.gz sonarqube-31e88758527886fa7b66508c0c8d6afd57db4868.zip |
SONAR-8838 Filter by tags in WS api/components/search_projects
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<FilterParser.Criterion> criteria, @Nullable Set<String> projectUuids) { + static ProjectMeasuresQuery newProjectMeasuresQuery(List<Criterion> criteria, @Nullable Set<String> 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<String> 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<String> 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.<br>" + + .setDescription("Filter of projects on name, key, measure value, quality gate, language, tag or whether a project is a favorite or not.<br>" + "The filter must be encoded to form a valid URL (for example '=' must be replaced by '%3D').<br>" + "Examples of use:" + "<ul>" + @@ -149,12 +149,17 @@ public class SearchProjectsAction implements ComponentsWsAction { " <li>'WARN' for Warning</li>" + " <li>'ERROR' for Failed</li>" + "</ul>" + - "To filter on language keys use language key' : " + + "To filter on language keys use the language key: " + "<ul>" + - " <li>To filter on a single language you can use 'language = java'</li>" + - " <li>To filter on a many language you must use 'language IN (java, js)'</li>" + - "<ul/>" + - "Use the WS api/languages/list to find the key of a language."); + " <li>to filter on a single language you can use 'language = java'</li>" + + " <li>to filter on several languages you must use 'language IN (java, js)'</li>" + + "</ul>" + + "Use the WS api/languages/list to find the key of a language." + + "To filter on tags use the 'tag' keyword:" + + "<ul> " + + " <li>to filter on one tag you can use <code>tag = finance</code></li>" + + " <li>to filter on several tags you must use <code>tag in (offshore, java)</code></li>" + + "</ul>"); action.createParam(Param.SORT) .setDescription("Sort projects by numeric metric key, quality gate status (using '%s'), or by project name.<br/>" + "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> criterion = FilterParser.parse("ncloc>10 and coverage<=80 and language in (java,js)"); + List<Criterion> 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> 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(); @@ -298,6 +296,20 @@ public class SearchProjectsActionTest { } @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"); insertProjectInDbAndEs(newProjectDto(organizationDto).setKey("sonar-java").setName("Sonar Java")); @@ -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); |