From 975b9c625513e4fb7b050150e3bc323c5ce2d561 Mon Sep 17 00:00:00 2001 From: =?utf8?q?S=C3=A9bastien=20Lesaint?= Date: Thu, 26 Jan 2017 17:36:57 +0100 Subject: [PATCH] SONAR-8647 add organization param to api/components/search_projects --- .../ws/ProjectMeasuresQueryFactory.java | 41 ++++-- .../component/ws/SearchProjectsAction.java | 106 +++++++++++--- .../ws/ProjectMeasuresQueryFactoryTest.java | 69 ++++++--- .../ws/ProjectMeasuresQueryValidatorTest.java | 14 +- .../ws/SearchProjectsActionTest.java | 136 ++++++++++++++---- .../client/component/ComponentsService.java | 2 + .../component/ComponentsWsParameters.java | 1 + .../component/SearchProjectsRequest.java | 16 ++- 8 files changed, 294 insertions(+), 91 deletions(-) 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 294e833b7c5..a7483c0d514 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 @@ -21,11 +21,15 @@ package org.sonar.server.component.ws; import com.google.common.base.Splitter; +import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.apache.commons.lang.StringUtils; +import java.util.stream.StreamSupport; +import javax.annotation.Nullable; import org.sonar.api.measures.Metric.Level; +import org.sonar.core.util.stream.Collectors; import org.sonar.server.measure.index.ProjectMeasuresQuery; import static com.google.common.base.Preconditions.checkArgument; @@ -43,27 +47,34 @@ class ProjectMeasuresQueryFactory { // prevent instantiation } - static ProjectMeasuresQuery newProjectMeasuresQuery(String filter, Set favoriteProjectUuids) { - if (StringUtils.isBlank(filter)) { - return new ProjectMeasuresQuery(); - } + static List toCriteria(String filter) { + return StreamSupport.stream(CRITERIA_SPLITTER.split(filter).spliterator(), false) + .filter(Objects::nonNull) + .filter(criterion -> !criterion.isEmpty()) + .collect(Collectors.toList()); + } - ProjectMeasuresQuery query = new ProjectMeasuresQuery(); + static boolean hasIsFavouriteCriterion(List criteria) { + return criteria.stream().anyMatch(IS_FAVORITE_CRITERION::equalsIgnoreCase); + } - CRITERIA_SPLITTER.split(filter) - .forEach(criteria -> processCriterion(criteria, query, favoriteProjectUuids)); - return query; + static ProjectMeasuresQuery newProjectMeasuresQuery(List criteria, @Nullable Set projectUuids) { + ProjectMeasuresQuery res = new ProjectMeasuresQuery(); + if (projectUuids != null) { + res.setProjectUuids(projectUuids); + } + criteria.forEach(criterion -> processCriterion(criterion, res)); + return res; } - private static void processCriterion(String rawCriterion, ProjectMeasuresQuery query, Set favoriteProjectUuids) { + private static void processCriterion(String rawCriterion, ProjectMeasuresQuery query) { String criterion = rawCriterion.trim(); - try { - if (IS_FAVORITE_CRITERION.equalsIgnoreCase(criterion)) { - query.setProjectUuids(favoriteProjectUuids); - return; - } + if (IS_FAVORITE_CRITERION.equalsIgnoreCase(criterion)) { + return; + } + try { Matcher matcher = CRITERIA_PATTERN.matcher(criterion); checkArgument(matcher.find() && matcher.groupCount() == 3, "Criterion should be 'isFavourite' or criterion should have a metric, an operator and a value"); String metric = matcher.group(1).toLowerCase(ENGLISH); 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 8bbc7cdda0b..005ee55d449 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 @@ -20,6 +20,7 @@ package org.sonar.server.component.ws; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Ordering; import java.util.Collections; import java.util.LinkedHashMap; @@ -30,6 +31,8 @@ import java.util.Set; import java.util.function.Function; import java.util.stream.Collector; import java.util.stream.Stream; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; import org.sonar.api.resources.Qualifiers; import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Response; @@ -56,10 +59,15 @@ import org.sonarqube.ws.client.component.SearchProjectsRequest; import static com.google.common.base.MoreObjects.firstNonNull; import static java.lang.String.format; +import static org.sonar.core.util.stream.Collectors.toSet; +import static org.sonar.server.component.ws.ProjectMeasuresQueryFactory.hasIsFavouriteCriterion; import static org.sonar.server.component.ws.ProjectMeasuresQueryFactory.newProjectMeasuresQuery; +import static org.sonar.server.component.ws.ProjectMeasuresQueryFactory.toCriteria; import static org.sonar.server.measure.index.ProjectMeasuresIndex.SUPPORTED_FACETS; +import static org.sonar.server.ws.WsUtils.checkFoundWithOptional; import static org.sonar.server.ws.WsUtils.writeProtobuf; import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_FILTER; +import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_ORGANIZATION; import static org.sonarqube.ws.client.component.SearchProjectsRequest.DEFAULT_PAGE_SIZE; import static org.sonarqube.ws.client.component.SearchProjectsRequest.MAX_PAGE_SIZE; @@ -86,6 +94,10 @@ public class SearchProjectsAction implements ComponentsWsAction { .setResponseExample(getClass().getResource("search_projects-example.json")) .setHandler(this); + action.createParam(PARAM_ORGANIZATION) + .setDescription("the organization to search projects in") + .setRequired(false) + .setSince("6.3"); action.createParam(Param.FACETS) .setDescription("Comma-separated list of the facets to be computed. No facet is computed by default.") .setPossibleValues(SUPPORTED_FACETS); @@ -130,22 +142,39 @@ public class SearchProjectsAction implements ComponentsWsAction { private SearchProjectsWsResponse doHandle(SearchProjectsRequest request) { try (DbSession dbSession = dbClient.openSession(false)) { - SearchResults searchResults = searchData(dbSession, request); - Set organizationUuids = searchResults.projects.stream().map(ComponentDto::getOrganizationUuid).collect(Collectors.toSet()); - Map organizationsByUuid = dbClient.organizationDao().selectByUuids(dbSession, organizationUuids) - .stream() - .collect(Collectors.uniqueIndex(OrganizationDto::getUuid)); - - return buildResponse(request, searchResults, organizationsByUuid); + String organizationKey = request.getOrganization(); + if (organizationKey == null) { + return handleForAnyOrganization(dbSession, request); + } else { + OrganizationDto organization = checkFoundWithOptional( + dbClient.organizationDao().selectByKey(dbSession, organizationKey), + "No organization for key '%s'", organizationKey); + return handleForOrganization(dbSession, request, organization); + } } } - private SearchResults searchData(DbSession dbSession, SearchProjectsRequest request) { - String filter = firstNonNull(request.getFilter(), ""); + private SearchProjectsWsResponse handleForAnyOrganization(DbSession dbSession, SearchProjectsRequest request) { + SearchResults searchResults = searchData(dbSession, request, null); + Set organizationUuids = searchResults.projects.stream().map(ComponentDto::getOrganizationUuid).collect(toSet()); + Map organizationsByUuid = dbClient.organizationDao().selectByUuids(dbSession, organizationUuids) + .stream() + .collect(Collectors.uniqueIndex(OrganizationDto::getUuid)); + return buildResponse(request, searchResults, organizationsByUuid); + } + + private SearchProjectsWsResponse handleForOrganization(DbSession dbSession, SearchProjectsRequest request, OrganizationDto organization) { + SearchResults searchResults = searchData(dbSession, request, organization); + return buildResponse(request, searchResults, ImmutableMap.of(organization.getUuid(), organization)); + } + + private SearchResults searchData(DbSession dbSession, SearchProjectsRequest request, @Nullable OrganizationDto organization) { + List criteria = toCriteria(firstNonNull(request.getFilter(), "")); - Set favoriteProjectUuids = searchFavoriteProjects(dbSession); + List favoriteProjects = searchFavoriteProjects(dbSession); + Set projectUuids = buildFilterOnProjectUuids(dbSession, criteria, favoriteProjects, organization); - ProjectMeasuresQuery query = newProjectMeasuresQuery(filter, favoriteProjectUuids); + ProjectMeasuresQuery query = newProjectMeasuresQuery(criteria, projectUuids); queryValidator.validate(dbSession, query); SearchIdResult esResults = index.search(query, new SearchOptions() @@ -155,27 +184,60 @@ public class SearchProjectsAction implements ComponentsWsAction { Ordering ordering = Ordering.explicit(esResults.getIds()).onResultOf(ComponentDto::uuid); List projects = ordering.immutableSortedCopy(dbClient.componentDao().selectByUuids(dbSession, esResults.getIds())); - return new SearchResults(projects, favoriteProjectUuids, esResults); + return new SearchResults(projects, favoriteProjects.stream().map(ComponentDto::uuid).collect(toSet()), esResults); + } + + /** + * Builds the set of project uuid on which the query on index measure should be filtering. + *
    + *
  • if neither isFavourite criterion nor an organization is specified, there is not filtering on projects at all
  • + *
  • if isFavourite criterion and an organization are specified, filtering is done on favourite projects of + * the user which belong to the specified organization
  • + *
  • if only isFavourite criterion is specified, filtering is done on favourite projects of the user
  • + *
  • if only an organization is specified, filtering is done on the projects of this organization
  • + *
+ */ + @CheckForNull + private Set buildFilterOnProjectUuids(DbSession dbSession, List criteria, List favoriteProjects, @Nullable OrganizationDto organization) { + boolean hasIsFavouriteCriterion = hasIsFavouriteCriterion(criteria); + if (hasIsFavouriteCriterion && organization != null) { + return favoriteProjects.stream() + .filter(project -> project.getOrganizationUuid().equals(organization.getUuid())) + .map(ComponentDto::uuid) + .collect(toSet()); + } + if (hasIsFavouriteCriterion) { + return favoriteProjects.stream() + .map(ComponentDto::uuid) + .collect(toSet()); + } + if (organization != null) { + return dbClient.componentDao().selectAllRootsByOrganization(dbSession, organization.getUuid()) + .stream() + .filter(componentDto -> Qualifiers.PROJECT.equals(componentDto.qualifier())) + .map(ComponentDto::uuid) + .collect(toSet()); + } + return null; } - private Set searchFavoriteProjects(DbSession dbSession) { - List favoriteDbIds = dbClient.propertiesDao().selectByQuery(PropertyQuery.builder() - .setUserId(userSession.getUserId()) - .setKey("favourite") - .build(), dbSession) + private List searchFavoriteProjects(DbSession dbSession) { + List favoriteDbIds = dbClient.propertiesDao().selectByQuery( + PropertyQuery.builder() + .setUserId(userSession.getUserId()) + .setKey("favourite") + .build(), + dbSession) .stream() .map(PropertyDto::getResourceId) .collect(Collectors.toList()); - return dbClient.componentDao().selectByIds(dbSession, favoriteDbIds) - .stream() - .filter(dbComponent -> Qualifiers.PROJECT.equals(dbComponent.qualifier())) - .map(ComponentDto::uuid) - .collect(Collectors.toSet()); + return dbClient.componentDao().selectByIds(dbSession, favoriteDbIds); } private static SearchProjectsRequest toRequest(Request httpRequest) { SearchProjectsRequest.Builder request = SearchProjectsRequest.builder() + .setOrganization(httpRequest.param(PARAM_ORGANIZATION)) .setFilter(httpRequest.param(PARAM_FILTER)) .setPage(httpRequest.mandatoryParamAsInt(Param.PAGE)) .setPageSize(httpRequest.mandatoryParamAsInt(Param.PAGE_SIZE)); 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 ea80e3c4273..6253d59e439 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 @@ -20,16 +20,19 @@ package org.sonar.server.component.ws; +import java.util.Collections; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.sonar.server.measure.index.ProjectMeasuresQuery; import org.sonar.server.tester.UserSessionRule; +import static java.util.Collections.emptyList; import static java.util.Collections.emptySet; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; import static org.sonar.server.component.ws.ProjectMeasuresQueryFactory.newProjectMeasuresQuery; +import static org.sonar.server.component.ws.ProjectMeasuresQueryFactory.toCriteria; import static org.sonar.server.computation.task.projectanalysis.measure.Measure.Level.OK; import static org.sonar.server.measure.index.ProjectMeasuresQuery.MetricCriterion; import static org.sonar.server.measure.index.ProjectMeasuresQuery.Operator; @@ -43,7 +46,7 @@ public class ProjectMeasuresQueryFactoryTest { @Test public void create_query() throws Exception { - ProjectMeasuresQuery query = newProjectMeasuresQuery("ncloc > 10 and coverage <= 80", emptySet()); + ProjectMeasuresQuery query = newProjectMeasuresQuery(toCriteria("ncloc > 10 and coverage <= 80"), emptySet()); assertThat(query.getMetricCriteria()) .extracting(MetricCriterion::getMetricKey, MetricCriterion::getOperator, MetricCriterion::getValue) @@ -54,7 +57,7 @@ public class ProjectMeasuresQueryFactoryTest { @Test public void create_query_having_lesser_than_operation() throws Exception { - ProjectMeasuresQuery query = newProjectMeasuresQuery("ncloc < 10", emptySet()); + ProjectMeasuresQuery query = newProjectMeasuresQuery(toCriteria("ncloc < 10"), emptySet()); assertThat(query.getMetricCriteria()) .extracting(MetricCriterion::getMetricKey, MetricCriterion::getOperator, MetricCriterion::getValue) @@ -63,7 +66,7 @@ public class ProjectMeasuresQueryFactoryTest { @Test public void create_query_having_lesser_than_or_equals_operation() throws Exception { - ProjectMeasuresQuery query = newProjectMeasuresQuery("ncloc <= 10", emptySet()); + ProjectMeasuresQuery query = newProjectMeasuresQuery(toCriteria("ncloc <= 10"), emptySet()); assertThat(query.getMetricCriteria()) .extracting(MetricCriterion::getMetricKey, MetricCriterion::getOperator, MetricCriterion::getValue) @@ -72,7 +75,7 @@ public class ProjectMeasuresQueryFactoryTest { @Test public void create_query_having_greater_than_operation() throws Exception { - ProjectMeasuresQuery query = newProjectMeasuresQuery("ncloc > 10", emptySet()); + ProjectMeasuresQuery query = newProjectMeasuresQuery(toCriteria("ncloc > 10"), emptySet()); assertThat(query.getMetricCriteria()) .extracting(MetricCriterion::getMetricKey, MetricCriterion::getOperator, MetricCriterion::getValue) @@ -81,7 +84,7 @@ public class ProjectMeasuresQueryFactoryTest { @Test public void create_query_having_greater_than_or_equals_operation() throws Exception { - ProjectMeasuresQuery query = newProjectMeasuresQuery("ncloc >= 10", emptySet()); + ProjectMeasuresQuery query = newProjectMeasuresQuery(toCriteria("ncloc >= 10"), emptySet()); assertThat(query.getMetricCriteria()) .extracting(MetricCriterion::getMetricKey, MetricCriterion::getOperator, MetricCriterion::getValue) @@ -90,7 +93,7 @@ public class ProjectMeasuresQueryFactoryTest { @Test public void create_query_having_equal_operation() throws Exception { - ProjectMeasuresQuery query = newProjectMeasuresQuery("ncloc = 10", emptySet()); + ProjectMeasuresQuery query = newProjectMeasuresQuery(toCriteria("ncloc = 10"), emptySet()); assertThat(query.getMetricCriteria()) .extracting(MetricCriterion::getMetricKey, MetricCriterion::getOperator, MetricCriterion::getValue) @@ -99,21 +102,42 @@ public class ProjectMeasuresQueryFactoryTest { @Test public void create_query_on_quality_gate() throws Exception { - ProjectMeasuresQuery query = newProjectMeasuresQuery("alert_status = OK", emptySet()); + ProjectMeasuresQuery query = newProjectMeasuresQuery(toCriteria("alert_status = OK"), emptySet()); assertThat(query.getQualityGateStatus().name()).isEqualTo(OK.name()); } @Test - public void query_without_favorites_by_default() { - ProjectMeasuresQuery query = newProjectMeasuresQuery("ncloc = 10", emptySet()); + public void do_not_filter_on_projectUuids_if_criteria_non_empty_and_projectUuid_is_null() { + ProjectMeasuresQuery query = newProjectMeasuresQuery(toCriteria("ncloc = 10"), null); assertThat(query.doesFilterOnProjectUuids()).isFalse(); } @Test - public void create_query_with_favorites() throws Exception { - ProjectMeasuresQuery query = newProjectMeasuresQuery("isFavorite", emptySet()); + public void filter_on_projectUuids_if_projectUuid_is_empty_and_criteria_non_empty() throws Exception { + ProjectMeasuresQuery query = newProjectMeasuresQuery(toCriteria("ncloc > 10"), emptySet()); + + assertThat(query.doesFilterOnProjectUuids()).isTrue(); + } + + @Test + public void filter_on_projectUuids_if_projectUuid_is_non_empty_and_criteria_non_empty() throws Exception { + ProjectMeasuresQuery query = newProjectMeasuresQuery(toCriteria("ncloc > 10"), Collections.singleton("foo")); + + assertThat(query.doesFilterOnProjectUuids()).isTrue(); + } + + @Test + public void filter_on_projectUuids_if_projectUuid_is_empty_and_criteria_is_empty() throws Exception { + ProjectMeasuresQuery query = newProjectMeasuresQuery(emptyList(), emptySet()); + + assertThat(query.doesFilterOnProjectUuids()).isTrue(); + } + + @Test + public void filter_on_projectUuids_if_projectUuid_is_non_empty_and_criteria_empty() throws Exception { + ProjectMeasuresQuery query = newProjectMeasuresQuery(emptyList(), Collections.singleton("foo")); assertThat(query.doesFilterOnProjectUuids()).isTrue(); } @@ -121,17 +145,17 @@ public class ProjectMeasuresQueryFactoryTest { @Test public void fail_to_create_query_on_quality_gate_when_operator_is_not_equal() throws Exception { expectedException.expect(IllegalArgumentException.class); - newProjectMeasuresQuery("alert_status > OK", emptySet()); + newProjectMeasuresQuery(toCriteria("alert_status > OK"), emptySet()); } @Test public void search_is_case_insensitive() throws Exception { - assertThat(newProjectMeasuresQuery("ncloc > 10 AnD coverage <= 80 AND debt = 10 AND issues = 20", emptySet()).getMetricCriteria()).hasSize(4); + assertThat(newProjectMeasuresQuery(toCriteria("ncloc > 10 AnD coverage <= 80 AND debt = 10 AND issues = 20"), emptySet()).getMetricCriteria()).hasSize(4); } @Test public void convert_metric_to_lower_case() throws Exception { - assertThat(newProjectMeasuresQuery("NCLOC > 10 AND coVERage <= 80", emptySet()).getMetricCriteria()) + assertThat(newProjectMeasuresQuery(toCriteria("NCLOC > 10 AND coVERage <= 80"), emptySet()).getMetricCriteria()) .extracting(MetricCriterion::getMetricKey, MetricCriterion::getOperator, MetricCriterion::getValue) .containsOnly( tuple("ncloc", Operator.GT, 10d), @@ -140,14 +164,14 @@ public class ProjectMeasuresQueryFactoryTest { @Test public void ignore_white_spaces() throws Exception { - assertThat(newProjectMeasuresQuery(" ncloc > 10 ", emptySet()).getMetricCriteria()) + assertThat(newProjectMeasuresQuery(toCriteria(" ncloc > 10 "), emptySet()).getMetricCriteria()) .extracting(MetricCriterion::getMetricKey, MetricCriterion::getOperator, MetricCriterion::getValue) .containsOnly(tuple("ncloc", Operator.GT, 10d)); } @Test public void accept_empty_query() throws Exception { - ProjectMeasuresQuery result = newProjectMeasuresQuery("", emptySet()); + ProjectMeasuresQuery result = newProjectMeasuresQuery(emptyList(), emptySet()); assertThat(result.getMetricCriteria()).isEmpty(); } @@ -156,7 +180,8 @@ public class ProjectMeasuresQueryFactoryTest { public void fail_on_invalid_criteria() throws Exception { expectedException.expect(IllegalArgumentException.class); expectedException.expectMessage("Invalid criterion 'ncloc ? 10'"); - newProjectMeasuresQuery("ncloc ? 10", emptySet()); + + newProjectMeasuresQuery(toCriteria("ncloc ? 10"), emptySet()); } @Test @@ -164,34 +189,34 @@ public class ProjectMeasuresQueryFactoryTest { expectedException.expect(IllegalArgumentException.class); expectedException.expectMessage("Invalid criterion 'ncloc ? 10'"); - newProjectMeasuresQuery(" ncloc ? 10 ", emptySet()); + newProjectMeasuresQuery(toCriteria(" ncloc ? 10 "), emptySet()); } @Test public void fail_when_not_double() throws Exception { expectedException.expect(IllegalArgumentException.class); expectedException.expectMessage("Invalid criterion 'ncloc > ten'"); - newProjectMeasuresQuery("ncloc > ten", emptySet()); + newProjectMeasuresQuery(toCriteria("ncloc > ten"), emptySet()); } @Test public void fail_when_no_operator() throws Exception { expectedException.expect(IllegalArgumentException.class); expectedException.expectMessage("Invalid criterion 'ncloc 10'"); - newProjectMeasuresQuery("ncloc 10", emptySet()); + newProjectMeasuresQuery(toCriteria("ncloc 10"), emptySet()); } @Test public void fail_when_no_key() throws Exception { expectedException.expect(IllegalArgumentException.class); expectedException.expectMessage("Invalid criterion '>= 10'"); - newProjectMeasuresQuery(">= 10", emptySet()); + newProjectMeasuresQuery(toCriteria(">= 10"), emptySet()); } @Test public void fail_when_no_value() throws Exception { expectedException.expect(IllegalArgumentException.class); expectedException.expectMessage("Invalid criterion 'ncloc >='"); - newProjectMeasuresQuery("ncloc >=", emptySet()); + newProjectMeasuresQuery(toCriteria("ncloc >="), emptySet()); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/ws/ProjectMeasuresQueryValidatorTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/ws/ProjectMeasuresQueryValidatorTest.java index 7aa322f0108..e3fb204883d 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/component/ws/ProjectMeasuresQueryValidatorTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/component/ws/ProjectMeasuresQueryValidatorTest.java @@ -30,6 +30,7 @@ import org.sonar.db.DbTester; import org.sonar.db.metric.MetricDto; import org.sonar.server.tester.UserSessionRule; +import static java.util.Collections.emptyList; import static java.util.Collections.emptySet; import static org.sonar.api.measures.Metric.ValueType.DATA; import static org.sonar.api.measures.Metric.ValueType.DISTRIB; @@ -38,6 +39,7 @@ import static org.sonar.api.measures.Metric.ValueType.STRING; import static org.sonar.api.measures.Metric.ValueType.WORK_DUR; import static org.sonar.db.metric.MetricTesting.newMetricDto; import static org.sonar.server.component.ws.ProjectMeasuresQueryFactory.newProjectMeasuresQuery; +import static org.sonar.server.component.ws.ProjectMeasuresQueryFactory.toCriteria; public class ProjectMeasuresQueryValidatorTest { @@ -57,14 +59,14 @@ public class ProjectMeasuresQueryValidatorTest { @Test public void query_with_empty_metrics_is_valid() throws Exception { - underTest.validate(dbSession, newProjectMeasuresQuery("", emptySet())); + underTest.validate(dbSession, newProjectMeasuresQuery(emptyList(), emptySet())); } @Test public void does_not_fail_when_metric_criteria_contains_an_existing_metric() throws Exception { insertValidMetric("ncloc"); - underTest.validate(dbSession, newProjectMeasuresQuery("ncloc > 10", emptySet())); + underTest.validate(dbSession, newProjectMeasuresQuery(toCriteria("ncloc > 10"), emptySet())); } @Test @@ -77,7 +79,7 @@ public class ProjectMeasuresQueryValidatorTest { expectedException.expect(IllegalArgumentException.class); expectedException.expectMessage("Following metrics are not numeric : [data, distrib, string]"); - underTest.validate(dbSession, newProjectMeasuresQuery("data > 10 and distrib = 11 and ncloc <= 20 and debt < 30 and string = 40", emptySet())); + underTest.validate(dbSession, newProjectMeasuresQuery(toCriteria("data > 10 and distrib = 11 and ncloc <= 20 and debt < 30 and string = 40"), emptySet())); } @Test @@ -86,7 +88,7 @@ public class ProjectMeasuresQueryValidatorTest { expectedException.expect(IllegalArgumentException.class); expectedException.expectMessage("Following metrics are disabled : [ncloc]"); - underTest.validate(dbSession, newProjectMeasuresQuery("ncloc > 10", emptySet())); + underTest.validate(dbSession, newProjectMeasuresQuery(toCriteria("ncloc > 10"), emptySet())); } @Test @@ -95,7 +97,7 @@ public class ProjectMeasuresQueryValidatorTest { expectedException.expect(IllegalArgumentException.class); expectedException.expectMessage("Unknown metric(s) [unknown]"); - underTest.validate(dbSession, newProjectMeasuresQuery("unknown > 10", emptySet())); + underTest.validate(dbSession, newProjectMeasuresQuery(toCriteria("unknown > 10"), emptySet())); } @Test @@ -104,7 +106,7 @@ public class ProjectMeasuresQueryValidatorTest { expectedException.expect(IllegalArgumentException.class); expectedException.expectMessage("Unknown metric(s) [coverage, debt]"); - underTest.validate(dbSession, newProjectMeasuresQuery("debt > 10 AND ncloc <= 20 AND coverage > 30", emptySet())); + underTest.validate(dbSession, newProjectMeasuresQuery(toCriteria("debt > 10 AND ncloc <= 20 AND coverage > 30"), emptySet())); } private void insertValidMetric(String metricKey) { 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 dbe896fa0b5..726114b0bba 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 @@ -28,6 +28,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.stream.IntStream; +import java.util.stream.Stream; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -76,10 +77,12 @@ import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.INDE import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.TYPE_PROJECT_MEASURE; import static org.sonar.test.JsonAssert.assertJson; import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_FILTER; +import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_ORGANIZATION; public class SearchProjectsActionTest { private static final String NCLOC = "ncloc"; private static final String COVERAGE = "coverage"; + private static final String IS_FAVOURITE_CRITERION = "isFavorite"; @Rule public ExpectedException expectedException = ExpectedException.none(); @@ -103,11 +106,26 @@ public class SearchProjectsActionTest { private SearchProjectsRequest.Builder request = SearchProjectsRequest.builder(); + @Test + public void verify_definition() { + WebService.Action def = ws.getDef(); + + assertThat(def.key()).isEqualTo("search_projects"); + assertThat(def.since()).isEqualTo("6.2"); + assertThat(def.isInternal()).isTrue(); + assertThat(def.isPost()).isFalse(); + assertThat(def.responseExampleAsString()).isNotEmpty(); + Param organization = def.param("organization"); + assertThat(organization.isRequired()).isFalse(); + assertThat(organization.description()).isEqualTo("the organization to search projects in"); + assertThat(organization.since()).isEqualTo("6.3"); + } + @Test public void json_example() { OrganizationDto organization1Dto = db.organizations().insertForKey("my-org-key-1"); OrganizationDto organization2Dto = db.organizations().insertForKey("my-org-key-2"); - long project1Id = insertProjectInDbAndEs(newProjectDto(organization1Dto) + ComponentDto project1 = insertProjectInDbAndEs(newProjectDto(organization1Dto) .setUuid(Uuids.UUID_EXAMPLE_01) .setKey(KeyExamples.KEY_PROJECT_EXAMPLE_001) .setName("My Project 1")); @@ -120,7 +138,7 @@ public class SearchProjectsActionTest { .setKey(KeyExamples.KEY_PROJECT_EXAMPLE_003) .setName("My Project 3")); userSession.login().setUserId(23); - dbClient.propertiesDao().saveProperty(dbSession, new PropertyDto().setKey("favourite").setResourceId(project1Id).setUserId(23L)); + addFavourite(project1); dbSession.commit(); String result = ws.newRequest().execute().getInput(); @@ -195,13 +213,83 @@ public class SearchProjectsActionTest { assertThat(result.getComponents(0).getName()).isEqualTo("Sonar Markdown"); } + @Test + public void filter_projects_with_query_within_specified_organization() { + OrganizationDto organization1 = db.organizations().insert(); + OrganizationDto organization2 = db.organizations().insert(); + insertProjectInDbAndEs(newProjectDto(organization1).setName("Sonar Java"), newArrayList(newMeasure(COVERAGE, 81), newMeasure(NCLOC, 10_000d))); + insertProjectInDbAndEs(newProjectDto(organization1).setName("Sonar Markdown"), newArrayList(newMeasure(COVERAGE, 80d), newMeasure(NCLOC, 10_000d))); + insertProjectInDbAndEs(newProjectDto(organization2).setName("Sonar Qube"), newArrayList(newMeasure(COVERAGE, 80d), newMeasure(NCLOC, 10_001d))); + insertMetrics(COVERAGE, NCLOC); + + assertThat(call(request.setOrganization(null)).getComponentsList()) + .extracting(Component::getName) + .containsOnly("Sonar Java", "Sonar Markdown", "Sonar Qube"); + assertThat(call(request.setOrganization(organization1.getKey())).getComponentsList()) + .extracting(Component::getName) + .containsOnly("Sonar Java", "Sonar Markdown"); + assertThat(call(request.setOrganization(organization2.getKey())).getComponentsList()) + .extracting(Component::getName) + .containsOnly("Sonar Qube"); + } + + @Test + public void filter_favourite_projects_with_query_with_or_without_a_specified_organization() { + OrganizationDto organization1 = db.organizations().insert(); + OrganizationDto organization2 = db.organizations().insert(); + OrganizationDto organization3 = db.organizations().insert(); + OrganizationDto organization4 = db.organizations().insert(); + OrganizationDto organization5 = db.organizations().insert(); + List> someMeasure = singletonList(newMeasure(COVERAGE, 81)); + ComponentDto favourite1_1 = insertProjectInDbAndEs(newProjectDto(organization1), someMeasure); + ComponentDto favourite1_2 = insertProjectInDbAndEs(newProjectDto(organization1), someMeasure); + ComponentDto nonFavourite1 = insertProjectInDbAndEs(newProjectDto(organization1), someMeasure); + ComponentDto favourite2 = insertProjectInDbAndEs(newProjectDto(organization2), someMeasure); + ComponentDto nonFavourite2 = insertProjectInDbAndEs(newProjectDto(organization2), someMeasure); + ComponentDto favourite3 = insertProjectInDbAndEs(newProjectDto(organization3), someMeasure); + ComponentDto nonFavourite4 = insertProjectInDbAndEs(newProjectDto(organization4), someMeasure); + Stream.of(favourite1_1, favourite1_2, favourite2, favourite3) + .forEach(this::addFavourite); + insertMetrics(COVERAGE, NCLOC); + + assertThat(call(request.setFilter(null).setOrganization(null)).getComponentsList()) + .extracting(Component::getName) + .containsOnly(favourite1_1.name(), favourite1_2.name(), nonFavourite1.name(), favourite2.name(), nonFavourite2.name(), favourite3.name(), nonFavourite4.name()); + assertThat(call(request.setFilter(IS_FAVOURITE_CRITERION).setOrganization(null)).getComponentsList()) + .extracting(Component::getName) + .containsOnly(favourite1_1.name(), favourite1_2.name(), favourite2.name(), favourite3.name()); + assertThat(call(request.setFilter(null).setOrganization(organization1.getKey())).getComponentsList()) + .extracting(Component::getName) + .containsOnly(favourite1_1.name(), favourite1_2.name(), nonFavourite1.name()); + assertThat(call(request.setFilter(IS_FAVOURITE_CRITERION).setOrganization(organization1.getKey())).getComponentsList()) + .extracting(Component::getName) + .containsOnly(favourite1_1.name(), favourite1_2.name()); + assertThat(call(request.setFilter(null).setOrganization(organization3.getKey())).getComponentsList()) + .extracting(Component::getName) + .containsOnly(favourite3.name()); + assertThat(call(request.setFilter(IS_FAVOURITE_CRITERION).setOrganization(organization3.getKey())).getComponentsList()) + .extracting(Component::getName) + .containsOnly(favourite3.name()); + assertThat(call(request.setFilter(null).setOrganization(organization4.getKey())).getComponentsList()) + .extracting(Component::getName) + .containsOnly(nonFavourite4.name()); + assertThat(call(request.setFilter(IS_FAVOURITE_CRITERION).setOrganization(organization4.getKey())).getComponentsList()) + .isEmpty(); + assertThat(call(request.setFilter(null).setOrganization(organization5.getKey())).getComponentsList()) + .isEmpty(); + assertThat(call(request.setFilter(IS_FAVOURITE_CRITERION).setOrganization(organization5.getKey())).getComponentsList()) + .isEmpty(); + } + @Test public void filter_projects_on_favorites() { - long javaId = insertProjectInDbAndEs(newProjectDto(db.getDefaultOrganization(), "java-id").setName("Sonar Java"), newArrayList(newMeasure(COVERAGE, 81), newMeasure(NCLOC, 10_000d))); - long markDownId = insertProjectInDbAndEs(newProjectDto(db.getDefaultOrganization(), "markdown-id").setName("Sonar Markdown"), newArrayList(newMeasure(COVERAGE, 80d), newMeasure(NCLOC, 10_000d))); + ComponentDto javaProject = insertProjectInDbAndEs(newProjectDto(db.getDefaultOrganization(), "java-id").setName("Sonar Java"), + newArrayList(newMeasure(COVERAGE, 81), newMeasure(NCLOC, 10_000d))); + ComponentDto markDownProject = insertProjectInDbAndEs(newProjectDto(db.getDefaultOrganization(), "markdown-id").setName("Sonar Markdown"), + newArrayList(newMeasure(COVERAGE, 80d), newMeasure(NCLOC, 10_000d))); insertProjectInDbAndEs(newProjectDto(db.organizations().insert()).setName("Sonar Qube"), newArrayList(newMeasure(COVERAGE, 80d), newMeasure(NCLOC, 10_001d))); - dbClient.propertiesDao().saveProperty(dbSession, new PropertyDto().setKey("favourite").setResourceId(javaId).setUserId(Long.valueOf(userSession.getUserId()))); - dbClient.propertiesDao().saveProperty(dbSession, new PropertyDto().setKey("favourite").setResourceId(markDownId).setUserId(Long.valueOf(userSession.getUserId()))); + addFavourite(javaProject); + addFavourite(markDownProject); dbSession.commit(); request.setFilter("isFavorite"); @@ -237,10 +325,11 @@ public class SearchProjectsActionTest { @Test public void return_nloc_facet() { - insertProjectInDbAndEs(newProjectDto(db.getDefaultOrganization()).setName("Sonar Java"), newArrayList(newMeasure(COVERAGE, 81), newMeasure(NCLOC, 5d))); - insertProjectInDbAndEs(newProjectDto(db.getDefaultOrganization()).setName("Sonar Groovy"), newArrayList(newMeasure(COVERAGE, 81), newMeasure(NCLOC, 5d))); - insertProjectInDbAndEs(newProjectDto(db.getDefaultOrganization()).setName("Sonar Markdown"), newArrayList(newMeasure(COVERAGE, 80d), newMeasure(NCLOC, 10_000d))); - insertProjectInDbAndEs(newProjectDto(db.getDefaultOrganization()).setName("Sonar Qube"), newArrayList(newMeasure(COVERAGE, 80d), newMeasure(NCLOC, 500_001d))); + OrganizationDto organization = db.getDefaultOrganization(); + insertProjectInDbAndEs(newProjectDto(organization).setName("Sonar Java"), newArrayList(newMeasure(COVERAGE, 81), newMeasure(NCLOC, 5d))); + insertProjectInDbAndEs(newProjectDto(organization).setName("Sonar Groovy"), newArrayList(newMeasure(COVERAGE, 81), newMeasure(NCLOC, 5d))); + insertProjectInDbAndEs(newProjectDto(organization).setName("Sonar Markdown"), newArrayList(newMeasure(COVERAGE, 80d), newMeasure(NCLOC, 10_000d))); + insertProjectInDbAndEs(newProjectDto(organization).setName("Sonar Qube"), newArrayList(newMeasure(COVERAGE, 80d), newMeasure(NCLOC, 500_001d))); insertMetrics(COVERAGE, NCLOC); SearchProjectsWsResponse result = call(request.setFacets(singletonList(NCLOC))); @@ -281,6 +370,10 @@ public class SearchProjectsActionTest { TestRequest httpRequest = ws.newRequest() .setMediaType(MediaTypes.PROTOBUF); + String organization = wsRequest.getOrganization(); + if (organization != null) { + httpRequest.setParam(PARAM_ORGANIZATION, organization); + } httpRequest.setParam(Param.PAGE, String.valueOf(wsRequest.getPage())); httpRequest.setParam(Param.PAGE_SIZE, String.valueOf(wsRequest.getPageSize())); String filter = wsRequest.getFilter(); @@ -296,23 +389,12 @@ public class SearchProjectsActionTest { } } - @Test - public void definition() { - WebService.Action def = ws.getDef(); - - assertThat(def.key()).isEqualTo("search_projects"); - assertThat(def.since()).isEqualTo("6.2"); - assertThat(def.isInternal()).isTrue(); - assertThat(def.isPost()).isFalse(); - assertThat(def.responseExampleAsString()).isNotEmpty(); - } - - private long insertProjectInDbAndEs(ComponentDto project) { + private ComponentDto insertProjectInDbAndEs(ComponentDto project) { return insertProjectInDbAndEs(project, emptyList()); } - private long insertProjectInDbAndEs(ComponentDto project, List> measures) { - componentDb.insertComponent(project); + private ComponentDto insertProjectInDbAndEs(ComponentDto project, List> measures) { + ComponentDto res = componentDb.insertComponent(project); try { es.putDocuments(INDEX_PROJECT_MEASURES, TYPE_PROJECT_MEASURE, new ProjectMeasuresDoc().setId(project.uuid()).setKey(project.key()).setName(project.name()).setMeasures(measures)); @@ -321,7 +403,7 @@ public class SearchProjectsActionTest { Throwables.propagate(e); } - return project.getId(); + return res; } private void insertMetrics(String... metricKeys) { @@ -334,4 +416,8 @@ public class SearchProjectsActionTest { private static Map newMeasure(String key, double value) { return ImmutableMap.of("key", key, "value", value); } + + private void addFavourite(ComponentDto project) { + dbClient.propertiesDao().saveProperty(dbSession, new PropertyDto().setKey("favourite").setResourceId(project.getId()).setUserId(Long.valueOf(userSession.getUserId()))); + } } diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/component/ComponentsService.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/component/ComponentsService.java index eb648f24109..bd555b39a44 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/component/ComponentsService.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/component/ComponentsService.java @@ -45,6 +45,7 @@ import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_FRO import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_ID; import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_KEY; import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_NEW_KEY; +import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_ORGANIZATION; import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_QUALIFIERS; import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_STRATEGY; import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_TO; @@ -105,6 +106,7 @@ public class ComponentsService extends BaseService { public SearchProjectsWsResponse searchProjects(SearchProjectsRequest request) { GetRequest get = new GetRequest(path(ACTION_SEARCH_PROJECTS)) + .setParam(PARAM_ORGANIZATION, request.getOrganization()) .setParam(PARAM_FILTER, request.getFilter()) .setParam(Param.FACETS, request.getFacets()) .setParam(Param.PAGE, request.getPage()) diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/component/ComponentsWsParameters.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/component/ComponentsWsParameters.java index 76487243281..16ae5feb354 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/component/ComponentsWsParameters.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/component/ComponentsWsParameters.java @@ -33,6 +33,7 @@ public class ComponentsWsParameters { public static final String ACTION_SUGGESTIONS = "suggestions"; // parameters + public static final String PARAM_ORGANIZATION = "organization"; public static final String PARAM_QUALIFIERS = "qualifiers"; public static final String PARAM_LANGUAGE = "language"; public static final String PARAM_BASE_COMPONENT_ID = "baseComponentId"; diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/component/SearchProjectsRequest.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/component/SearchProjectsRequest.java index 3b6ebceeceb..c30dd9655ec 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/component/SearchProjectsRequest.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/component/SearchProjectsRequest.java @@ -23,6 +23,7 @@ package org.sonarqube.ws.client.component; import java.util.ArrayList; import java.util.List; import javax.annotation.CheckForNull; +import javax.annotation.Nullable; import static com.google.common.base.Preconditions.checkArgument; import static java.util.Objects.requireNonNull; @@ -33,16 +34,23 @@ public class SearchProjectsRequest { private final int page; private final int pageSize; + private final String organization; private final String filter; private final List facets; private SearchProjectsRequest(Builder builder) { this.page = builder.page; this.pageSize = builder.pageSize; + this.organization = builder.organization; this.filter = builder.filter; this.facets = builder.facets; } + @CheckForNull + public String getOrganization() { + return organization; + } + @CheckForNull public String getFilter() { return filter; @@ -65,6 +73,7 @@ public class SearchProjectsRequest { } public static class Builder { + private String organization; private Integer page; private Integer pageSize; private String filter; @@ -74,7 +83,12 @@ public class SearchProjectsRequest { // enforce static factory method } - public Builder setFilter(String filter) { + public Builder setOrganization(@Nullable String organization) { + this.organization = organization; + return this; + } + + public Builder setFilter(@Nullable String filter) { this.filter = filter; return this; } -- 2.39.5