aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorJacek <jacek.poreda@sonarsource.com>2020-03-26 17:07:19 +0100
committersonartech <sonartech@sonarsource.com>2020-04-15 20:03:38 +0000
commitbd73de80860fda06408f7c342ba7455250b16151 (patch)
tree59eea7785a4847404f5bbb649a4db165f9bf2b81 /server
parent32c274e7d74a040b2895c613724fb89e0fc704cf (diff)
downloadsonarqube-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')
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/measure/ProjectMeasuresIndexerIterator.java18
-rw-r--r--server/sonar-db-dao/src/test/java/org/sonar/db/measure/ProjectMeasuresIndexerIteratorTest.java30
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/es/newindex/StringFieldBuilder.java2
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/measure/index/ProjectMeasuresDoc.java10
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndexDefinition.java4
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndexer.java4
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexerTest.java91
-rw-r--r--server/sonar-webserver-es/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndex.java4
-rw-r--r--server/sonar-webserver-es/src/main/java/org/sonar/server/measure/index/ProjectMeasuresQuery.java27
-rw-r--r--server/sonar-webserver-es/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTest.java84
-rw-r--r--server/sonar-webserver-es/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTextSearchTest.java3
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/component/ws/ApplicationLeakProjects.java88
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/component/ws/SearchProjectsAction.java85
-rw-r--r--server/sonar-webserver-webapi/src/main/resources/org/sonar/server/component/ws/search_projects-example.json3
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/component/ws/SearchProjectsActionTest.java108
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;