diff options
author | Jacek <jacek.poreda@sonarsource.com> | 2020-03-26 17:07:19 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2020-04-15 20:03:38 +0000 |
commit | bd73de80860fda06408f7c342ba7455250b16151 (patch) | |
tree | 59eea7785a4847404f5bbb649a4db165f9bf2b81 /server | |
parent | 32c274e7d74a040b2895c613724fb89e0fc704cf (diff) | |
download | sonarqube-bd73de80860fda06408f7c342ba7455250b16151.tar.gz sonarqube-bd73de80860fda06408f7c342ba7455250b16151.zip |
SONAR-13188 store qualifier in project measure index
- return applications in api/components/search_projects
Diffstat (limited to 'server')
15 files changed, 500 insertions, 61 deletions
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/measure/ProjectMeasuresIndexerIterator.java b/server/sonar-db-dao/src/main/java/org/sonar/db/measure/ProjectMeasuresIndexerIterator.java index af5254ebe41..18daceed44b 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/measure/ProjectMeasuresIndexerIterator.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/measure/ProjectMeasuresIndexerIterator.java @@ -72,10 +72,10 @@ public class ProjectMeasuresIndexerIterator extends CloseableIterator<ProjectMea CoreMetrics.NEW_LINES_KEY, CoreMetrics.NEW_RELIABILITY_RATING_KEY); - private static final String SQL_PROJECTS = "SELECT p.organization_uuid, p.uuid, p.kee, p.name, s.created_at, p.tags " + + private static final String SQL_PROJECTS = "SELECT p.organization_uuid, p.uuid, p.kee, p.name, s.created_at, p.tags, p.qualifier " + "FROM projects p " + "LEFT OUTER JOIN snapshots s ON s.component_uuid=p.uuid AND s.islast=? " + - "WHERE p.qualifier=?"; + "WHERE p.qualifier in (?, ?)"; private static final String PROJECT_FILTER = " AND p.uuid=?"; @@ -116,7 +116,8 @@ public class ProjectMeasuresIndexerIterator extends CloseableIterator<ProjectMea String name = rs.getString(4); Long analysisDate = DatabaseUtils.getLong(rs, 5); List<String> tags = readDbTags(DatabaseUtils.getString(rs, 6)); - Project project = new Project(orgUuid, uuid, key, name, tags, analysisDate); + String qualifier = rs.getString(7); + Project project = new Project(orgUuid, uuid, key, name, qualifier, tags, analysisDate); projects.add(project); } return projects; @@ -134,8 +135,9 @@ public class ProjectMeasuresIndexerIterator extends CloseableIterator<ProjectMea PreparedStatement stmt = session.getConnection().prepareStatement(sql.toString()); stmt.setBoolean(1, true); stmt.setString(2, Qualifiers.PROJECT); + stmt.setString(3, Qualifiers.APP); if (projectUuid != null) { - stmt.setString(3, projectUuid); + stmt.setString(4, projectUuid); } return stmt; } catch (SQLException e) { @@ -230,14 +232,16 @@ public class ProjectMeasuresIndexerIterator extends CloseableIterator<ProjectMea private final String uuid; private final String key; private final String name; + private final String qualifier; private final Long analysisDate; private final List<String> tags; - public Project(String organizationUuid, String uuid, String key, String name, List<String> tags, @Nullable Long analysisDate) { + public Project(String organizationUuid, String uuid, String key, String name, String qualifier, List<String> tags, @Nullable Long analysisDate) { this.organizationUuid = organizationUuid; this.uuid = uuid; this.key = key; this.name = name; + this.qualifier = qualifier; this.tags = tags; this.analysisDate = analysisDate; } @@ -258,6 +262,10 @@ public class ProjectMeasuresIndexerIterator extends CloseableIterator<ProjectMea return name; } + public String getQualifier() { + return qualifier; + } + public List<String> getTags() { return tags; } diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/measure/ProjectMeasuresIndexerIteratorTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/measure/ProjectMeasuresIndexerIteratorTest.java index 6549101fed6..1dbdd3aa301 100644 --- a/server/sonar-db-dao/src/test/java/org/sonar/db/measure/ProjectMeasuresIndexerIteratorTest.java +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/measure/ProjectMeasuresIndexerIteratorTest.java @@ -22,7 +22,6 @@ package org.sonar.db.measure; import com.google.common.collect.Maps; import java.util.Map; import javax.annotation.Nullable; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -31,12 +30,10 @@ import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.DbTester; import org.sonar.db.component.ComponentDto; -import org.sonar.db.component.ComponentTesting; import org.sonar.db.component.SnapshotDto; import org.sonar.db.measure.ProjectMeasuresIndexerIterator.ProjectMeasures; import org.sonar.db.metric.MetricDto; import org.sonar.db.organization.OrganizationDto; -import org.sonar.db.project.ProjectDto; import static com.google.common.collect.Lists.newArrayList; import static org.assertj.core.api.Assertions.assertThat; @@ -48,7 +45,6 @@ import static org.sonar.api.measures.Metric.ValueType.DISTRIB; import static org.sonar.api.measures.Metric.ValueType.INT; import static org.sonar.api.measures.Metric.ValueType.LEVEL; import static org.sonar.api.measures.Metric.ValueType.STRING; -import static org.sonar.db.component.ComponentTesting.newView; import static org.sonar.db.component.SnapshotTesting.newAnalysis; public class ProjectMeasuresIndexerIteratorTest { @@ -67,7 +63,6 @@ public class ProjectMeasuresIndexerIteratorTest { ComponentDto project = dbTester.components().insertPrivateProject(organization, c -> c.setDbKey("Project-Key").setName("Project Name"), p -> p.setTags(newArrayList("platform", "java"))); - ProjectDto projectDto = dbTester.components().getProjectDto(project); SnapshotDto analysis = dbTester.components().insertSnapshot(project); MetricDto metric1 = dbTester.measures().insertMetric(m -> m.setValueType(INT.name()).setKey("ncloc")); @@ -83,12 +78,37 @@ public class ProjectMeasuresIndexerIteratorTest { assertThat(doc.getProject().getUuid()).isEqualTo(project.uuid()); assertThat(doc.getProject().getKey()).isEqualTo("Project-Key"); assertThat(doc.getProject().getName()).isEqualTo("Project Name"); + assertThat(doc.getProject().getQualifier()).isEqualTo("TRK"); assertThat(doc.getProject().getTags()).containsExactly("platform", "java"); assertThat(doc.getProject().getAnalysisDate()).isNotNull().isEqualTo(analysis.getCreatedAt()); assertThat(doc.getMeasures().getNumericMeasures()).containsOnly(entry(metric1.getKey(), 10d), entry(metric2.getKey(), 20d)); } @Test + public void return_application_measure() { + OrganizationDto organization = dbTester.organizations().insert(); + ComponentDto project = dbTester.components().insertPrivateApplication(organization, + c -> c.setDbKey("App-Key").setName("App Name")); + + SnapshotDto analysis = dbTester.components().insertSnapshot(project); + MetricDto metric1 = dbTester.measures().insertMetric(m -> m.setValueType(INT.name()).setKey("ncloc")); + MetricDto metric2 = dbTester.measures().insertMetric(m -> m.setValueType(INT.name()).setKey("coverage")); + dbTester.measures().insertLiveMeasure(project, metric1, m -> m.setValue(10d)); + dbTester.measures().insertLiveMeasure(project, metric2, m -> m.setValue(20d)); + + Map<String, ProjectMeasures> docsById = createResultSetAndReturnDocsById(); + + assertThat(docsById).hasSize(1); + ProjectMeasures doc = docsById.get(project.uuid()); + assertThat(doc).isNotNull(); + assertThat(doc.getProject().getUuid()).isEqualTo(project.uuid()); + assertThat(doc.getProject().getKey()).isEqualTo("App-Key"); + assertThat(doc.getProject().getName()).isEqualTo("App Name"); + assertThat(doc.getProject().getAnalysisDate()).isNotNull().isEqualTo(analysis.getCreatedAt()); + assertThat(doc.getMeasures().getNumericMeasures()).containsOnly(entry(metric1.getKey(), 10d), entry(metric2.getKey(), 20d)); + } + + @Test public void return_project_measure_having_leak() { OrganizationDto organization = dbTester.organizations().insert(); ComponentDto project = dbTester.components().insertPrivateProject(organization, diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/es/newindex/StringFieldBuilder.java b/server/sonar-server-common/src/main/java/org/sonar/server/es/newindex/StringFieldBuilder.java index f9b7cfeb634..a5d4201982c 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/es/newindex/StringFieldBuilder.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/es/newindex/StringFieldBuilder.java @@ -72,7 +72,7 @@ public abstract class StringFieldBuilder<U extends FieldAware<U>, T extends Stri } /** - * Norms consume useless memory if string field is used for filtering or aggregations. + * Norms consume useless memory if string field is used solely for filtering or aggregations. * * https://www.elastic.co/guide/en/elasticsearch/reference/2.3/norms.html * https://www.elastic.co/guide/en/elasticsearch/guide/current/scoring-theory.html#field-norm diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/measure/index/ProjectMeasuresDoc.java b/server/sonar-server-common/src/main/java/org/sonar/server/measure/index/ProjectMeasuresDoc.java index 73ba62e3c62..30f3451d42a 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/measure/index/ProjectMeasuresDoc.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/measure/index/ProjectMeasuresDoc.java @@ -41,6 +41,7 @@ import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIEL import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_NAME; import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_NCLOC_DISTRIBUTION; import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_ORGANIZATION_UUID; +import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_QUALIFIER; import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_QUALITY_GATE_STATUS; import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_TAGS; import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_UUID; @@ -96,6 +97,15 @@ public class ProjectMeasuresDoc extends BaseDoc { return this; } + public String getQualifier() { + return getField(FIELD_QUALIFIER); + } + + public ProjectMeasuresDoc setQualifier(String s) { + setField(FIELD_QUALIFIER, s); + return this; + } + @CheckForNull public Date getAnalysedAt() { return getNullableField(FIELD_ANALYSED_AT); diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndexDefinition.java b/server/sonar-server-common/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndexDefinition.java index 447042bacfb..cb03590e64a 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndexDefinition.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndexDefinition.java @@ -44,10 +44,11 @@ public class ProjectMeasuresIndexDefinition implements IndexDefinition { public static final String FIELD_ORGANIZATION_UUID = "organizationUuid"; /** - * Project key. Only projects (qualifier=TRK) + * Project key. Only projects and applications (qualifier=TRK, APP) */ public static final String FIELD_KEY = "key"; public static final String FIELD_NAME = "name"; + public static final String FIELD_QUALIFIER = "qualifier"; public static final String FIELD_ANALYSED_AT = "analysedAt"; public static final String FIELD_QUALITY_GATE_STATUS = "qualityGateStatus"; public static final String FIELD_TAGS = "tags"; @@ -97,6 +98,7 @@ public class ProjectMeasuresIndexDefinition implements IndexDefinition { mapping.keywordFieldBuilder(FIELD_UUID).disableNorms().build(); mapping.keywordFieldBuilder(FIELD_ORGANIZATION_UUID).disableNorms().build(); mapping.keywordFieldBuilder(FIELD_KEY).disableNorms().addSubFields(SORTABLE_ANALYZER).build(); + mapping.keywordFieldBuilder(FIELD_QUALIFIER).disableNorms().build(); mapping.keywordFieldBuilder(FIELD_NAME).addSubFields(SORTABLE_ANALYZER, SEARCH_GRAMS_ANALYZER).build(); mapping.keywordFieldBuilder(FIELD_QUALITY_GATE_STATUS).build(); mapping.keywordFieldBuilder(FIELD_TAGS).build(); diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndexer.java b/server/sonar-server-common/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndexer.java index 113b45d8c1d..ce3f21c47ab 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndexer.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndexer.java @@ -51,7 +51,8 @@ import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.TYPE public class ProjectMeasuresIndexer implements ProjectIndexer, NeedAuthorizationIndexer { - private static final AuthorizationScope AUTHORIZATION_SCOPE = new AuthorizationScope(TYPE_PROJECT_MEASURES, project -> Qualifiers.PROJECT.equals(project.getQualifier())); + private static final AuthorizationScope AUTHORIZATION_SCOPE = new AuthorizationScope(TYPE_PROJECT_MEASURES, + project -> Qualifiers.PROJECT.equals(project.getQualifier()) || Qualifiers.APP.equals(project.getQualifier())); private static final ImmutableSet<IndexType> INDEX_TYPES = ImmutableSet.of(TYPE_PROJECT_MEASURES); private final DbClient dbClient; @@ -170,6 +171,7 @@ public class ProjectMeasuresIndexer implements ProjectIndexer, NeedAuthorization .setOrganizationUuid(project.getOrganizationUuid()) .setKey(project.getKey()) .setName(project.getName()) + .setQualifier(project.getQualifier()) .setQualityGateStatus(projectMeasures.getMeasures().getQualityGateStatus()) .setTags(project.getTags()) .setAnalysedAt(analysisDate == null ? null : new Date(analysisDate)) diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexerTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexerTest.java index 1335c5de1ab..da52873dbf5 100644 --- a/server/sonar-server-common/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexerTest.java +++ b/server/sonar-server-common/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexerTest.java @@ -22,10 +22,12 @@ package org.sonar.server.measure.index; import java.util.Arrays; import java.util.Collection; import java.util.function.Consumer; +import java.util.function.Predicate; import org.elasticsearch.action.search.SearchRequestBuilder; import org.elasticsearch.search.SearchHit; import org.junit.Rule; import org.junit.Test; +import org.sonar.api.resources.Qualifiers; import org.sonar.api.utils.System2; import org.sonar.db.DbSession; import org.sonar.db.DbTester; @@ -37,6 +39,8 @@ import org.sonar.db.project.ProjectDto; import org.sonar.server.es.EsTester; import org.sonar.server.es.IndexingResult; import org.sonar.server.es.ProjectIndexer; +import org.sonar.server.permission.index.AuthorizationScope; +import org.sonar.server.permission.index.IndexPermissions; import static java.util.Collections.emptyList; import static java.util.Collections.emptySet; @@ -44,14 +48,18 @@ import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.elasticsearch.index.query.QueryBuilders.boolQuery; import static org.elasticsearch.index.query.QueryBuilders.termQuery; +import static org.elasticsearch.index.query.QueryBuilders.termsQuery; import static org.sonar.db.component.ComponentTesting.newPrivateProjectDto; import static org.sonar.server.es.IndexType.FIELD_INDEX_TYPE; import static org.sonar.server.es.ProjectIndexer.Cause.PROJECT_CREATION; import static org.sonar.server.es.ProjectIndexer.Cause.PROJECT_DELETION; import static org.sonar.server.es.ProjectIndexer.Cause.PROJECT_KEY_UPDATE; import static org.sonar.server.es.ProjectIndexer.Cause.PROJECT_TAGS_UPDATE; +import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_QUALIFIER; import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_TAGS; +import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_UUID; import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.TYPE_PROJECT_MEASURES; +import static org.sonar.server.permission.index.IndexAuthorizationConstants.TYPE_AUTHORIZATION; public class ProjectMeasuresIndexerTest { @@ -65,6 +73,21 @@ public class ProjectMeasuresIndexerTest { private ProjectMeasuresIndexer underTest = new ProjectMeasuresIndexer(db.getDbClient(), es.client()); @Test + public void test_getAuthorizationScope() { + AuthorizationScope scope = underTest.getAuthorizationScope(); + assertThat(scope.getIndexType().getIndex()).isEqualTo(ProjectMeasuresIndexDefinition.DESCRIPTOR); + assertThat(scope.getIndexType().getType()).isEqualTo(TYPE_AUTHORIZATION); + + Predicate<IndexPermissions> projectPredicate = scope.getProjectPredicate(); + IndexPermissions project = new IndexPermissions("P1", Qualifiers.PROJECT); + IndexPermissions app = new IndexPermissions("P1", Qualifiers.APP); + IndexPermissions file = new IndexPermissions("F1", Qualifiers.FILE); + assertThat(projectPredicate.test(project)).isTrue(); + assertThat(projectPredicate.test(app)).isTrue(); + assertThat(projectPredicate.test(file)).isFalse(); + } + + @Test public void index_nothing() { underTest.indexOnStartup(emptySet()); @@ -81,6 +104,7 @@ public class ProjectMeasuresIndexerTest { underTest.indexOnStartup(emptySet()); assertThatIndexContainsOnly(project1, project2, project3); + assertThatQualifierIs("TRK", project1, project2, project3); } /** @@ -106,6 +130,38 @@ public class ProjectMeasuresIndexerTest { } @Test + public void indexOnStartup_indexes_all_applications() { + OrganizationDto organization = db.organizations().insert(); + ComponentDto application1 = db.components().insertPrivateApplication(organization); + ComponentDto application2 = db.components().insertPrivateApplication(organization); + ComponentDto application3 = db.components().insertPrivateApplication(organization); + + underTest.indexOnStartup(emptySet()); + + assertThatIndexContainsOnly(application1, application2, application3); + assertThatQualifierIs("APP", application1, application2, application3); + } + + @Test + public void indexOnStartup_indexes_projects_and_applications() { + OrganizationDto organization = db.organizations().insert(); + + ComponentDto project1 = db.components().insertPrivateProject(); + ComponentDto project2 = db.components().insertPrivateProject(); + ComponentDto project3 = db.components().insertPrivateProject(); + + ComponentDto application1 = db.components().insertPrivateApplication(organization); + ComponentDto application2 = db.components().insertPrivateApplication(organization); + ComponentDto application3 = db.components().insertPrivateApplication(organization); + + underTest.indexOnStartup(emptySet()); + + assertThatIndexContainsOnly(project1, project2, project3, application1, application2, application3); + assertThatQualifierIs("TRK", project1, project2, project3); + assertThatQualifierIs("APP", application1, application2, application3); + } + + @Test public void indexOnAnalysis_indexes_provisioned_project() { ComponentDto project1 = db.components().insertPrivateProject(); ComponentDto project2 = db.components().insertPrivateProject(); @@ -116,6 +172,16 @@ public class ProjectMeasuresIndexerTest { } @Test + public void indexOnAnalysis_indexes_provisioned_application() { + ComponentDto app1 = db.components().insertPrivateApplication(); + ComponentDto app2 = db.components().insertPrivateApplication(); + + underTest.indexOnAnalysis(app1.uuid()); + + assertThatIndexContainsOnly(app1); + } + + @Test public void update_index_when_project_key_is_updated() { ComponentDto project = db.components().insertPrivateProject(); @@ -169,9 +235,10 @@ public class ProjectMeasuresIndexerTest { } @Test - public void do_nothing_if_no_projects_to_index() { + public void do_nothing_if_no_projects_and_apps_to_index() { // this project should not be indexed db.components().insertPrivateProject(); + db.components().insertPrivateApplication(); underTest.index(db.getSession(), emptyList()); @@ -245,6 +312,28 @@ public class ProjectMeasuresIndexerTest { Arrays.stream(expectedProjects).map(ComponentDto::uuid).toArray(String[]::new)); } + private void assertThatQualifierIs(String qualifier, ComponentDto... expectedComponents) { + String[] expectedComponentUuids = Arrays.stream(expectedComponents).map(ComponentDto::uuid).toArray(String[]::new); + assertThatQualifierIs(qualifier, expectedComponentUuids); + } + + private void assertThatQualifierIs(String qualifier, SnapshotDto... expectedComponents) { + String[] expectedComponentUuids = Arrays.stream(expectedComponents).map(SnapshotDto::getComponentUuid).toArray(String[]::new); + assertThatQualifierIs(qualifier, expectedComponentUuids); + } + + private void assertThatQualifierIs(String qualifier, String... componentsUuid) { + SearchRequestBuilder request = es.client() + .prepareSearch(TYPE_PROJECT_MEASURES.getMainType()) + .setQuery(boolQuery() + .filter(termQuery(FIELD_INDEX_TYPE, TYPE_PROJECT_MEASURES.getName())) + .filter(termQuery(FIELD_QUALIFIER, qualifier)) + .filter(termsQuery(FIELD_UUID, componentsUuid))); + assertThat(request.get().getHits().getHits()) + .extracting(SearchHit::getId) + .containsExactlyInAnyOrder(componentsUuid); + } + private IndexingResult recover() { Collection<EsQueueDto> items = db.getDbClient().esQueueDao().selectForRecovery(db.getSession(), System.currentTimeMillis() + 1_000L, 10); return underTest.index(db.getSession(), items); diff --git a/server/sonar-webserver-es/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndex.java b/server/sonar-webserver-es/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndex.java index 7b80c6f7ce3..cf23696d95a 100644 --- a/server/sonar-webserver-es/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndex.java +++ b/server/sonar-webserver-es/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndex.java @@ -119,6 +119,7 @@ import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIEL import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_NAME; import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_NCLOC_DISTRIBUTION; import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_ORGANIZATION_UUID; +import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_QUALIFIER; import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_QUALITY_GATE_STATUS; import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_TAGS; import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.SUB_FIELD_MEASURES_KEY; @@ -378,6 +379,9 @@ public class ProjectMeasuresIndex { query.getTags().ifPresent(tags -> filters.addFilter(FIELD_TAGS, TAGS.getFilterScope(), termsQuery(FIELD_TAGS, tags))); + query.getQualifiers() + .ifPresent(qualifiers -> filters.addFilter(FIELD_QUALIFIER, new SimpleFieldFilterScope(FIELD_QUALIFIER), termsQuery(FIELD_QUALIFIER, qualifiers))); + query.getQueryText() .map(ProjectsTextSearchQueryFactory::createQuery) .ifPresent(queryBuilder -> filters.addFilter("textQuery", new SimpleFieldFilterScope(FIELD_NAME), queryBuilder)); diff --git a/server/sonar-webserver-es/src/main/java/org/sonar/server/measure/index/ProjectMeasuresQuery.java b/server/sonar-webserver-es/src/main/java/org/sonar/server/measure/index/ProjectMeasuresQuery.java index 9f24311845d..9c745f56ce9 100644 --- a/server/sonar-webserver-es/src/main/java/org/sonar/server/measure/index/ProjectMeasuresQuery.java +++ b/server/sonar-webserver-es/src/main/java/org/sonar/server/measure/index/ProjectMeasuresQuery.java @@ -37,16 +37,18 @@ public class ProjectMeasuresQuery { public static final String SORT_BY_LAST_ANALYSIS_DATE = "analysisDate"; private List<MetricCriterion> metricCriteria = new ArrayList<>(); - private Metric.Level qualityGateStatus; - private String organizationUuid; - private Set<String> projectUuids; - private Set<String> languages; - private Set<String> tags; + private Metric.Level qualityGateStatus = null; + private String organizationUuid = null; + private Set<String> projectUuids = null; + private Set<String> languages = null; + private Set<String> tags = null; + private Set<String> qualifiers = null; private String sort = SORT_BY_NAME; private boolean asc = true; - private String queryText; - private boolean ignoreAuthorization; - private boolean ignoreWarning; + + private String queryText = null; + private boolean ignoreAuthorization = false; + private boolean ignoreWarning = false; public ProjectMeasuresQuery addMetricCriterion(MetricCriterion metricCriterion) { this.metricCriteria.add(metricCriterion); @@ -147,6 +149,15 @@ public class ProjectMeasuresQuery { return this; } + public Optional<Set<String>> getQualifiers() { + return Optional.ofNullable(qualifiers); + } + + public ProjectMeasuresQuery setQualifiers(@Nullable Set<String> qualifiers) { + this.qualifiers = qualifiers; + return this; + } + public static class MetricCriterion { private final String metricKey; private final Operator operator; diff --git a/server/sonar-webserver-es/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTest.java b/server/sonar-webserver-es/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTest.java index cecbbbfd3e1..a521daec2c3 100644 --- a/server/sonar-webserver-es/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTest.java +++ b/server/sonar-webserver-es/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTest.java @@ -21,6 +21,7 @@ package org.sonar.server.measure.index; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; import com.tngtech.java.junit.dataprovider.DataProvider; import com.tngtech.java.junit.dataprovider.DataProviderRunner; import com.tngtech.java.junit.dataprovider.UseDataProvider; @@ -33,6 +34,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; +import org.sonar.api.resources.Qualifiers; import org.sonar.api.utils.System2; import org.sonar.db.component.ComponentDto; import org.sonar.db.component.ComponentTesting; @@ -58,12 +60,12 @@ import static java.util.Collections.singletonList; import static java.util.stream.Collectors.toList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; -import static org.assertj.core.groups.Tuple.tuple; import static org.sonar.api.measures.CoreMetrics.ALERT_STATUS_KEY; import static org.sonar.api.measures.CoreMetrics.COVERAGE_KEY; import static org.sonar.api.measures.Metric.Level.ERROR; import static org.sonar.api.measures.Metric.Level.OK; import static org.sonar.api.measures.Metric.Level.WARN; +import static org.sonar.api.resources.Qualifiers.APP; import static org.sonar.api.resources.Qualifiers.PROJECT; import static org.sonar.db.component.ComponentTesting.newPrivateProjectDto; import static org.sonar.db.user.GroupTesting.newGroupDto; @@ -97,6 +99,9 @@ public class ProjectMeasuresIndexTest { private static final ComponentDto PROJECT1 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("Project-1").setName("Project 1").setDbKey("key-1"); private static final ComponentDto PROJECT2 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("Project-2").setName("Project 2").setDbKey("key-2"); private static final ComponentDto PROJECT3 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("Project-3").setName("Project 3").setDbKey("key-3"); + private static final ComponentDto APP1 = ComponentTesting.newApplication(ORG).setUuid("App-1").setName("App 1").setDbKey("app-key-1"); + private static final ComponentDto APP2 = ComponentTesting.newApplication(ORG).setUuid("App-2").setName("App 2").setDbKey("app-key-2"); + private static final ComponentDto APP3 = ComponentTesting.newApplication(ORG).setUuid("App-3").setName("App 3").setDbKey("app-key-3"); private static final UserDto USER1 = newUserDto(); private static final UserDto USER2 = newUserDto(); private static final GroupDto GROUP1 = newGroupDto(); @@ -491,58 +496,79 @@ public class ProjectMeasuresIndexTest { } @Test - public void return_only_projects_authorized_for_user() { - indexForUser(USER1, newDoc(PROJECT1), newDoc(PROJECT2)); - indexForUser(USER2, newDoc(PROJECT3)); + public void filter_on_qualifier() { + index(newDoc(PROJECT1), newDoc(PROJECT2), newDoc(PROJECT3), + newDoc(APP1), newDoc(APP2), newDoc(APP3)); + + assertResults(new ProjectMeasuresQuery(), + APP1, APP2, APP3, PROJECT1, PROJECT2, PROJECT3); + + assertResults(new ProjectMeasuresQuery().setQualifiers(Sets.newHashSet(PROJECT, APP)), + APP1, APP2, APP3, PROJECT1, PROJECT2, PROJECT3); + + assertResults(new ProjectMeasuresQuery().setQualifiers(Sets.newHashSet(PROJECT)), + PROJECT1, PROJECT2, PROJECT3); + + assertResults(new ProjectMeasuresQuery().setQualifiers(Sets.newHashSet(APP)), + APP1, APP2, APP3); + } + + @Test + public void return_only_projects_and_applications_authorized_for_user() { + indexForUser(USER1, newDoc(PROJECT1), newDoc(PROJECT2), + newDoc(APP1), newDoc(APP2)); + indexForUser(USER2, newDoc(PROJECT3), newDoc(APP3)); userSession.logIn(USER1); - assertResults(new ProjectMeasuresQuery(), PROJECT1, PROJECT2); + assertResults(new ProjectMeasuresQuery(), APP1, APP2, PROJECT1, PROJECT2); } @Test - public void return_only_projects_authorized_for_user_groups() { - indexForGroup(GROUP1, newDoc(PROJECT1), newDoc(PROJECT2)); + public void return_only_projects_and_applications_authorized_for_user_groups() { + indexForGroup(GROUP1, newDoc(PROJECT1), newDoc(PROJECT2), + newDoc(APP1), newDoc(APP2)); indexForGroup(GROUP2, newDoc(PROJECT3)); userSession.logIn().setGroups(GROUP1); - assertResults(new ProjectMeasuresQuery(), PROJECT1, PROJECT2); + assertResults(new ProjectMeasuresQuery(), APP1, APP2, PROJECT1, PROJECT2); } @Test - public void return_only_projects_authorized_for_user_and_groups() { - indexForUser(USER1, newDoc(PROJECT1), newDoc(PROJECT2)); + public void return_only_projects_and_applications_authorized_for_user_and_groups() { + indexForUser(USER1, newDoc(PROJECT1), newDoc(PROJECT2), + newDoc(APP1), newDoc(APP2)); indexForGroup(GROUP1, newDoc(PROJECT3)); userSession.logIn(USER1).setGroups(GROUP1); - assertResults(new ProjectMeasuresQuery(), PROJECT1, PROJECT2, PROJECT3); + assertResults(new ProjectMeasuresQuery(), APP1, APP2, PROJECT1, PROJECT2, PROJECT3); } @Test - public void anonymous_user_can_only_access_projects_authorized_for_anyone() { - index(newDoc(PROJECT1)); - indexForUser(USER1, newDoc(PROJECT2)); + public void anonymous_user_can_only_access_projects_and_applications_authorized_for_anyone() { + index(newDoc(PROJECT1), newDoc(APP1)); + indexForUser(USER1, newDoc(PROJECT2), newDoc(APP2)); userSession.anonymous(); - assertResults(new ProjectMeasuresQuery(), PROJECT1); + assertResults(new ProjectMeasuresQuery(), APP1, PROJECT1); } @Test - public void root_user_can_access_all_projects() { - indexForUser(USER1, newDoc(PROJECT1)); + public void root_user_can_access_all_projects_and_applications() { + indexForUser(USER1, newDoc(PROJECT1), newDoc(APP1)); // connecting with a root but not USER1 userSession.logIn().setRoot(); - assertResults(new ProjectMeasuresQuery(), PROJECT1); + assertResults(new ProjectMeasuresQuery(), APP1, PROJECT1); } @Test - public void return_all_projects_when_setIgnoreAuthorization_is_true() { - indexForUser(USER1, newDoc(PROJECT1), newDoc(PROJECT2)); - indexForUser(USER2, newDoc(PROJECT3)); + public void return_all_projects_and_applications_when_setIgnoreAuthorization_is_true() { + indexForUser(USER1, newDoc(PROJECT1), newDoc(PROJECT2), newDoc(APP1), newDoc(APP2)); + indexForUser(USER2, newDoc(PROJECT3), newDoc(APP3)); userSession.logIn(USER1); - assertResults(new ProjectMeasuresQuery().setIgnoreAuthorization(false), PROJECT1, PROJECT2); - assertResults(new ProjectMeasuresQuery().setIgnoreAuthorization(true), PROJECT1, PROJECT2, PROJECT3); + assertResults(new ProjectMeasuresQuery().setIgnoreAuthorization(false), APP1, APP2, PROJECT1, PROJECT2); + assertResults(new ProjectMeasuresQuery().setIgnoreAuthorization(true), APP1, APP2, APP3, PROJECT1, PROJECT2, PROJECT3); } @Test @@ -1239,10 +1265,11 @@ public class ProjectMeasuresIndexTest { assertThat(underTest.search(new ProjectMeasuresQuery().setIgnoreWarning(true), new SearchOptions().addFacets(ALERT_STATUS_KEY)).getFacets().get(ALERT_STATUS_KEY)).containsOnly( entry(ERROR.name(), 4L), entry(OK.name(), 2L)); - assertThat(underTest.search(new ProjectMeasuresQuery().setIgnoreWarning(false), new SearchOptions().addFacets(ALERT_STATUS_KEY)).getFacets().get(ALERT_STATUS_KEY)).containsOnly( - entry(ERROR.name(), 4L), - entry(WARN.name(), 0L), - entry(OK.name(), 2L)); + assertThat(underTest.search(new ProjectMeasuresQuery().setIgnoreWarning(false), new SearchOptions().addFacets(ALERT_STATUS_KEY)).getFacets().get(ALERT_STATUS_KEY)) + .containsOnly( + entry(ERROR.name(), 4L), + entry(WARN.name(), 0L), + entry(OK.name(), 2L)); } @Test @@ -1539,7 +1566,8 @@ public class ProjectMeasuresIndexTest { .setOrganizationUuid(project.getOrganizationUuid()) .setId(project.uuid()) .setKey(project.getDbKey()) - .setName(project.name()); + .setName(project.name()) + .setQualifier(project.qualifier()); } private static ProjectMeasuresDoc newDoc() { diff --git a/server/sonar-webserver-es/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTextSearchTest.java b/server/sonar-webserver-es/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTextSearchTest.java index faddb6bd67b..090f01b8f3e 100644 --- a/server/sonar-webserver-es/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTextSearchTest.java +++ b/server/sonar-webserver-es/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTextSearchTest.java @@ -296,7 +296,8 @@ public class ProjectMeasuresIndexTextSearchTest { .setOrganizationUuid(project.getOrganizationUuid()) .setId(project.uuid()) .setKey(project.getDbKey()) - .setName(project.name()); + .setName(project.name()) + .setQualifier(project.qualifier()); } private static ProjectMeasuresDoc newDoc(ComponentDto project, String metric1, Object value1) { diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/component/ws/ApplicationLeakProjects.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/component/ws/ApplicationLeakProjects.java new file mode 100644 index 00000000000..b292d8800e6 --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/component/ws/ApplicationLeakProjects.java @@ -0,0 +1,88 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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.component.ws; + +import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; + +public class ApplicationLeakProjects { + + @SerializedName("leakProjects") + private List<LeakProject> projects = new ArrayList<>(); + + public ApplicationLeakProjects() { + // even if empty constructor is not required for Gson, it is strongly recommended: + // http://stackoverflow.com/a/18645370/229031 + } + + public ApplicationLeakProjects addProject(LeakProject project) { + this.projects.add(project); + return this; + } + + public Optional<LeakProject> getOldestLeak() { + return projects.stream().min(Comparator.comparingLong(o -> o.leak)); + } + + public static ApplicationLeakProjects parse(String json) { + Gson gson = new Gson(); + return gson.fromJson(json, ApplicationLeakProjects.class); + } + + public String format() { + Gson gson = new Gson(); + return gson.toJson(this, ApplicationLeakProjects.class); + } + + public static class LeakProject { + @SerializedName("id") + private String id; + @SerializedName("leak") + private long leak; + + public LeakProject() { + // even if empty constructor is not required for Gson, it is strongly recommended: + // http://stackoverflow.com/a/18645370/229031 + } + + public LeakProject setId(String id) { + this.id = id; + return this; + } + + public String getId() { + return id; + } + + public LeakProject setLeak(long leak) { + this.leak = leak; + return this; + } + + public long getLeak() { + return leak; + } + } + +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/component/ws/SearchProjectsAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/component/ws/SearchProjectsAction.java index 721071b1f8f..e1648e9bdf9 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/component/ws/SearchProjectsAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/component/ws/SearchProjectsAction.java @@ -20,7 +20,9 @@ package org.sonar.server.component.ws; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; import com.google.common.collect.Ordering; +import com.google.common.collect.Sets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -30,6 +32,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.Function; @@ -44,6 +47,8 @@ import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Response; import org.sonar.api.server.ws.WebService; import org.sonar.api.server.ws.WebService.Param; +import org.sonar.core.platform.EditionProvider.Edition; +import org.sonar.core.platform.PlatformEditionProvider; import org.sonar.core.util.stream.MoreCollectors; import org.sonar.db.DbClient; import org.sonar.db.DbSession; @@ -102,18 +107,24 @@ public class SearchProjectsAction implements ComponentsWsAction { private static final String ORGANIZATIONS = "organizations"; private static final String ANALYSIS_DATE = "analysisDate"; private static final String LEAK_PERIOD_DATE = "leakPeriodDate"; + private static final String METRIC_LEAK_PROJECTS_KEY = "leak_projects"; + private static final String HTML_UL_START_TAG = "<ul>"; + private static final String HTML_UL_END_TAG = "</ul>"; private static final Set<String> POSSIBLE_FIELDS = newHashSet(ALL, ORGANIZATIONS, ANALYSIS_DATE, LEAK_PERIOD_DATE); private final DbClient dbClient; private final ProjectMeasuresIndex index; private final UserSession userSession; private final ProjectsInWarning projectsInWarning; + private final PlatformEditionProvider editionProvider; - public SearchProjectsAction(DbClient dbClient, ProjectMeasuresIndex index, UserSession userSession, ProjectsInWarning projectsInWarning) { + public SearchProjectsAction(DbClient dbClient, ProjectMeasuresIndex index, UserSession userSession, ProjectsInWarning projectsInWarning, + PlatformEditionProvider editionProvider) { this.dbClient = dbClient; this.index = index; this.userSession = userSession; this.projectsInWarning = projectsInWarning; + this.editionProvider = editionProvider; } @Override @@ -250,6 +261,9 @@ public class SearchProjectsAction implements ComponentsWsAction { .map(OrganizationDto::getUuid) .ifPresent(query::setOrganizationUuid); + Set<String> qualifiersBasedOnEdition = getQualifiersBasedOnEdition(query); + query.setQualifiers(qualifiersBasedOnEdition); + ProjectMeasuresQueryValidator.validate(query); SearchIdResult<String> esResults = index.search(query, new SearchOptions() @@ -260,7 +274,40 @@ public class SearchProjectsAction implements ComponentsWsAction { Ordering<ProjectDto> ordering = Ordering.explicit(projectUuids).onResultOf(ProjectDto::getUuid); List<ProjectDto> projects = ordering.immutableSortedCopy(dbClient.projectDao().selectByUuids(dbSession, new HashSet<>(projectUuids))); Map<String, SnapshotDto> analysisByProjectUuid = getSnapshots(dbSession, request, projectUuids); - return new SearchResults(projects, favoriteProjectUuids, esResults, analysisByProjectUuid, query); + + Map<String, Long> applicationsLeakPeriod = getApplicationsLeakPeriod(dbSession, request, qualifiersBasedOnEdition, projectUuids); + + return new SearchResults(projects, favoriteProjectUuids, esResults, analysisByProjectUuid, applicationsLeakPeriod, query); + } + + private Set<String> getQualifiersBasedOnEdition(ProjectMeasuresQuery query) { + Set<String> availableQualifiers = getQualifiersFromEdition(); + Set<String> requestQualifiers = query.getQualifiers().orElse(availableQualifiers); + + Set<String> resolvedQualifiers = requestQualifiers.stream() + .filter(availableQualifiers::contains) + .collect(Collectors.toSet()); + if (!resolvedQualifiers.isEmpty()) { + return resolvedQualifiers; + } else { + throw new IllegalArgumentException("Invalid qualifier, available are: " + String.join(",", availableQualifiers)); + } + } + + private Set<String> getQualifiersFromEdition() { + Optional<Edition> edition = editionProvider.get(); + + if (!edition.isPresent()) { + return Sets.newHashSet(Qualifiers.PROJECT); + } + + switch (edition.get()) { + case ENTERPRISE: + case DATACENTER: + return Sets.newHashSet(Qualifiers.PROJECT, Qualifiers.APP); + default: + return Sets.newHashSet(Qualifiers.PROJECT); + } } private static boolean hasFavoriteFilter(List<Criterion> criteria) { @@ -287,7 +334,7 @@ public class SearchProjectsAction implements ComponentsWsAction { return dbClient.componentDao().selectByUuids(dbSession, favoriteDbUuids).stream() .filter(ComponentDto::isEnabled) - .filter(f -> f.qualifier().equals(Qualifiers.PROJECT)) + .filter(f -> f.qualifier().equals(Qualifiers.PROJECT) || f.qualifier().equals(Qualifiers.APP)) .map(ComponentDto::uuid) .collect(MoreCollectors.toSet()); } @@ -301,6 +348,19 @@ public class SearchProjectsAction implements ComponentsWsAction { return emptyMap(); } + private Map<String, Long> getApplicationsLeakPeriod(DbSession dbSession, SearchProjectsRequest request, Set<String> qualifiers, List<String> projectUuids) { + if (qualifiers.contains(Qualifiers.APP) && request.getAdditionalFields().contains(LEAK_PERIOD_DATE)) { + return dbClient.liveMeasureDao().selectByComponentUuidsAndMetricKeys(dbSession, projectUuids, Collections.singleton(METRIC_LEAK_PROJECTS_KEY)) + .stream() + .filter(lm -> !Objects.isNull(lm.getDataAsString())) + .map(lm -> Maps.immutableEntry(lm.getComponentUuid(), ApplicationLeakProjects.parse(lm.getDataAsString()).getOldestLeak())) + .filter(entry -> entry.getValue().isPresent()) + .collect(Collectors.toMap(Entry::getKey, entry -> entry.getValue().get().getLeak())); + } + + return emptyMap(); + } + private static SearchProjectsRequest toRequest(Request httpRequest) { RequestBuilder request = new RequestBuilder() .setOrganization(httpRequest.param(PARAM_ORGANIZATION)) @@ -324,8 +384,8 @@ public class SearchProjectsAction implements ComponentsWsAction { } private SearchProjectsWsResponse buildResponse(SearchProjectsRequest request, SearchResults searchResults, Map<String, OrganizationDto> organizationsByUuid) { - Function<ProjectDto, Component> dbToWsComponent = new DbToWsComponent(request, organizationsByUuid, searchResults.favoriteProjectUuids, searchResults.analysisByProjectUuid, - userSession.isLoggedIn()); + Function<ProjectDto, Component> dbToWsComponent = new DbToWsComponent(request, organizationsByUuid, searchResults.favoriteProjectUuids, + searchResults.analysisByProjectUuid, searchResults.applicationsLeakPeriods, userSession.isLoggedIn()); Map<String, OrganizationDto> organizationsByUuidForAdditionalInfo = new HashMap<>(); if (request.additionalFields.contains(ORGANIZATIONS)) { @@ -438,11 +498,13 @@ public class SearchProjectsAction implements ComponentsWsAction { private final Set<String> favoriteProjectUuids; private final boolean isUserLoggedIn; private final Map<String, SnapshotDto> analysisByProjectUuid; + private final Map<String, Long> applicationsLeakPeriod; private DbToWsComponent(SearchProjectsRequest request, Map<String, OrganizationDto> organizationsByUuid, Set<String> favoriteProjectUuids, - Map<String, SnapshotDto> analysisByProjectUuid, boolean isUserLoggedIn) { + Map<String, SnapshotDto> analysisByProjectUuid, Map<String, Long> applicationsLeakPeriod, boolean isUserLoggedIn) { this.request = request; this.analysisByProjectUuid = analysisByProjectUuid; + this.applicationsLeakPeriod = applicationsLeakPeriod; this.wsComponent = Component.newBuilder(); this.organizationsByUuid = organizationsByUuid; this.favoriteProjectUuids = favoriteProjectUuids; @@ -459,6 +521,7 @@ public class SearchProjectsAction implements ComponentsWsAction { .setOrganization(organizationDto.getKey()) .setKey(dbProject.getKey()) .setName(dbProject.getName()) + .setQualifier(dbProject.getQualifier()) .setVisibility(Visibility.getLabel(dbProject.isPrivate())); wsComponent.getTagsBuilder().addAllTags(dbProject.getTags()); @@ -468,7 +531,11 @@ public class SearchProjectsAction implements ComponentsWsAction { wsComponent.setAnalysisDate(formatDateTime(snapshotDto.getCreatedAt())); } if (request.getAdditionalFields().contains(LEAK_PERIOD_DATE)) { - ofNullable(snapshotDto.getPeriodDate()).ifPresent(leakPeriodDate -> wsComponent.setLeakPeriodDate(formatDateTime(leakPeriodDate))); + if (Qualifiers.APP.equals(dbProject.getQualifier())) { + ofNullable(applicationsLeakPeriod.get(dbProject.getUuid())).ifPresent(leakPeriodDate -> wsComponent.setLeakPeriodDate(formatDateTime(leakPeriodDate))); + } else { + ofNullable(snapshotDto.getPeriodDate()).ifPresent(leakPeriodDate -> wsComponent.setLeakPeriodDate(formatDateTime(leakPeriodDate))); + } } } @@ -485,16 +552,18 @@ public class SearchProjectsAction implements ComponentsWsAction { private final Set<String> favoriteProjectUuids; private final Facets facets; private final Map<String, SnapshotDto> analysisByProjectUuid; + private final Map<String, Long> applicationsLeakPeriods; private final ProjectMeasuresQuery query; private final int total; private SearchResults(List<ProjectDto> projects, Set<String> favoriteProjectUuids, SearchIdResult<String> searchResults, Map<String, SnapshotDto> analysisByProjectUuid, - ProjectMeasuresQuery query) { + Map<String, Long> applicationsLeakPeriods, ProjectMeasuresQuery query) { this.projects = projects; this.favoriteProjectUuids = favoriteProjectUuids; this.total = (int) searchResults.getTotal(); this.facets = searchResults.getFacets(); this.analysisByProjectUuid = analysisByProjectUuid; + this.applicationsLeakPeriods = applicationsLeakPeriods; this.query = query; } } diff --git a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/component/ws/search_projects-example.json b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/component/ws/search_projects-example.json index ca957b47819..f87b1e3144e 100644 --- a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/component/ws/search_projects-example.json +++ b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/component/ws/search_projects-example.json @@ -19,6 +19,7 @@ "organization": "my-org-key-1", "key": "my_project", "name": "My Project 1", + "qualifier": "TRK", "isFavorite": true, "tags": [ "finance", @@ -30,6 +31,7 @@ "organization": "my-org-key-1", "key": "another_project", "name": "My Project 2", + "qualifier": "TRK", "isFavorite": false, "tags": [], "visibility": "public" @@ -38,6 +40,7 @@ "organization": "my-org-key-2", "key": "third_project", "name": "My Project 3", + "qualifier": "TRK", "isFavorite": false, "tags": [ "sales", diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/component/ws/SearchProjectsActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/component/ws/SearchProjectsActionTest.java index ec39c563d4e..0f19d36630c 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/component/ws/SearchProjectsActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/component/ws/SearchProjectsActionTest.java @@ -26,6 +26,7 @@ import com.tngtech.java.junit.dataprovider.UseDataProvider; import java.util.Arrays; import java.util.Date; import java.util.List; +import java.util.Optional; import java.util.function.Consumer; import java.util.stream.IntStream; import java.util.stream.Stream; @@ -35,9 +36,12 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.sonar.api.measures.Metric; +import org.sonar.api.resources.Qualifiers; import org.sonar.api.server.ws.WebService; import org.sonar.api.server.ws.WebService.Param; import org.sonar.api.utils.System2; +import org.sonar.core.platform.EditionProvider.Edition; +import org.sonar.core.platform.PlatformEditionProvider; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.DbTester; @@ -67,6 +71,8 @@ import static java.util.Collections.singletonList; import static java.util.Optional.ofNullable; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import static org.sonar.api.measures.CoreMetrics.ALERT_STATUS_KEY; import static org.sonar.api.measures.CoreMetrics.DUPLICATED_LINES_DENSITY_KEY; import static org.sonar.api.measures.CoreMetrics.NCLOC_LANGUAGE_DISTRIBUTION_KEY; @@ -78,6 +84,7 @@ import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_RATING_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.api.measures.Metric.ValueType.DATA; import static org.sonar.api.measures.Metric.ValueType.INT; import static org.sonar.api.measures.Metric.ValueType.LEVEL; import static org.sonar.api.server.ws.WebService.Param.ASCENDING; @@ -103,6 +110,7 @@ public class SearchProjectsActionTest { private static final String NCLOC = "ncloc"; private static final String COVERAGE = "coverage"; private static final String NEW_COVERAGE = "new_coverage"; + private static final String LEAK_PROJECTS_KEY = "leak_projects"; private static final String QUALITY_GATE_STATUS = "alert_status"; private static final String ANALYSIS_DATE = "analysisDate"; private static final String IS_FAVOURITE_CRITERION = "isFavorite"; @@ -126,15 +134,26 @@ public class SearchProjectsActionTest { return new Object[][] {{NEW_MAINTAINABILITY_RATING_KEY}, {NEW_RELIABILITY_RATING_KEY}, {NEW_SECURITY_RATING_KEY}}; } + @DataProvider + public static Object[][] component_qualifiers_for_valid_editions() { + return new Object[][] { + {new String[] {Qualifiers.PROJECT}, Edition.COMMUNITY}, + {new String[] {Qualifiers.PROJECT}, Edition.DEVELOPER}, + {new String[] {Qualifiers.APP, Qualifiers.PROJECT}, Edition.ENTERPRISE}, + {new String[] {Qualifiers.APP, Qualifiers.PROJECT}, Edition.DATACENTER}, + }; + } + private DbClient dbClient = db.getDbClient(); private DbSession dbSession = db.getSession(); + private PlatformEditionProvider editionProviderMock = mock(PlatformEditionProvider.class); private PermissionIndexerTester authorizationIndexerTester = new PermissionIndexerTester(es, new ProjectMeasuresIndexer(dbClient, es.client())); private ProjectMeasuresIndex index = new ProjectMeasuresIndex(es.client(), new WebAuthorizationTypeSupport(userSession), System2.INSTANCE); private ProjectMeasuresIndexer projectMeasuresIndexer = new ProjectMeasuresIndexer(db.getDbClient(), es.client()); private ProjectsInWarning projectsInWarning = new ProjectsInWarning(); - private WsActionTester ws = new WsActionTester(new SearchProjectsAction(dbClient, index, userSession, projectsInWarning)); + private WsActionTester ws = new WsActionTester(new SearchProjectsAction(dbClient, index, userSession, projectsInWarning, editionProviderMock)); private RequestBuilder request = SearchProjectsRequest.builder(); @@ -600,6 +619,63 @@ public class SearchProjectsActionTest { } @Test + @UseDataProvider("component_qualifiers_for_valid_editions") + public void filter_projects_and_apps_by_editions(String[] qualifiers, Edition edition) { + when(editionProviderMock.get()).thenReturn(Optional.of(edition)); + userSession.logIn(); + OrganizationDto organization = db.organizations().insert(); + ComponentDto portfolio1 = insertPortfolio(organization); + ComponentDto portfolio2 = insertPortfolio(organization); + + ComponentDto application1 = insertApplication(organization); + ComponentDto application2 = insertApplication(organization); + ComponentDto application3 = insertApplication(organization); + + ComponentDto project1 = insertProject(organization); + ComponentDto project2 = insertProject(organization); + ComponentDto project3 = insertProject(organization); + + SearchProjectsWsResponse result = call(request); + + assertThat(result.getComponentsCount()).isEqualTo( + Stream.of(application1, application2, application3, project1, project2, project3) + .filter(c -> Stream.of(qualifiers).anyMatch(s -> s.equals(c.qualifier()))) + .count()); + + assertThat(result.getComponentsList()).extracting(Component::getKey) + .containsExactly( + Stream.of(application1, application2, application3, project1, project2, project3) + .filter(c -> Stream.of(qualifiers).anyMatch(s -> s.equals(c.qualifier()))) + .map(ComponentDto::getDbKey) + .toArray(String[]::new)); + } + + @Test + public void should_return_projects_only_when_no_edition() { + when(editionProviderMock.get()).thenReturn(Optional.empty()); + userSession.logIn(); + OrganizationDto organization = db.organizations().insert(); + + ComponentDto portfolio1 = insertPortfolio(organization); + ComponentDto portfolio2 = insertPortfolio(organization); + + insertApplication(organization); + insertApplication(organization); + insertApplication(organization); + + ComponentDto project1 = insertProject(organization); + ComponentDto project2 = insertProject(organization); + ComponentDto project3 = insertProject(organization); + + SearchProjectsWsResponse result = call(request); + + assertThat(result.getComponentsCount()).isEqualTo(3); + + assertThat(result.getComponentsList()).extracting(Component::getKey) + .containsExactly(Stream.of(project1, project2, project3).map(ComponentDto::getDbKey).toArray(String[]::new)); + } + + @Test public void do_not_return_isFavorite_if_anonymous_user() { userSession.anonymous(); OrganizationDto organization = db.organizations().insert(); @@ -1061,6 +1137,7 @@ public class SearchProjectsActionTest { @Test public void return_leak_period_date() { + when(editionProviderMock.get()).thenReturn(Optional.of(Edition.ENTERPRISE)); userSession.logIn(); OrganizationDto organization = db.organizations().insert(); ComponentDto project1 = db.components().insertPublicProject(organization); @@ -1073,6 +1150,13 @@ public class SearchProjectsActionTest { // No snapshot on project 3 ComponentDto project3 = db.components().insertPublicProject(organization); authorizationIndexerTester.allowOnlyAnyone(project3); + + MetricDto leakProjects = db.measures().insertMetric(c -> c.setKey(LEAK_PROJECTS_KEY).setValueType(DATA.name())); + ComponentDto application1 = insertApplication(organization, + new Measure(leakProjects, c -> c.setData("{\"leakProjects\":[{\"id\": 1, \"leak\":20000000000}, {\"id\": 2, \"leak\":10000000000}]}"))); + db.components().insertSnapshot(application1); + + authorizationIndexerTester.allowOnlyAnyone(application1); projectMeasuresIndexer.indexOnStartup(null); SearchProjectsWsResponse result = call(request.setAdditionalFields(singletonList("leakPeriodDate"))); @@ -1081,7 +1165,8 @@ public class SearchProjectsActionTest { .containsOnly( tuple(project1.getDbKey(), true, formatDateTime(new Date(10_000_000_000L))), tuple(project2.getDbKey(), false, ""), - tuple(project3.getDbKey(), false, "")); + tuple(project3.getDbKey(), false, ""), + tuple(application1.getDbKey(), true, formatDateTime(new Date(10_000_000_000L)))); } @Test @@ -1201,6 +1286,25 @@ public class SearchProjectsActionTest { return project; } + private ComponentDto insertApplication(OrganizationDto organizationDto, Measure... measures) { + return insertApplication(organizationDto, defaults(), measures); + } + + private ComponentDto insertApplication(OrganizationDto organizationDto, Consumer<ComponentDto> componentConsumer, Measure... measures) { + ComponentDto application = db.components().insertPublicApplication(organizationDto, componentConsumer); + Arrays.stream(measures).forEach(m -> db.measures().insertLiveMeasure(application, m.metric, m.consumer)); + authorizationIndexerTester.allowOnlyAnyone(application); + projectMeasuresIndexer.indexOnAnalysis(application.uuid()); + return application; + } + + private ComponentDto insertPortfolio(OrganizationDto organizationDto) { + ComponentDto portfolio = db.components().insertPublicPortfolio(organizationDto); + authorizationIndexerTester.allowOnlyAnyone(portfolio); + projectMeasuresIndexer.indexOnAnalysis(portfolio.uuid()); + return portfolio; + } + private static class Measure { private final MetricDto metric; private final Consumer<LiveMeasureDto> consumer; |