Browse Source

SONAR-21799 fix issue with sorting by impact data metric

tags/10.5.0.89998
Léo Geoffroy 1 month ago
parent
commit
5f3312ab96

+ 6
- 0
server/sonar-server-common/src/main/java/org/sonar/server/measure/ImpactMeasureBuilder.java View File

import java.util.Arrays; import java.util.Arrays;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import javax.annotation.CheckForNull;
import org.sonar.api.issue.impact.Severity; import org.sonar.api.issue.impact.Severity;


import static org.sonar.api.utils.Preconditions.checkArgument; import static org.sonar.api.utils.Preconditions.checkArgument;
return this; return this;
} }


@CheckForNull
public Long getTotal() {
return map.get(TOTAL_KEY);
}

public ImpactMeasureBuilder add(ImpactMeasureBuilder other) { public ImpactMeasureBuilder add(ImpactMeasureBuilder other) {
other.buildAsMap().forEach((key, val) -> map.merge(key, val, Long::sum)); other.buildAsMap().forEach((key, val) -> map.merge(key, val, Long::sum));
return this; return this;

+ 19
- 12
server/sonar-server-common/src/test/java/org/sonar/server/measure/ImpactMeasureBuilderTest.java View File

package org.sonar.server.measure; package org.sonar.server.measure;


import java.util.Map; import java.util.Map;
import org.junit.Test;
import org.junit.jupiter.api.Test;
import org.sonar.api.issue.impact.Severity; import org.sonar.api.issue.impact.Severity;


import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;


public class ImpactMeasureBuilderTest {
class ImpactMeasureBuilderTest {


@Test @Test
public void createEmptyMeasure_shouldReturnMeasureWithAllFields() {
void createEmptyMeasure_shouldReturnMeasureWithAllFields() {
ImpactMeasureBuilder builder = ImpactMeasureBuilder.createEmpty(); ImpactMeasureBuilder builder = ImpactMeasureBuilder.createEmpty();
assertThat(builder.buildAsMap()) assertThat(builder.buildAsMap())
.containsAllEntriesOf(getImpactMap(0L, 0L, 0L, 0L)); .containsAllEntriesOf(getImpactMap(0L, 0L, 0L, 0L));
} }


@Test @Test
public void fromMap_shouldInitializeCorrectlyTheBuilder() {
void fromMap_shouldInitializeCorrectlyTheBuilder() {
Map<String, Long> map = getImpactMap(6L, 3L, 2L, 1L); Map<String, Long> map = getImpactMap(6L, 3L, 2L, 1L);
ImpactMeasureBuilder builder = ImpactMeasureBuilder.fromMap(map); ImpactMeasureBuilder builder = ImpactMeasureBuilder.fromMap(map);
assertThat(builder.buildAsMap()) assertThat(builder.buildAsMap())
} }


@Test @Test
public void fromMap_whenMissingField_shouldThrowException() {
void fromMap_whenMissingField_shouldThrowException() {
Map<String, Long> map = Map.of(); Map<String, Long> map = Map.of();
assertThatThrownBy(() -> ImpactMeasureBuilder.fromMap(map)) assertThatThrownBy(() -> ImpactMeasureBuilder.fromMap(map))
.isInstanceOf(IllegalArgumentException.class) .isInstanceOf(IllegalArgumentException.class)
} }


@Test @Test
public void toString_shouldInitializeCorrectlyTheBuilder() {
void toString_shouldInitializeCorrectlyTheBuilder() {
ImpactMeasureBuilder builder = ImpactMeasureBuilder.fromString(""" ImpactMeasureBuilder builder = ImpactMeasureBuilder.fromString("""
{ {
total: 6, total: 6,
} }


@Test @Test
public void buildAsMap_whenIsEmpty_shouldThrowException() {
void buildAsMap_whenIsEmpty_shouldThrowException() {
ImpactMeasureBuilder impactMeasureBuilder = ImpactMeasureBuilder.newInstance(); ImpactMeasureBuilder impactMeasureBuilder = ImpactMeasureBuilder.newInstance();
assertThatThrownBy(impactMeasureBuilder::buildAsMap) assertThatThrownBy(impactMeasureBuilder::buildAsMap)
.isInstanceOf(IllegalArgumentException.class) .isInstanceOf(IllegalArgumentException.class)
} }


@Test @Test
public void buildAsMap_whenMissingSeverity_shouldThrowException() {
void buildAsMap_whenMissingSeverity_shouldThrowException() {
ImpactMeasureBuilder impactMeasureBuilder = ImpactMeasureBuilder.newInstance() ImpactMeasureBuilder impactMeasureBuilder = ImpactMeasureBuilder.newInstance()
.setTotal(1L) .setTotal(1L)
.setSeverity(Severity.HIGH, 1L) .setSeverity(Severity.HIGH, 1L)
} }


@Test @Test
public void buildAsString_whenMissingSeverity_shouldThrowException() {
void buildAsString_whenMissingSeverity_shouldThrowException() {
ImpactMeasureBuilder impactMeasureBuilder = ImpactMeasureBuilder.newInstance() ImpactMeasureBuilder impactMeasureBuilder = ImpactMeasureBuilder.newInstance()
.setTotal(1L) .setTotal(1L)
.setSeverity(Severity.HIGH, 1L) .setSeverity(Severity.HIGH, 1L)
} }


@Test @Test
public void setSeverity_shouldInitializeSeverityValues() {
void setSeverity_shouldInitializeSeverityValues() {
ImpactMeasureBuilder builder = ImpactMeasureBuilder.newInstance() ImpactMeasureBuilder builder = ImpactMeasureBuilder.newInstance()
.setSeverity(Severity.HIGH, 3L) .setSeverity(Severity.HIGH, 3L)
.setSeverity(Severity.MEDIUM, 2L) .setSeverity(Severity.MEDIUM, 2L)
} }


@Test @Test
public void add_shouldSumImpactsAndTotal() {
void add_shouldSumImpactsAndTotal() {
ImpactMeasureBuilder builder = ImpactMeasureBuilder.fromMap(getImpactMap(6L, 3L, 2L, 1L)) ImpactMeasureBuilder builder = ImpactMeasureBuilder.fromMap(getImpactMap(6L, 3L, 2L, 1L))
.add(ImpactMeasureBuilder.newInstance().setTotal(6L).setSeverity(Severity.HIGH, 3L).setSeverity(Severity.MEDIUM, 2L).setSeverity(Severity.LOW, 1L)); .add(ImpactMeasureBuilder.newInstance().setTotal(6L).setSeverity(Severity.HIGH, 3L).setSeverity(Severity.MEDIUM, 2L).setSeverity(Severity.LOW, 1L));
assertThat(builder.buildAsMap()) assertThat(builder.buildAsMap())
} }


@Test @Test
public void add_whenOtherMapHasMissingField_shouldThrowException() {
void add_whenOtherMapHasMissingField_shouldThrowException() {
ImpactMeasureBuilder impactMeasureBuilder = ImpactMeasureBuilder.newInstance(); ImpactMeasureBuilder impactMeasureBuilder = ImpactMeasureBuilder.newInstance();
ImpactMeasureBuilder otherBuilder = ImpactMeasureBuilder.newInstance(); ImpactMeasureBuilder otherBuilder = ImpactMeasureBuilder.newInstance();
assertThatThrownBy(() -> impactMeasureBuilder.add(otherBuilder)) assertThatThrownBy(() -> impactMeasureBuilder.add(otherBuilder))
.hasMessage("Map must contain a total key"); .hasMessage("Map must contain a total key");
} }


@Test
void getTotal_shoudReturnExpectedTotal() {
ImpactMeasureBuilder builder = ImpactMeasureBuilder.fromMap(getImpactMap(6L, 3L, 2L, 1L));

assertThat(builder.getTotal()).isEqualTo(6L);
}

} }

+ 18
- 30
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/ws/ComponentTreeAction.java View File

import static java.util.Collections.emptyList; import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap; import static java.util.Collections.emptyMap;
import static java.util.Optional.ofNullable; import static java.util.Optional.ofNullable;
import static org.sonar.api.measures.CoreMetrics.MAINTAINABILITY_ISSUES_KEY;
import static org.sonar.api.measures.CoreMetrics.NEW_MAINTAINABILITY_ISSUES_KEY;
import static org.sonar.api.measures.CoreMetrics.NEW_RELIABILITY_ISSUES_KEY;
import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_ISSUES_KEY;
import static org.sonar.api.measures.CoreMetrics.RELIABILITY_ISSUES_KEY;
import static org.sonar.api.measures.CoreMetrics.SECURITY_ISSUES_KEY;
import static org.sonar.api.measures.Metric.ValueType.DATA; import static org.sonar.api.measures.Metric.ValueType.DATA;
import static org.sonar.api.measures.Metric.ValueType.DISTRIB; import static org.sonar.api.measures.Metric.ValueType.DISTRIB;
import static org.sonar.api.utils.Paging.offset; import static org.sonar.api.utils.Paging.offset;
public void define(WebService.NewController context) { public void define(WebService.NewController context) {
WebService.NewAction action = context.createAction(ACTION_COMPONENT_TREE) WebService.NewAction action = context.createAction(ACTION_COMPONENT_TREE)
.setDescription(format("Navigate through components based on the chosen strategy with specified measures.<br>" + .setDescription(format("Navigate through components based on the chosen strategy with specified measures.<br>" +
"Requires the following permission: 'Browse' on the specified project.<br>" +
"For applications, it also requires 'Browse' permission on its child projects. <br>" +
"When limiting search with the %s parameter, directories are not returned.", Param.TEXT_QUERY))
"Requires the following permission: 'Browse' on the specified project.<br>" +
"For applications, it also requires 'Browse' permission on its child projects. <br>" +
"When limiting search with the %s parameter, directories are not returned.", Param.TEXT_QUERY))
.setResponseExample(getClass().getResource("component_tree-example.json")) .setResponseExample(getClass().getResource("component_tree-example.json"))
.setSince("5.4") .setSince("5.4")
.setHandler(this) .setHandler(this)
MeasuresWsModule.getDeprecatedMetricsInSonarQube105())), MeasuresWsModule.getDeprecatedMetricsInSonarQube105())),
new Change("10.5", "Added new accepted values for the 'metricKeys' param: 'maintainability_issues', 'reliability_issues', 'security_issues'"), new Change("10.5", "Added new accepted values for the 'metricKeys' param: 'maintainability_issues', 'reliability_issues', 'security_issues'"),
new Change("10.4", String.format("The metrics %s are now deprecated " + new Change("10.4", String.format("The metrics %s are now deprecated " +
"without exact replacement. Use 'maintainability_issues', 'reliability_issues' and 'security_issues' instead.",
"without exact replacement. Use 'maintainability_issues', 'reliability_issues' and 'security_issues' instead.",
MeasuresWsModule.getDeprecatedMetricsInSonarQube104())), MeasuresWsModule.getDeprecatedMetricsInSonarQube104())),
new Change("10.4", "The metrics 'open_issues', 'reopened_issues' and 'confirmed_issues' are now deprecated in the response. Consume 'violations' instead."), new Change("10.4", "The metrics 'open_issues', 'reopened_issues' and 'confirmed_issues' are now deprecated in the response. Consume 'violations' instead."),
new Change("10.4", "The use of 'open_issues', 'reopened_issues' and 'confirmed_issues' values in 'metricKeys' param are now deprecated. Use 'violations' instead."), new Change("10.4", "The use of 'open_issues', 'reopened_issues' and 'confirmed_issues' values in 'metricKeys' param are now deprecated. Use 'violations' instead."),


action.createParam(Param.TEXT_QUERY) action.createParam(Param.TEXT_QUERY)
.setDescription("Limit search to: <ul>" + .setDescription("Limit search to: <ul>" +
"<li>component names that contain the supplied string</li>" +
"<li>component keys that are exactly the same as the supplied string</li>" +
"</ul>")
"<li>component names that contain the supplied string</li>" +
"<li>component keys that are exactly the same as the supplied string</li>" +
"</ul>")
.setMinimumLength(QUERY_MINIMUM_LENGTH) .setMinimumLength(QUERY_MINIMUM_LENGTH)
.setExampleValue("FILE_NAM"); .setExampleValue("FILE_NAM");




action.createParam(PARAM_METRIC_SORT_FILTER) action.createParam(PARAM_METRIC_SORT_FILTER)
.setDescription(format("Filter components. Sort must be on a metric. Possible values are: " + .setDescription(format("Filter components. Sort must be on a metric. Possible values are: " +
"<ul>" +
"<li>%s: return all components</li>" +
"<li>%s: filter out components that do not have a measure on the sorted metric</li>" +
"</ul>", ALL_METRIC_SORT_FILTER, WITH_MEASURES_ONLY_METRIC_SORT_FILTER))
"<ul>" +
"<li>%s: return all components</li>" +
"<li>%s: filter out components that do not have a measure on the sorted metric</li>" +
"</ul>", ALL_METRIC_SORT_FILTER, WITH_MEASURES_ONLY_METRIC_SORT_FILTER))
.setDefaultValue(ALL_METRIC_SORT_FILTER) .setDefaultValue(ALL_METRIC_SORT_FILTER)
.setPossibleValues(METRIC_SORT_FILTERS); .setPossibleValues(METRIC_SORT_FILTERS);




action.createParam(PARAM_STRATEGY) action.createParam(PARAM_STRATEGY)
.setDescription("Strategy to search for base component descendants:" + .setDescription("Strategy to search for base component descendants:" +
"<ul>" +
"<li>children: return the children components of the base component. Grandchildren components are not returned</li>" +
"<li>all: return all the descendants components of the base component. Grandchildren are returned.</li>" +
"<li>leaves: return all the descendant components (files, in general) which don't have other children. They are the leaves of the component tree.</li>" +
"</ul>")
"<ul>" +
"<li>children: return the children components of the base component. Grandchildren components are not returned</li>" +
"<li>all: return all the descendants components of the base component. Grandchildren are returned.</li>" +
"<li>leaves: return all the descendant components (files, in general) which don't have other children. They are the leaves of the component tree.</li>" +
"</ul>")
.setPossibleValues(STRATEGIES.keySet()) .setPossibleValues(STRATEGIES.keySet())
.setDefaultValue(ALL_STRATEGY); .setDefaultValue(ALL_STRATEGY);
} }
INSTANCE; INSTANCE;


static final Set<String> FORBIDDEN_METRIC_TYPES = Set.of(DISTRIB.name()); static final Set<String> FORBIDDEN_METRIC_TYPES = Set.of(DISTRIB.name());
static final Map<String, Set<String>> PARTIALLY_SUPPORTED_METRICS= Map. of(
static final Map<String, Set<String>> PARTIALLY_SUPPORTED_METRICS = Map.of(
DATA.name(), DATA.name(),
Set.of(
SECURITY_ISSUES_KEY,
MAINTAINABILITY_ISSUES_KEY,
RELIABILITY_ISSUES_KEY,
NEW_SECURITY_ISSUES_KEY,
NEW_MAINTAINABILITY_ISSUES_KEY,
NEW_RELIABILITY_ISSUES_KEY));
DataSupportedMetrics.IMPACTS_SUPPORTED_METRICS);


@Override @Override
public boolean test(@Nonnull MetricDto input) { public boolean test(@Nonnull MetricDto input) {

+ 75
- 23
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/ws/ComponentTreeSort.java View File

import com.google.common.collect.Ordering; import com.google.common.collect.Ordering;
import com.google.common.collect.Table; import com.google.common.collect.Table;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import org.jetbrains.annotations.NotNull;
import org.sonar.api.measures.Metric; import org.sonar.api.measures.Metric;
import org.sonar.api.measures.Metric.ValueType; import org.sonar.api.measures.Metric.ValueType;
import org.sonar.db.component.ComponentDto; import org.sonar.db.component.ComponentDto;
import org.sonar.db.metric.MetricDto; import org.sonar.db.metric.MetricDto;
import org.sonar.server.exceptions.BadRequestException; import org.sonar.server.exceptions.BadRequestException;
import org.sonar.server.measure.ImpactMeasureBuilder;


import static java.lang.String.CASE_INSENSITIVE_ORDER; import static java.lang.String.CASE_INSENSITIVE_ORDER;
import static java.lang.String.format; import static java.lang.String.format;
} }


public static List<ComponentDto> sortComponents(List<ComponentDto> components, ComponentTreeRequest wsRequest, List<MetricDto> metrics, public static List<ComponentDto> sortComponents(List<ComponentDto> components, ComponentTreeRequest wsRequest, List<MetricDto> metrics,
Table<String, MetricDto, ComponentTreeData.Measure> measuresByComponentUuidAndMetric) {
Table<String, MetricDto, ComponentTreeData.Measure> measuresByComponentUuidAndMetric) {
List<String> sortParameters = wsRequest.getSort(); List<String> sortParameters = wsRequest.getSort();
if (sortParameters == null || sortParameters.isEmpty()) { if (sortParameters == null || sortParameters.isEmpty()) {
return components; return components;
} }


private static Ordering<ComponentDto> metricValueOrdering(ComponentTreeRequest wsRequest, List<MetricDto> metrics, private static Ordering<ComponentDto> metricValueOrdering(ComponentTreeRequest wsRequest, List<MetricDto> metrics,
Table<String, MetricDto, ComponentTreeData.Measure> measuresByComponentUuidAndMetric) {
Table<String, MetricDto, ComponentTreeData.Measure> measuresByComponentUuidAndMetric) {
boolean isAscending = Optional.ofNullable(wsRequest.getAsc()).orElse(false);
if (wsRequest.getMetricSort() == null) { if (wsRequest.getMetricSort() == null) {
return componentNameOrdering(wsRequest.getAsc());
return componentNameOrdering(isAscending);
} }
Map<String, MetricDto> metricsByKey = Maps.uniqueIndex(metrics, MetricDto::getKey); Map<String, MetricDto> metricsByKey = Maps.uniqueIndex(metrics, MetricDto::getKey);
MetricDto metric = metricsByKey.get(wsRequest.getMetricSort()); MetricDto metric = metricsByKey.get(wsRequest.getMetricSort());


boolean isAscending = wsRequest.getAsc();
ValueType metricValueType = ValueType.valueOf(metric.getValueType()); ValueType metricValueType = ValueType.valueOf(metric.getValueType());
if (NUMERIC_VALUE_TYPES.contains(metricValueType)) { if (NUMERIC_VALUE_TYPES.contains(metricValueType)) {
return numericalMetricOrdering(isAscending, metric, measuresByComponentUuidAndMetric); return numericalMetricOrdering(isAscending, metric, measuresByComponentUuidAndMetric);
return stringOrdering(isAscending, new ComponentDtoToTextualMeasureValue(metric, measuresByComponentUuidAndMetric)); return stringOrdering(isAscending, new ComponentDtoToTextualMeasureValue(metric, measuresByComponentUuidAndMetric));
} else if (ValueType.LEVEL.equals(ValueType.valueOf(metric.getValueType()))) { } else if (ValueType.LEVEL.equals(ValueType.valueOf(metric.getValueType()))) {
return levelMetricOrdering(isAscending, metric, measuresByComponentUuidAndMetric); return levelMetricOrdering(isAscending, metric, measuresByComponentUuidAndMetric);
} else if (ValueType.DATA.equals(ValueType.valueOf(metric.getValueType()))
&& DataSupportedMetrics.IMPACTS_SUPPORTED_METRICS.contains(metric.getKey())) {
return totalMetricOrdering(isAscending, metric, measuresByComponentUuidAndMetric);
} }


throw new IllegalStateException("Unrecognized metric value type: " + metric.getValueType()); throw new IllegalStateException("Unrecognized metric value type: " + metric.getValueType());
} }


private static Ordering<ComponentDto> metricPeriodOrdering(ComponentTreeRequest wsRequest, List<MetricDto> metrics, private static Ordering<ComponentDto> metricPeriodOrdering(ComponentTreeRequest wsRequest, List<MetricDto> metrics,
Table<String, MetricDto, ComponentTreeData.Measure> measuresByComponentUuidAndMetric) {
Table<String, MetricDto, ComponentTreeData.Measure> measuresByComponentUuidAndMetric) {
boolean isAscending = Optional.ofNullable(wsRequest.getAsc()).orElse(false);
if (wsRequest.getMetricSort() == null || wsRequest.getMetricPeriodSort() == null) { if (wsRequest.getMetricSort() == null || wsRequest.getMetricPeriodSort() == null) {
return componentNameOrdering(wsRequest.getAsc());
return componentNameOrdering(isAscending);
} }
Map<String, MetricDto> metricsByKey = Maps.uniqueIndex(metrics, MetricDto::getKey); Map<String, MetricDto> metricsByKey = Maps.uniqueIndex(metrics, MetricDto::getKey);
MetricDto metric = metricsByKey.get(wsRequest.getMetricSort()); MetricDto metric = metricsByKey.get(wsRequest.getMetricSort());


ValueType metricValueType = ValueType.valueOf(metric.getValueType()); ValueType metricValueType = ValueType.valueOf(metric.getValueType());
if (NUMERIC_VALUE_TYPES.contains(metricValueType)) { if (NUMERIC_VALUE_TYPES.contains(metricValueType)) {
return numericalMetricPeriodOrdering(wsRequest, metric, measuresByComponentUuidAndMetric);
return numericalMetricPeriodOrdering(isAscending, metric, measuresByComponentUuidAndMetric);
} else if (ValueType.DATA.equals(metricValueType)
&& DataSupportedMetrics.IMPACTS_SUPPORTED_METRICS.contains(metric.getKey())) {
return totalNewPeriodMetricOrdering(isAscending, metric, measuresByComponentUuidAndMetric);
} }


throw BadRequestException.create(format("Impossible to sort metric '%s' by measure period.", metric.getKey())); throw BadRequestException.create(format("Impossible to sort metric '%s' by measure period.", metric.getKey()));


private static Ordering<ComponentDto> numericalMetricOrdering(boolean isAscending, @Nullable MetricDto metric, private static Ordering<ComponentDto> numericalMetricOrdering(boolean isAscending, @Nullable MetricDto metric,
Table<String, MetricDto, ComponentTreeData.Measure> measuresByComponentUuidAndMetric) { Table<String, MetricDto, ComponentTreeData.Measure> measuresByComponentUuidAndMetric) {
Ordering<Double> ordering = Ordering.natural();
Ordering<Double> ordering = getOrdering(isAscending);

return ordering.onResultOf(new ComponentDtoToNumericalMeasureValue(metric, measuresByComponentUuidAndMetric));
}

@NotNull
private static <T extends Comparable<T>> Ordering<T> getOrdering(boolean isAscending) {
Ordering<T> ordering = Ordering.natural();


if (!isAscending) { if (!isAscending) {
ordering = ordering.reverse(); ordering = ordering.reverse();
} }
return ordering.nullsLast();
}


return ordering.nullsLast().onResultOf(new ComponentDtoToNumericalMeasureValue(metric, measuresByComponentUuidAndMetric));
private static Ordering<ComponentDto> totalMetricOrdering(boolean isAscending, MetricDto metric,
Table<String, MetricDto, ComponentTreeData.Measure> measuresByComponentUuidAndMetric) {
Ordering<Long> ordering = getOrdering(isAscending);
return ordering.onResultOf(new ComponentDtoToTotalImpactMeasureValue(metric, measuresByComponentUuidAndMetric, false));
} }


private static Ordering<ComponentDto> numericalMetricPeriodOrdering(ComponentTreeRequest request, @Nullable MetricDto metric,
Table<String, MetricDto, ComponentTreeData.Measure> measuresByComponentUuidAndMetric) {
Ordering<Double> ordering = Ordering.natural();
private static Ordering<ComponentDto> totalNewPeriodMetricOrdering(boolean isAscending, MetricDto metric,
Table<String, MetricDto, ComponentTreeData.Measure> measuresByComponentUuidAndMetric) {
Ordering<Long> ordering = getOrdering(isAscending);
return ordering.onResultOf(new ComponentDtoToTotalImpactMeasureValue(metric, measuresByComponentUuidAndMetric, true));
}


if (!request.getAsc()) {
ordering = ordering.reverse();
}


return ordering.nullsLast().onResultOf(new ComponentDtoToMeasureVariationValue(metric, measuresByComponentUuidAndMetric));
private static Ordering<ComponentDto> numericalMetricPeriodOrdering(boolean isAscending, @Nullable MetricDto metric,
Table<String, MetricDto, ComponentTreeData.Measure> measuresByComponentUuidAndMetric) {
Ordering<Double> ordering = getOrdering(isAscending);

return ordering.onResultOf(new ComponentDtoToMeasureVariationValue(metric, measuresByComponentUuidAndMetric));
} }


private static Ordering<ComponentDto> levelMetricOrdering(boolean isAscending, @Nullable MetricDto metric, private static Ordering<ComponentDto> levelMetricOrdering(boolean isAscending, @Nullable MetricDto metric,
Table<String, MetricDto, ComponentTreeData.Measure> measuresByComponentUuidAndMetric) { Table<String, MetricDto, ComponentTreeData.Measure> measuresByComponentUuidAndMetric) {
Ordering<Integer> ordering = Ordering.natural();

// inverse the order of org.sonar.api.measures.Metric.Level
if (isAscending) {
ordering = ordering.reverse();
}
// inverse the order of org.sonar.api.measures.Metric.Level enum
Ordering<Integer> ordering = getOrdering(!isAscending);


return ordering.nullsLast().onResultOf(new ComponentDtoToLevelIndex(metric, measuresByComponentUuidAndMetric));
return ordering.onResultOf(new ComponentDtoToLevelIndex(metric, measuresByComponentUuidAndMetric));
} }


private static class ComponentDtoToNumericalMeasureValue implements Function<ComponentDto, Double> { private static class ComponentDtoToNumericalMeasureValue implements Function<ComponentDto, Double> {
} }
} }


private static class ComponentDtoToTotalImpactMeasureValue implements Function<ComponentDto, Long> {
private final MetricDto metric;
private final Table<String, MetricDto, ComponentTreeData.Measure> measuresByComponentUuidAndMetric;
private final boolean onlyNewPeriodMeasures;

//Store the total value for each component to avoid multiple deserialization of the same measure
Map<String, Long> totalByComponentUuid = new HashMap<>();

private ComponentDtoToTotalImpactMeasureValue(@Nullable MetricDto metric,
Table<String, MetricDto, ComponentTreeData.Measure> measuresByComponentUuidAndMetric, boolean onlyNewPeriodMeasures) {
this.metric = metric;
this.measuresByComponentUuidAndMetric = measuresByComponentUuidAndMetric;
this.onlyNewPeriodMeasures = onlyNewPeriodMeasures;
}

@Override
public Long apply(@Nonnull ComponentDto input) {
if (onlyNewPeriodMeasures && metric != null && !metric.getKey().startsWith("new_")) {
return null;
}
return totalByComponentUuid.computeIfAbsent(input.uuid(),
k -> {
ComponentTreeData.Measure measure = measuresByComponentUuidAndMetric.get(input.uuid(), metric);
return Optional.ofNullable(measure).map(ComponentTreeData.Measure::getData)
.map(data -> ImpactMeasureBuilder.fromString(measure.getData()).getTotal())
.orElse(null);
});
}
}

} }

+ 48
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/ws/DataSupportedMetrics.java View File

/*
* SonarQube
* Copyright (C) 2009-2024 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.measure.ws;

import java.util.Set;

import static org.sonar.api.measures.CoreMetrics.MAINTAINABILITY_ISSUES_KEY;
import static org.sonar.api.measures.CoreMetrics.NEW_MAINTAINABILITY_ISSUES_KEY;
import static org.sonar.api.measures.CoreMetrics.NEW_RELIABILITY_ISSUES_KEY;
import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_ISSUES_KEY;
import static org.sonar.api.measures.CoreMetrics.RELIABILITY_ISSUES_KEY;
import static org.sonar.api.measures.CoreMetrics.SECURITY_ISSUES_KEY;


/**
* This class contains the list of metrics that are supported in the web-api for the data type.
*/
public class DataSupportedMetrics {

public static final Set<String> IMPACTS_SUPPORTED_METRICS = Set.of(
SECURITY_ISSUES_KEY,
MAINTAINABILITY_ISSUES_KEY,
RELIABILITY_ISSUES_KEY,
NEW_SECURITY_ISSUES_KEY,
NEW_MAINTAINABILITY_ISSUES_KEY,
NEW_RELIABILITY_ISSUES_KEY);

private DataSupportedMetrics() {
// only static methods
}
}

+ 85
- 19
server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/ws/ComponentTreeSortTest.java View File

import com.google.common.collect.Table; import com.google.common.collect.Table;
import java.util.List; import java.util.List;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import org.junit.Before;
import org.junit.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.sonar.api.issue.impact.Severity;
import org.sonar.api.measures.CoreMetrics; import org.sonar.api.measures.CoreMetrics;
import org.sonar.api.measures.Metric.ValueType; import org.sonar.api.measures.Metric.ValueType;
import org.sonar.api.resources.Qualifiers; import org.sonar.api.resources.Qualifiers;
import org.sonar.db.component.ComponentDto; import org.sonar.db.component.ComponentDto;
import org.sonar.db.measure.LiveMeasureDto; import org.sonar.db.measure.LiveMeasureDto;
import org.sonar.db.metric.MetricDto; import org.sonar.db.metric.MetricDto;
import org.sonar.server.measure.ImpactMeasureBuilder;


import static com.google.common.collect.Lists.newArrayList; import static com.google.common.collect.Lists.newArrayList;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
import static org.sonar.server.measure.ws.ComponentTreeAction.QUALIFIER_SORT; import static org.sonar.server.measure.ws.ComponentTreeAction.QUALIFIER_SORT;
import static org.sonar.server.measure.ws.ComponentTreeData.Measure.createFromMeasureDto; import static org.sonar.server.measure.ws.ComponentTreeData.Measure.createFromMeasureDto;


public class ComponentTreeSortTest {
class ComponentTreeSortTest {
private static final String NUM_METRIC_KEY = "violations"; private static final String NUM_METRIC_KEY = "violations";
private static final String NEW_METRIC_KEY = "new_violations"; private static final String NEW_METRIC_KEY = "new_violations";
private static final String TEXT_METRIC_KEY = "sqale_index"; private static final String TEXT_METRIC_KEY = "sqale_index";


private static final String DATA_IMPACT_METRIC_KEY = "reliability_issues";
private static final String NEW_DATA_IMPACT_METRIC_KEY = "new_reliability_issues";

private List<MetricDto> metrics; private List<MetricDto> metrics;
private Table<String, MetricDto, ComponentTreeData.Measure> measuresByComponentUuidAndMetric; private Table<String, MetricDto, ComponentTreeData.Measure> measuresByComponentUuidAndMetric;
private List<ComponentDto> components; private List<ComponentDto> components;


@Before
public void setUp() {
@BeforeEach
void setUp() {
components = newArrayList( components = newArrayList(
newComponentWithoutSnapshotId("name-1", "qualifier-2", "path-9"), newComponentWithoutSnapshotId("name-1", "qualifier-2", "path-9"),
newComponentWithoutSnapshotId("name-3", "qualifier-3", "path-8"), newComponentWithoutSnapshotId("name-3", "qualifier-3", "path-8"),
MetricDto sqaleIndexMetric = newMetricDto() MetricDto sqaleIndexMetric = newMetricDto()
.setKey(TEXT_METRIC_KEY) .setKey(TEXT_METRIC_KEY)
.setValueType(ValueType.STRING.name()); .setValueType(ValueType.STRING.name());
MetricDto reliabilityIssueMetric = newMetricDto()
.setKey(DATA_IMPACT_METRIC_KEY)
.setValueType(ValueType.DATA.name());
MetricDto newReliabilityIssueMetric = newMetricDto()
.setKey(NEW_DATA_IMPACT_METRIC_KEY)
.setValueType(ValueType.DATA.name());


metrics = newArrayList(violationsMetric, sqaleIndexMetric, newViolationsMetric);
metrics = newArrayList(violationsMetric, sqaleIndexMetric, newViolationsMetric, reliabilityIssueMetric, newReliabilityIssueMetric);


measuresByComponentUuidAndMetric = HashBasedTable.create(components.size(), 2); measuresByComponentUuidAndMetric = HashBasedTable.create(components.size(), 2);
// same number than path field // same number than path field
measuresByComponentUuidAndMetric.put(component.uuid(), violationsMetric, createFromMeasureDto(new LiveMeasureDto().setValue(currentValue))); measuresByComponentUuidAndMetric.put(component.uuid(), violationsMetric, createFromMeasureDto(new LiveMeasureDto().setValue(currentValue)));
measuresByComponentUuidAndMetric.put(component.uuid(), newViolationsMetric, createFromMeasureDto(new LiveMeasureDto().setValue(currentValue))); measuresByComponentUuidAndMetric.put(component.uuid(), newViolationsMetric, createFromMeasureDto(new LiveMeasureDto().setValue(currentValue)));
measuresByComponentUuidAndMetric.put(component.uuid(), sqaleIndexMetric, createFromMeasureDto(new LiveMeasureDto().setData(String.valueOf(currentValue)))); measuresByComponentUuidAndMetric.put(component.uuid(), sqaleIndexMetric, createFromMeasureDto(new LiveMeasureDto().setData(String.valueOf(currentValue))));
measuresByComponentUuidAndMetric.put(component.uuid(), reliabilityIssueMetric, createFromMeasureDto(new LiveMeasureDto().setData(buildJsonImpact((int) currentValue))));
measuresByComponentUuidAndMetric.put(component.uuid(), newReliabilityIssueMetric, createFromMeasureDto(new LiveMeasureDto().setData(buildJsonImpact((int) currentValue))));
currentValue--; currentValue--;
} }
} }


private static String buildJsonImpact(int currentValue) {
return String.valueOf(ImpactMeasureBuilder.newInstance()
.setSeverity(Severity.HIGH, currentValue)
.setSeverity(Severity.MEDIUM, currentValue)
.setSeverity(Severity.LOW, currentValue)
.setTotal(currentValue)
.buildAsString());
}

@Test @Test
public void sort_by_names() {
void sort_by_names() {
ComponentTreeRequest wsRequest = newRequest(singletonList(NAME_SORT), true, null); ComponentTreeRequest wsRequest = newRequest(singletonList(NAME_SORT), true, null);
List<ComponentDto> result = sortComponents(wsRequest); List<ComponentDto> result = sortComponents(wsRequest);


} }


@Test @Test
public void sort_by_qualifier() {
void sort_by_qualifier() {
ComponentTreeRequest wsRequest = newRequest(singletonList(QUALIFIER_SORT), false, null); ComponentTreeRequest wsRequest = newRequest(singletonList(QUALIFIER_SORT), false, null);


List<ComponentDto> result = sortComponents(wsRequest); List<ComponentDto> result = sortComponents(wsRequest);
} }


@Test @Test
public void sort_by_path() {
void sort_by_path() {
ComponentTreeRequest wsRequest = newRequest(singletonList(PATH_SORT), true, null); ComponentTreeRequest wsRequest = newRequest(singletonList(PATH_SORT), true, null);


List<ComponentDto> result = sortComponents(wsRequest); List<ComponentDto> result = sortComponents(wsRequest);
} }


@Test @Test
public void sort_by_numerical_metric_key_ascending() {
void sort_by_numerical_metric_key_ascending() {
components.add(newComponentWithoutSnapshotId("name-without-measure", "qualifier-without-measure", "path-without-measure")); components.add(newComponentWithoutSnapshotId("name-without-measure", "qualifier-without-measure", "path-without-measure"));
ComponentTreeRequest wsRequest = newRequest(singletonList(METRIC_SORT), true, NUM_METRIC_KEY); ComponentTreeRequest wsRequest = newRequest(singletonList(METRIC_SORT), true, NUM_METRIC_KEY);


} }


@Test @Test
public void sort_by_numerical_metric_key_descending() {
void sort_by_numerical_metric_key_descending() {
components.add(newComponentWithoutSnapshotId("name-without-measure", "qualifier-without-measure", "path-without-measure")); components.add(newComponentWithoutSnapshotId("name-without-measure", "qualifier-without-measure", "path-without-measure"));
ComponentTreeRequest wsRequest = newRequest(singletonList(METRIC_SORT), false, NUM_METRIC_KEY); ComponentTreeRequest wsRequest = newRequest(singletonList(METRIC_SORT), false, NUM_METRIC_KEY);


} }


@Test @Test
public void sort_by_name_ascending_in_case_of_equality() {
void sort_by_name_ascending_in_case_of_equality() {
components = newArrayList( components = newArrayList(
newComponentWithoutSnapshotId("PROJECT 12", Qualifiers.PROJECT, "PROJECT_PATH_1"), newComponentWithoutSnapshotId("PROJECT 12", Qualifiers.PROJECT, "PROJECT_PATH_1"),
newComponentWithoutSnapshotId("PROJECT 11", Qualifiers.PROJECT, "PROJECT_PATH_1"), newComponentWithoutSnapshotId("PROJECT 11", Qualifiers.PROJECT, "PROJECT_PATH_1"),
} }


@Test @Test
public void sort_by_alert_status_ascending() {
void sort_by_alert_status_ascending() {
components = newArrayList( components = newArrayList(
newComponentWithoutSnapshotId("PROJECT OK 1", Qualifiers.PROJECT, "PROJECT_OK_PATH_1"), newComponentWithoutSnapshotId("PROJECT OK 1", Qualifiers.PROJECT, "PROJECT_OK_PATH_1"),
newComponentWithoutSnapshotId("PROJECT ERROR 1", Qualifiers.PROJECT, "PROJECT_ERROR_PATH_1"), newComponentWithoutSnapshotId("PROJECT ERROR 1", Qualifiers.PROJECT, "PROJECT_ERROR_PATH_1"),
} }


@Test @Test
public void sort_by_numerical_metric_period_1_key_descending() {
void sort_by_numerical_metric_period_1_key_descending() {
components.add(newComponentWithoutSnapshotId("name-without-measure", "qualifier-without-measure", "path-without-measure")); components.add(newComponentWithoutSnapshotId("name-without-measure", "qualifier-without-measure", "path-without-measure"));
ComponentTreeRequest wsRequest = newRequest(singletonList(METRIC_PERIOD_SORT), false, NEW_METRIC_KEY).setMetricPeriodSort(1); ComponentTreeRequest wsRequest = newRequest(singletonList(METRIC_PERIOD_SORT), false, NEW_METRIC_KEY).setMetricPeriodSort(1);


} }


@Test @Test
public void sort_by_numerical_metric_period_1_key_ascending() {
void sort_by_numerical_metric_period_1_key_ascending() {
components.add(newComponentWithoutSnapshotId("name-without-measure", "qualifier-without-measure", "path-without-measure")); components.add(newComponentWithoutSnapshotId("name-without-measure", "qualifier-without-measure", "path-without-measure"));
ComponentTreeRequest wsRequest = newRequest(singletonList(METRIC_PERIOD_SORT), true, NEW_METRIC_KEY).setMetricPeriodSort(1); ComponentTreeRequest wsRequest = newRequest(singletonList(METRIC_PERIOD_SORT), true, NEW_METRIC_KEY).setMetricPeriodSort(1);


} }


@Test @Test
public void sort_by_numerical_metric_period_5_key() {
void sort_by_numerical_metric_period_5_key() {
components.add(newComponentWithoutSnapshotId("name-without-measure", "qualifier-without-measure", "path-without-measure")); components.add(newComponentWithoutSnapshotId("name-without-measure", "qualifier-without-measure", "path-without-measure"));
ComponentTreeRequest wsRequest = newRequest(singletonList(METRIC_SORT), false, NUM_METRIC_KEY).setMetricPeriodSort(5); ComponentTreeRequest wsRequest = newRequest(singletonList(METRIC_SORT), false, NUM_METRIC_KEY).setMetricPeriodSort(5);


} }


@Test @Test
public void sort_by_textual_metric_key_ascending() {
void sort_by_textual_metric_key_ascending() {
components.add(newComponentWithoutSnapshotId("name-without-measure", "qualifier-without-measure", "path-without-measure")); components.add(newComponentWithoutSnapshotId("name-without-measure", "qualifier-without-measure", "path-without-measure"));
ComponentTreeRequest wsRequest = newRequest(singletonList(METRIC_SORT), true, TEXT_METRIC_KEY); ComponentTreeRequest wsRequest = newRequest(singletonList(METRIC_SORT), true, TEXT_METRIC_KEY);


} }


@Test @Test
public void sort_by_textual_metric_key_descending() {
void sort_by_textual_metric_key_descending() {
components.add(newComponentWithoutSnapshotId("name-without-measure", "qualifier-without-measure", "path-without-measure")); components.add(newComponentWithoutSnapshotId("name-without-measure", "qualifier-without-measure", "path-without-measure"));
ComponentTreeRequest wsRequest = newRequest(singletonList(METRIC_SORT), false, TEXT_METRIC_KEY); ComponentTreeRequest wsRequest = newRequest(singletonList(METRIC_SORT), false, TEXT_METRIC_KEY);


} }


@Test @Test
public void sort_on_multiple_fields() {
void sort_on_multiple_fields() {
components = newArrayList( components = newArrayList(
newComponentWithoutSnapshotId("name-1", "qualifier-1", "path-2"), newComponentWithoutSnapshotId("name-1", "qualifier-1", "path-2"),
newComponentWithoutSnapshotId("name-1", "qualifier-1", "path-3"), newComponentWithoutSnapshotId("name-1", "qualifier-1", "path-3"),
.containsExactly("path-1", "path-2", "path-3"); .containsExactly("path-1", "path-2", "path-3");
} }


@Test
void sortComponent_whenMetricIsImpactDataType_shouldOrderByTotalAscending() {
components.add(newComponentWithoutSnapshotId("name-without-measure", "qualifier-without-measure", "path-without-measure"));
ComponentTreeRequest wsRequest = newRequest(singletonList(METRIC_SORT), true, DATA_IMPACT_METRIC_KEY);

List<ComponentDto> result = sortComponents(wsRequest);

assertThat(result).extracting("path")
.containsExactly("path-1", "path-2", "path-3", "path-4", "path-5", "path-6", "path-7", "path-8", "path-9", "path-without-measure");
}

@Test
void sortComponent_whenMetricIsImpactDataType_shouldOrderByTotalDescending() {
components.add(newComponentWithoutSnapshotId("name-without-measure", "qualifier-without-measure", "path-without-measure"));
ComponentTreeRequest wsRequest = newRequest(singletonList(METRIC_SORT), false, DATA_IMPACT_METRIC_KEY);

List<ComponentDto> result = sortComponents(wsRequest);

assertThat(result).extracting("path")
.containsExactly("path-9", "path-8", "path-7", "path-6", "path-5", "path-4", "path-3", "path-2", "path-1", "path-without-measure");
}

@Test
void sortComponent_whenMetricIsNewAndMetricPeriodSort_shouldOrderByTotal() {
components.add(newComponentWithoutSnapshotId("name-without-measure", "qualifier-without-measure", "path-without-measure"));
ComponentTreeRequest wsRequest = newRequest(singletonList(METRIC_PERIOD_SORT), false, NEW_DATA_IMPACT_METRIC_KEY).setMetricPeriodSort(1);

List<ComponentDto> result = sortComponents(wsRequest);

assertThat(result).extracting("path")
.containsExactly("path-9", "path-8", "path-7", "path-6", "path-5", "path-4", "path-3", "path-2", "path-1", "path-without-measure");
}

@Test
void sortComponent_whenMetricIsNotNewAndMetricPeriodSort_shouldNotOrder() {
components.add(newComponentWithoutSnapshotId("name-without-measure", "qualifier-without-measure", "path-without-measure"));
ComponentTreeRequest wsRequest = newRequest(singletonList(METRIC_PERIOD_SORT), false, DATA_IMPACT_METRIC_KEY).setMetricPeriodSort(1);

List<ComponentDto> result = sortComponents(wsRequest);

assertThat(result).extracting("path")
.containsExactly("path-9", "path-7", "path-8", "path-6", "path-3", "path-4", "path-5", "path-1", "path-2", "path-without-measure");
}

private List<ComponentDto> sortComponents(ComponentTreeRequest wsRequest) { private List<ComponentDto> sortComponents(ComponentTreeRequest wsRequest) {
return ComponentTreeSort.sortComponents(components, wsRequest, metrics, measuresByComponentUuidAndMetric); return ComponentTreeSort.sortComponents(components, wsRequest, metrics, measuresByComponentUuidAndMetric);
} }

Loading…
Cancel
Save