@@ -20,10 +20,12 @@ | |||
package org.sonar.server.measure.ws; | |||
import java.util.Map; | |||
import java.util.function.Function; | |||
import org.sonar.db.component.ComponentDto; | |||
import org.sonar.db.measure.MeasureDto; | |||
import org.sonar.db.metric.MetricDto; | |||
import org.sonarqube.ws.WsMeasures; | |||
import org.sonarqube.ws.WsMeasures.Component; | |||
import org.sonarqube.ws.WsMeasures.SearchWsResponse; | |||
import static org.sonar.server.measure.ws.MeasureDtoToWsMeasure.measureDtoToWsMeasure; | |||
@@ -32,9 +34,9 @@ class ComponentDtoToWsComponent { | |||
// static methods only | |||
} | |||
static WsMeasures.Component.Builder componentDtoToWsComponent(ComponentDto component, Map<MetricDto, MeasureDto> measuresByMetric, | |||
static Component.Builder componentDtoToWsComponent(ComponentDto component, Map<MetricDto, MeasureDto> measuresByMetric, | |||
Map<String, ComponentDto> referenceComponentsByUuid) { | |||
WsMeasures.Component.Builder wsComponent = componentDtoToWsComponent(component); | |||
Component.Builder wsComponent = componentDtoToWsComponent(component); | |||
ComponentDto referenceComponent = referenceComponentsByUuid.get(component.getCopyResourceUuid()); | |||
if (referenceComponent != null) { | |||
@@ -49,12 +51,16 @@ class ComponentDtoToWsComponent { | |||
return wsComponent; | |||
} | |||
static WsMeasures.Component dbToWsComponent(ComponentDto dbComponent, Iterable<WsMeasures.Measure> measures) { | |||
return componentDtoToWsComponent(dbComponent).addAllMeasures(measures).build(); | |||
static Function<ComponentDto, SearchWsResponse.Component> dbToWsComponent() { | |||
return dbComponent -> SearchWsResponse.Component.newBuilder() | |||
.setId(dbComponent.uuid()) | |||
.setKey(dbComponent.key()) | |||
.setName(dbComponent.name()) | |||
.build(); | |||
} | |||
static WsMeasures.Component.Builder componentDtoToWsComponent(ComponentDto component) { | |||
WsMeasures.Component.Builder wsComponent = WsMeasures.Component.newBuilder() | |||
static Component.Builder componentDtoToWsComponent(ComponentDto component) { | |||
Component.Builder wsComponent = Component.newBuilder() | |||
.setId(component.uuid()) | |||
.setKey(component.key()) | |||
.setName(component.name()) |
@@ -22,6 +22,7 @@ package org.sonar.server.measure.ws; | |||
import org.sonar.db.measure.MeasureDto; | |||
import org.sonar.db.metric.MetricDto; | |||
import org.sonarqube.ws.WsMeasures; | |||
import org.sonarqube.ws.WsMeasures.Measure; | |||
import static org.sonar.server.measure.ws.MeasureValueFormatter.formatMeasureValue; | |||
import static org.sonar.server.measure.ws.MeasureValueFormatter.formatNumericalValue; | |||
@@ -32,29 +33,42 @@ class MeasureDtoToWsMeasure { | |||
// static methods | |||
} | |||
static WsMeasures.Measure measureDtoToWsMeasure(MetricDto metricDto, MeasureDto measureDto) { | |||
/** | |||
* @see #measureDtoToWsMeasure(MetricDto, MeasureDto) | |||
* add component uuid to the WS Measure object | |||
* | |||
*/ | |||
static Measure dbToWsMeasure(MeasureDto dbMeasure, MetricDto dbMetric) { | |||
return map(dbMetric, dbMeasure).setComponent(dbMeasure.getComponentUuid()).build(); | |||
} | |||
static Measure measureDtoToWsMeasure(MetricDto metricDto, MeasureDto measureDto) { | |||
return map(metricDto, measureDto).build(); | |||
} | |||
private static Measure.Builder map(MetricDto dbMetric, MeasureDto dbMeasure) { | |||
try { | |||
WsMeasures.Measure.Builder measure = WsMeasures.Measure.newBuilder(); | |||
measure.setMetric(metricDto.getKey()); | |||
Measure.Builder measure = Measure.newBuilder(); | |||
measure.setMetric(dbMetric.getKey()); | |||
// a measure value can be null, new_violations metric for example | |||
if (measureDto.getValue() != null | |||
|| measureDto.getData() != null) { | |||
measure.setValue(formatMeasureValue(measureDto, metricDto)); | |||
if (dbMeasure.getValue() != null | |||
|| dbMeasure.getData() != null) { | |||
measure.setValue(formatMeasureValue(dbMeasure, dbMetric)); | |||
} | |||
WsMeasures.PeriodValue.Builder periodBuilder = WsMeasures.PeriodValue.newBuilder(); | |||
for (int i = 1; i <= 5; i++) { | |||
if (measureDto.getVariation(i) != null) { | |||
if (dbMeasure.getVariation(i) != null) { | |||
measure.getPeriodsBuilder().addPeriodsValue(periodBuilder | |||
.clear() | |||
.setIndex(i) | |||
.setValue(formatNumericalValue(measureDto.getVariation(i), metricDto))); | |||
.setValue(formatNumericalValue(dbMeasure.getVariation(i), dbMetric))); | |||
} | |||
} | |||
return measure.build(); | |||
return measure; | |||
} catch (Exception e) { | |||
throw new IllegalStateException(String.format("Error while mapping a measure of metric key '%s' and parameters %s", metricDto.getKey(), measureDto.toString()), e); | |||
throw new IllegalStateException(String.format("Error while mapping a measure of metric key '%s' and parameters %s", dbMetric.getKey(), dbMeasure.toString()), e); | |||
} | |||
} | |||
} |
@@ -45,18 +45,20 @@ import org.sonar.db.measure.MeasureDto; | |||
import org.sonar.db.measure.MeasureQuery; | |||
import org.sonar.db.metric.MetricDto; | |||
import org.sonarqube.ws.WsMeasures; | |||
import org.sonarqube.ws.WsMeasures.Measure; | |||
import org.sonarqube.ws.WsMeasures.SearchWsResponse; | |||
import org.sonarqube.ws.WsMeasures.SearchWsResponse.Component; | |||
import org.sonarqube.ws.client.measure.SearchRequest; | |||
import static com.google.common.base.Preconditions.checkArgument; | |||
import static com.google.common.collect.Maps.immutableEntry; | |||
import static java.util.Comparator.comparing; | |||
import static java.util.Objects.requireNonNull; | |||
import static java.util.function.Function.identity; | |||
import static org.sonar.core.util.Uuids.UUID_EXAMPLE_01; | |||
import static org.sonar.core.util.Uuids.UUID_EXAMPLE_02; | |||
import static org.sonar.core.util.Uuids.UUID_EXAMPLE_03; | |||
import static org.sonar.server.measure.ws.ComponentDtoToWsComponent.dbToWsComponent; | |||
import static org.sonar.server.measure.ws.MeasureDtoToWsMeasure.measureDtoToWsMeasure; | |||
import static org.sonar.server.measure.ws.MeasureDtoToWsMeasure.dbToWsMeasure; | |||
import static org.sonar.server.measure.ws.MeasuresWsParametersBuilder.createMetricKeysParameter; | |||
import static org.sonar.server.measure.ws.MetricDtoWithBestValue.buildBestMeasure; | |||
import static org.sonar.server.measure.ws.MetricDtoWithBestValue.isEligibleForBestValue; | |||
@@ -133,7 +135,7 @@ public class SearchAction implements MeasuresWsAction { | |||
} | |||
SearchWsResponse build() { | |||
this.request = setRequest(); | |||
this.request = createRequest(); | |||
this.components = searchComponents(); | |||
this.metrics = searchMetrics(); | |||
this.measures = searchMeasures(); | |||
@@ -149,16 +151,13 @@ public class SearchAction implements MeasuresWsAction { | |||
return dbClient.snapshotDao().selectLastAnalysesByRootComponentUuids(dbSession, projectUuids); | |||
} | |||
private SearchRequest setRequest() { | |||
private SearchRequest createRequest() { | |||
request = SearchRequest.builder() | |||
.setMetricKeys(httpRequest.mandatoryParamAsStrings(PARAM_METRIC_KEYS)) | |||
.setComponentIds(httpRequest.paramAsStrings(PARAM_COMPONENT_IDS)) | |||
.setComponentKeys(httpRequest.paramAsStrings(PARAM_COMPONENT_KEYS)) | |||
.build(); | |||
this.components = searchComponents(); | |||
this.metrics = searchMetrics(); | |||
return request; | |||
} | |||
@@ -200,11 +199,6 @@ public class SearchAction implements MeasuresWsAction { | |||
requireNonNull(components); | |||
requireNonNull(metrics); | |||
Set<String> projectUuids = components.stream().map(ComponentDto::projectUuid).collect(Collectors.toSet()); | |||
dbClient.snapshotDao().selectLastAnalysesByRootComponentUuids(dbSession, projectUuids).stream() | |||
.map(SnapshotDtoToWsPeriods::snapshotToWsPeriods); | |||
return dbClient.measureDao().selectByQuery(dbSession, MeasureQuery.builder() | |||
.setComponentUuids(components.stream().map(ComponentDto::uuid).collect(Collectors.toList())) | |||
.setMetricIds(metrics.stream().map(MetricDto::getId).collect(Collectors.toList())) | |||
@@ -226,34 +220,35 @@ public class SearchAction implements MeasuresWsAction { | |||
requireNonNull(components); | |||
requireNonNull(snapshots); | |||
Map<Integer, MetricDto> metricsById = metrics.stream().collect(Collectors.toMap(MetricDto::getId, identity())); | |||
List<Measure> wsMeasures = buildWsMeasures(); | |||
List<Component> wsComponents = buildWsComponents(); | |||
ImmutableMultimap<String, WsMeasures.Measure> wsMeasuresByComponentUuid = Stream | |||
.concat(measures.stream(), buildBestMeasures().stream()) | |||
.collect(Collectors.toMap(identity(), MeasureDto::getComponentUuid)) | |||
.entrySet().stream() | |||
.map(entry -> immutableEntry( | |||
measureDtoToWsMeasure(metricsById.get(entry.getKey().getMetricId()), entry.getKey()), | |||
entry.getValue())) | |||
.sorted((e1, e2) -> e1.getKey().getMetric().compareTo(e2.getKey().getMetric())) | |||
.collect(Collector.of( | |||
ImmutableMultimap::<String, WsMeasures.Measure>builder, | |||
(result, entry) -> result.put(entry.getValue(), entry.getKey()), | |||
(result1, result2) -> { | |||
throw new IllegalStateException("Parallel execution forbidden while building WS measures"); | |||
}, | |||
ImmutableMultimap.Builder::build)); | |||
return SearchWsResponse.newBuilder() | |||
.addAllMeasures(wsMeasures) | |||
.addAllComponents(wsComponents) | |||
.build(); | |||
} | |||
private List<Component> buildWsComponents() { | |||
return components.stream() | |||
.map(dbComponent -> dbToWsComponent(dbComponent, wsMeasuresByComponentUuid.get(dbComponent.uuid()))) | |||
.sorted((c1, c2) -> c1.getName().compareTo(c2.getName())) | |||
.collect(Collector.of( | |||
SearchWsResponse::newBuilder, | |||
SearchWsResponse.Builder::addComponents, | |||
(result1, result2) -> { | |||
throw new IllegalStateException("Parallel execution forbidden while building SearchWsResponse"); | |||
}, | |||
SearchWsResponse.Builder::build)); | |||
.map(dbToWsComponent()) | |||
.sorted(comparing(Component::getName)) | |||
.collect(Collectors.toList()); | |||
} | |||
private List<Measure> buildWsMeasures() { | |||
Map<String, String> componentNamesByUuid = components.stream().collect(Collectors.toMap(ComponentDto::uuid, ComponentDto::name)); | |||
Map<Integer, MetricDto> metricsById = metrics.stream().collect(Collectors.toMap(MetricDto::getId, identity())); | |||
Function<MeasureDto, MetricDto> dbMeasureToDbMetric = dbMeasure -> metricsById.get(dbMeasure.getMetricId()); | |||
Function<Measure, String> byMetricKey = Measure::getMetric; | |||
Function<Measure, String> byComponentName = wsMeasure -> componentNamesByUuid.get(wsMeasure.getComponent()); | |||
return Stream | |||
.concat(measures.stream(), buildBestMeasures().stream()) | |||
.map(dbMeasure -> dbToWsMeasure(dbMeasure, dbMeasureToDbMetric.apply(dbMeasure))) | |||
.sorted(comparing(byMetricKey).thenComparing(byComponentName)) | |||
.collect(Collectors.toList()); | |||
} | |||
private List<MeasureDto> buildBestMeasures() { |
@@ -1,146 +1,140 @@ | |||
{ | |||
"components": [ | |||
"measures": [ | |||
{ | |||
"id": "AVIwDXE-bJbJqrw6wFv5", | |||
"key": "com.sonarsource:java-markdown:src/main/java/com/sonarsource/markdown/impl/ElementImpl.java", | |||
"name": "ElementImpl.java", | |||
"qualifier": "FIL", | |||
"path": "src/main/java/com/sonarsource/markdown/impl/ElementImpl.java", | |||
"language": "java", | |||
"measures": [ | |||
"metric": "complexity", | |||
"component": "AVIwDXE-bJbJqrw6wFv5", | |||
"value": "12" | |||
}, | |||
{ | |||
"metric": "complexity", | |||
"component": "project-id", | |||
"value": "42" | |||
}, | |||
{ | |||
"metric": "complexity", | |||
"component": "AVIwDXE-bJbJqrw6wFv8", | |||
"value": "35", | |||
"periods": [ | |||
{ | |||
"index": 2, | |||
"value": "0" | |||
} | |||
] | |||
}, | |||
{ | |||
"metric": "ncloc", | |||
"component": "AVIwDXE-bJbJqrw6wFv5", | |||
"value": "114" | |||
}, | |||
{ | |||
"metric": "ncloc", | |||
"component": "project-id", | |||
"value": "1984" | |||
}, | |||
{ | |||
"metric": "ncloc", | |||
"component": "AVIwDXE-bJbJqrw6wFv8", | |||
"value": "217", | |||
"periods": [ | |||
{ | |||
"index": 2, | |||
"value": "0" | |||
} | |||
] | |||
}, | |||
{ | |||
"metric": "new_violations", | |||
"component": "AVIwDXE-bJbJqrw6wFv5", | |||
"periods": [ | |||
{ | |||
"metric": "complexity", | |||
"value": "12" | |||
"index": 1, | |||
"value": "25" | |||
}, | |||
{ | |||
"metric": "ncloc", | |||
"value": "114" | |||
"index": 2, | |||
"value": "0" | |||
}, | |||
{ | |||
"metric": "new_violations", | |||
"periods": [ | |||
{ | |||
"index": 1, | |||
"value": "25" | |||
}, | |||
{ | |||
"index": 2, | |||
"value": "0" | |||
}, | |||
{ | |||
"index": 3, | |||
"value": "25" | |||
} | |||
] | |||
"index": 3, | |||
"value": "25" | |||
} | |||
] | |||
}, | |||
{ | |||
"id": "AVIwDXE_bJbJqrw6wFwJ", | |||
"key": "com.sonarsource:java-markdown:src/test/java/com/sonarsource/markdown/impl/ElementImplTest.java", | |||
"name": "ElementImplTest.java", | |||
"qualifier": "UTS", | |||
"path": "src/test/java/com/sonarsource/markdown/impl/ElementImplTest.java", | |||
"language": "java", | |||
"measures": [ | |||
"metric": "new_violations", | |||
"component": "AVIwDXE_bJbJqrw6wFwJ", | |||
"periods": [ | |||
{ | |||
"index": 1, | |||
"value": "0" | |||
}, | |||
{ | |||
"index": 2, | |||
"value": "0" | |||
}, | |||
{ | |||
"metric": "new_violations", | |||
"periods": [ | |||
{ | |||
"index": 1, | |||
"value": "0" | |||
}, | |||
{ | |||
"index": 2, | |||
"value": "0" | |||
}, | |||
{ | |||
"index": 3, | |||
"value": "0" | |||
} | |||
] | |||
"index": 3, | |||
"value": "0" | |||
} | |||
] | |||
}, | |||
{ | |||
"id": "project-id", | |||
"key": "MY_PROJECT", | |||
"name": "My Project", | |||
"description": "My Project Description", | |||
"qualifier": "TRK", | |||
"measures": [ | |||
"metric": "new_violations", | |||
"component": "project-id", | |||
"periods": [ | |||
{ | |||
"metric": "complexity", | |||
"value": "42" | |||
"index": 1, | |||
"value": "255" | |||
}, | |||
{ | |||
"metric": "ncloc", | |||
"value": "1984" | |||
"index": 2, | |||
"value": "0" | |||
}, | |||
{ | |||
"metric": "new_violations", | |||
"periods": [ | |||
{ | |||
"index": 1, | |||
"value": "255" | |||
}, | |||
{ | |||
"index": 2, | |||
"value": "0" | |||
}, | |||
{ | |||
"index": 3, | |||
"value": "255" | |||
} | |||
] | |||
"index": 3, | |||
"value": "255" | |||
} | |||
] | |||
}, | |||
{ | |||
"id": "AVIwDXE-bJbJqrw6wFv8", | |||
"key": "com.sonarsource:java-markdown:src/main/java/com/sonarsource/markdown/impl", | |||
"name": "src/main/java/com/sonarsource/markdown/impl", | |||
"qualifier": "DIR", | |||
"path": "src/main/java/com/sonarsource/markdown/impl", | |||
"measures": [ | |||
"metric": "new_violations", | |||
"component": "AVIwDXE-bJbJqrw6wFv8", | |||
"periods": [ | |||
{ | |||
"metric": "complexity", | |||
"value": "35", | |||
"periods": [ | |||
{ | |||
"index": 2, | |||
"value": "0" | |||
} | |||
] | |||
"index": 1, | |||
"value": "25" | |||
}, | |||
{ | |||
"metric": "ncloc", | |||
"value": "217", | |||
"periods": [ | |||
{ | |||
"index": 2, | |||
"value": "0" | |||
} | |||
] | |||
"index": 2, | |||
"value": "0" | |||
}, | |||
{ | |||
"metric": "new_violations", | |||
"periods": [ | |||
{ | |||
"index": 1, | |||
"value": "25" | |||
}, | |||
{ | |||
"index": 2, | |||
"value": "0" | |||
}, | |||
{ | |||
"index": 3, | |||
"value": "25" | |||
} | |||
] | |||
"index": 3, | |||
"value": "25" | |||
} | |||
] | |||
} | |||
], | |||
"components": [ | |||
{ | |||
"id": "AVIwDXE-bJbJqrw6wFv5", | |||
"key": "com.sonarsource:java-markdown:src/main/java/com/sonarsource/markdown/impl/ElementImpl.java", | |||
"name": "ElementImpl.java" | |||
}, | |||
{ | |||
"id": "AVIwDXE_bJbJqrw6wFwJ", | |||
"key": "com.sonarsource:java-markdown:src/test/java/com/sonarsource/markdown/impl/ElementImplTest.java", | |||
"name": "ElementImplTest.java" | |||
}, | |||
{ | |||
"id": "project-id", | |||
"key": "MY_PROJECT", | |||
"name": "My Project" | |||
}, | |||
{ | |||
"id": "AVIwDXE-bJbJqrw6wFv8", | |||
"key": "com.sonarsource:java-markdown:src/main/java/com/sonarsource/markdown/impl", | |||
"name": "src/main/java/com/sonarsource/markdown/impl" | |||
} | |||
] | |||
} |
@@ -43,8 +43,7 @@ import org.sonar.db.metric.MetricDto; | |||
import org.sonar.server.exceptions.BadRequestException; | |||
import org.sonar.server.ws.TestRequest; | |||
import org.sonar.server.ws.WsActionTester; | |||
import org.sonarqube.ws.WsMeasures; | |||
import org.sonarqube.ws.WsMeasures.Component; | |||
import org.sonarqube.ws.WsMeasures.Measure; | |||
import org.sonarqube.ws.WsMeasures.SearchWsResponse; | |||
import static com.google.common.collect.Lists.newArrayList; | |||
@@ -87,7 +86,6 @@ public class SearchActionTest { | |||
.getInput(); | |||
assertJson(result).withStrictArrayOrder().isSimilarTo(ws.getDef().responseExampleAsString()); | |||
} | |||
@Test | |||
@@ -98,18 +96,21 @@ public class SearchActionTest { | |||
SearchWsResponse result = callByComponentUuids(singletonList(dbComponent.uuid()), singletonList("complexity")); | |||
assertThat(result.getComponentsCount()).isEqualTo(1); | |||
Component wsComponent = result.getComponents(0); | |||
assertThat(wsComponent.getMeasuresCount()).isEqualTo(0); | |||
SearchWsResponse.Component wsComponent = result.getComponents(0); | |||
assertThat(wsComponent.getId()).isEqualTo(dbComponent.uuid()); | |||
assertThat(wsComponent.getKey()).isEqualTo(dbComponent.key()); | |||
assertThat(wsComponent.getQualifier()).isEqualTo(dbComponent.qualifier()); | |||
assertThat(wsComponent.getName()).isEqualTo(dbComponent.name()); | |||
assertThat(wsComponent.getDescription()).isEqualTo(dbComponent.description()); | |||
assertThat(wsComponent.getProjectId()).isEqualTo(""); | |||
assertThat(wsComponent.getLanguage()).isEqualTo(""); | |||
assertThat(wsComponent.getPath()).isEqualTo(""); | |||
assertThat(wsComponent.getRefId()).isEqualTo(""); | |||
assertThat(wsComponent.getRefKey()).isEqualTo(""); | |||
} | |||
@Test | |||
public void search_by_component_uuid() { | |||
ComponentDto project = componentDb.insertProject(); | |||
insertComplexityMetric(); | |||
SearchWsResponse result = callByComponentUuids(singletonList(project.uuid()), singletonList("complexity")); | |||
assertThat(result.getComponentsCount()).isEqualTo(1); | |||
assertThat(result.getComponents(0).getId()).isEqualTo(project.uuid()); | |||
} | |||
@Test | |||
@@ -151,11 +152,14 @@ public class SearchActionTest { | |||
SearchWsResponse result = callByComponentUuids(newArrayList(directoryDto.uuid(), file.uuid()), newArrayList("ncloc", "coverage", "new_violations")); | |||
// directory is not eligible for best value | |||
assertThat(result.getComponentsList().get(0).getMeasuresList()).extracting("metric").containsOnly("coverage"); | |||
assertThat(result.getMeasuresList().stream() | |||
.filter(measure -> directoryDto.uuid().equals(measure.getComponent())) | |||
.map(Measure::getMetric)) | |||
.containsOnly("coverage"); | |||
// file measures | |||
List<WsMeasures.Measure> fileMeasures = result.getComponentsList().get(1).getMeasuresList(); | |||
assertThat(fileMeasures).extracting("metric").containsOnly("ncloc", "coverage", "new_violations"); | |||
assertThat(fileMeasures).extracting("value").containsOnly("100", "15.5", ""); | |||
List<Measure> fileMeasures = result.getMeasuresList().stream().filter(measure -> file.uuid().equals(measure.getComponent())).collect(Collectors.toList()); | |||
assertThat(fileMeasures).extracting(Measure::getMetric).containsOnly("ncloc", "coverage", "new_violations"); | |||
assertThat(fileMeasures).extracting(Measure::getValue).containsOnly("100", "15.5", ""); | |||
} | |||
@Test |
@@ -44,16 +44,23 @@ message ComponentWsResponse { | |||
// WS api/measures/search | |||
message SearchWsResponse { | |||
repeated Component components = 1; | |||
repeated Measure measures = 1; | |||
repeated Component components = 2; | |||
message Component { | |||
optional string id = 1; | |||
optional string key = 2; | |||
optional string name = 3; | |||
} | |||
} | |||
message Component { | |||
optional string id = 1; | |||
optional string key = 2; | |||
optional string refId = 3; | |||
optional string refKey = 4; | |||
optional string projectId = 5; | |||
optional string name = 6; | |||
optional string name = 3; | |||
optional string refId = 4; | |||
optional string refKey = 5; | |||
optional string projectId = 6; | |||
optional string description = 7; | |||
optional string qualifier = 8; | |||
optional string path = 9; | |||
@@ -78,8 +85,9 @@ message Metrics { | |||
message Measure { | |||
optional string metric = 1; | |||
optional string value = 2; | |||
optional PeriodsValue periods = 3; | |||
optional string component = 2; | |||
optional string value = 3; | |||
optional PeriodsValue periods = 4; | |||
} | |||
message PeriodsValue { |