import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_METRIC_KEYS;
import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_METRIC_PERIOD_SORT;
import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_METRIC_SORT;
+import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_METRIC_SORT_FILTER;
import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_QUALIFIERS;
import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_STRATEGY;
public class ComponentTreeAction implements MeasuresWsAction {
private static final int MAX_SIZE = 500;
private static final int QUERY_MINIMUM_LENGTH = 3;
+ // tree exploration strategies
static final String ALL_STRATEGY = "all";
static final String CHILDREN_STRATEGY = "children";
static final String LEAVES_STRATEGY = "leaves";
static final Set<String> STRATEGIES = ImmutableSortedSet.of(ALL_STRATEGY, CHILDREN_STRATEGY, LEAVES_STRATEGY);
+ // sort
static final String NAME_SORT = "name";
static final String PATH_SORT = "path";
static final String QUALIFIER_SORT = "qualifier";
static final String METRIC_SORT = "metric";
static final String METRIC_PERIOD_SORT = "metricPeriod";
static final Set<String> SORTS = ImmutableSortedSet.of(NAME_SORT, PATH_SORT, QUALIFIER_SORT, METRIC_SORT, METRIC_PERIOD_SORT);
+ static final String ALL_METRIC_SORT_FILTER = "all";
+ static final String WITH_MEASURES_ONLY_METRIC_SORT_FILTER = "withMeasuresOnly";
+ static final Set<String> METRIC_SORT_FILTERS = ImmutableSortedSet.of(ALL_METRIC_SORT_FILTER, WITH_MEASURES_ONLY_METRIC_SORT_FILTER);
private final ComponentTreeDataLoader dataLoader;
private final UserSession userSession;
.setSince("5.5")
.setPossibleValues(1, 2, 3, 4, 5);
+ action.createParam(PARAM_METRIC_SORT_FILTER)
+ .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))
+ .setDefaultValue(ALL_METRIC_SORT_FILTER)
+ .setPossibleValues(METRIC_SORT_FILTERS);
+
createMetricKeysParameter(action);
createAdditionalFieldsParameter(action);
createDeveloperParameters(action);
.setSort(request.paramAsStrings(Param.SORT))
.setAsc(request.paramAsBoolean(Param.ASCENDING))
.setMetricSort(request.param(PARAM_METRIC_SORT))
+ .setMetricSortFilter(request.mandatoryParam(PARAM_METRIC_SORT_FILTER))
.setMetricPeriodSort(request.paramAsInt(PARAM_METRIC_PERIOD_SORT))
.setDeveloperId(request.param(PARAM_DEVELOPER_ID))
.setDeveloperKey(request.param(PARAM_DEVELOPER_KEY))
"To sort by the '%s' metric, it must be in the list of metric keys in the '%s' parameter", metricSortValue, PARAM_METRIC_KEYS);
checkRequest(componentTreeWsRequest.getMetricPeriodSort() == null ^ componentTreeWsRequest.getSort().contains(METRIC_PERIOD_SORT),
"To sort by a metric period, the '%s' parameter must contain '%s' and the '%s' must be provided.", Param.SORT, METRIC_PERIOD_SORT, PARAM_METRIC_PERIOD_SORT);
+ checkRequest(ALL_METRIC_SORT_FILTER.equals(componentTreeWsRequest.getMetricSortFilter()) || metricSortValue != null,
+ "To filter components based on the sort metric, the '%s' parameter must contain '%s' or '%s' and the '%s' parameter must be provided",
+ Param.SORT, METRIC_SORT, METRIC_PERIOD_SORT, PARAM_METRIC_SORT);
return componentTreeWsRequest;
}
}
import com.google.common.base.Function;
import com.google.common.base.Joiner;
+import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.HashBasedTable;
import org.sonarqube.ws.client.measure.ComponentTreeWsRequest;
import static com.google.common.base.Objects.firstNonNull;
+import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.FluentIterable.from;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Sets.newHashSet;
import static org.sonar.server.measure.ws.ComponentTreeAction.METRIC_PERIOD_SORT;
import static org.sonar.server.measure.ws.ComponentTreeAction.METRIC_SORT;
import static org.sonar.server.measure.ws.ComponentTreeAction.NAME_SORT;
+import static org.sonar.server.measure.ws.ComponentTreeAction.WITH_MEASURES_ONLY_METRIC_SORT_FILTER;
import static org.sonar.server.measure.ws.SnapshotDtoToWsPeriods.snapshotToWsPeriods;
import static org.sonar.server.user.AbstractUserSession.insufficientPrivilegesException;
Table<String, MetricDto, MeasureDto> measuresByComponentUuidAndMetric = searchMeasuresByComponentUuidAndMetric(dbSession, baseComponent, baseSnapshot, components, metrics,
periods, developerId);
+ components = filterComponents(components, measuresByComponentUuidAndMetric, metrics, wsRequest);
components = sortComponents(components, wsRequest, metrics, measuresByComponentUuidAndMetric);
components = paginateComponents(components, componentCount, wsRequest);
Map<Long, ComponentDto> referenceComponentsById = searchReferenceComponentsById(dbSession, components);
/**
* Conditions for best value measure:
* <ul>
- * <li>component is a production file or test file</li>
- * <li>metric is optimized for best value</li>
+ * <li>component is a production file or test file</li>
+ * <li>metric is optimized for best value</li>
* </ul>
*/
private static void addBestValuesToMeasures(Table<String, MetricDto, MeasureDto> measuresByComponentUuidAndMetric, List<ComponentDtoWithSnapshotId> components,
}
}
+ private static List<ComponentDtoWithSnapshotId> filterComponents(List<ComponentDtoWithSnapshotId> components,
+ Table<String, MetricDto, MeasureDto> measuresByComponentUuidAndMetric,
+ List<MetricDto> metrics, ComponentTreeWsRequest wsRequest) {
+ if (!WITH_MEASURES_ONLY_METRIC_SORT_FILTER.equals(wsRequest.getMetricSortFilter())) {
+ return components;
+ }
+
+ final String metricKeyToSort = wsRequest.getMetricSort();
+ Optional<MetricDto> metricToSort = from(metrics).firstMatch(new MatchMetricKey(metricKeyToSort));
+ checkState(metricToSort.isPresent(), "Metric '%s' not found", metricKeyToSort, wsRequest.getMetricKeys());
+
+ return from(components)
+ .filter(new HasMeasure(measuresByComponentUuidAndMetric, metricToSort.get()))
+ .toList();
+ }
+
private static List<ComponentDtoWithSnapshotId> sortComponents(List<ComponentDtoWithSnapshotId> components, ComponentTreeWsRequest wsRequest, List<MetricDto> metrics,
Table<String, MetricDto, MeasureDto> measuresByComponentUuidAndMetric) {
if (!isSortByMetric(wsRequest)) {
}
private static boolean isSortByMetric(ComponentTreeWsRequest wsRequest) {
+ requireNonNull(wsRequest.getSort());
return wsRequest.getSort().contains(METRIC_SORT) || wsRequest.getSort().contains(METRIC_PERIOD_SORT);
}
private enum ComponentDtoWithSnapshotIdToCopyResourceIdFunction implements Function<ComponentDtoWithSnapshotId, Long> {
INSTANCE;
+
@Override
public Long apply(@Nonnull ComponentDtoWithSnapshotId input) {
return input.getCopyResourceId();
}
}
+
+ private static class HasMeasure implements Predicate<ComponentDtoWithSnapshotId> {
+ private final Table<String, MetricDto, MeasureDto> measuresByComponentUuidAndMetric;
+ private final MetricDto metric;
+
+ private HasMeasure(Table<String, MetricDto, MeasureDto> measuresByComponentUuidAndMetric, MetricDto metric) {
+ this.measuresByComponentUuidAndMetric = measuresByComponentUuidAndMetric;
+ this.metric = metric;
+ }
+
+ @Override
+ public boolean apply(@Nonnull ComponentDtoWithSnapshotId input) {
+ return measuresByComponentUuidAndMetric.contains(input.uuid(), metric);
+ }
+ }
+
+ private static class MatchMetricKey implements Predicate<MetricDto> {
+ private final String metricKeyToSort;
+
+ private MatchMetricKey(String metricKeyToSort) {
+ this.metricKeyToSort = metricKeyToSort;
+ }
+
+ @Override
+ public boolean apply(@Nonnull MetricDto input) {
+ return input.getKey().equals(metricKeyToSort);
+ }
+ }
}
import static org.sonar.server.measure.ws.ComponentTreeAction.METRIC_PERIOD_SORT;
import static org.sonar.server.measure.ws.ComponentTreeAction.METRIC_SORT;
import static org.sonar.server.measure.ws.ComponentTreeAction.NAME_SORT;
+import static org.sonar.server.measure.ws.ComponentTreeAction.WITH_MEASURES_ONLY_METRIC_SORT_FILTER;
import static org.sonar.test.JsonAssert.assertJson;
import static org.sonarqube.ws.client.measure.MeasuresWsParameters.ADDITIONAL_PERIODS;
import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_ADDITIONAL_FIELDS;
import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_METRIC_KEYS;
import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_METRIC_PERIOD_SORT;
import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_METRIC_SORT;
+import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_METRIC_SORT_FILTER;
import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_QUALIFIERS;
import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_STRATEGY;
public void sort_by_metric_value() {
ComponentDto projectDto = newProjectDto("project-uuid");
SnapshotDto projectSnapshot = componentDb.insertProjectAndSnapshot(projectDto);
+ componentDb.insertComponentAndSnapshot(newFileDto(projectDto, "file-uuid-4"), projectSnapshot);
SnapshotDto fileSnapshot3 = componentDb.insertComponentAndSnapshot(newFileDto(projectDto, "file-uuid-3"), projectSnapshot);
SnapshotDto fileSnapshot1 = componentDb.insertComponentAndSnapshot(newFileDto(projectDto, "file-uuid-1"), projectSnapshot);
SnapshotDto fileSnapshot2 = componentDb.insertComponentAndSnapshot(newFileDto(projectDto, "file-uuid-2"), projectSnapshot);
.setParam(PARAM_METRIC_SORT, "ncloc")
.setParam(PARAM_METRIC_KEYS, "ncloc"));
- assertThat(response.getComponentsList()).extracting("id").containsExactly("file-uuid-1", "file-uuid-2", "file-uuid-3");
+ assertThat(response.getComponentsList()).extracting("id").containsExactly("file-uuid-1", "file-uuid-2", "file-uuid-3", "file-uuid-4");
+ }
+
+ @Test
+ public void remove_components_without_measure_on_the_metric_sort() {
+ ComponentDto projectDto = newProjectDto("project-uuid");
+ SnapshotDto projectSnapshot = componentDb.insertProjectAndSnapshot(projectDto);
+ componentDb.insertComponentAndSnapshot(newFileDto(projectDto, "file-uuid-4"), projectSnapshot);
+ SnapshotDto fileSnapshot3 = componentDb.insertComponentAndSnapshot(newFileDto(projectDto, "file-uuid-3"), projectSnapshot);
+ SnapshotDto fileSnapshot1 = componentDb.insertComponentAndSnapshot(newFileDto(projectDto, "file-uuid-1"), projectSnapshot);
+ SnapshotDto fileSnapshot2 = componentDb.insertComponentAndSnapshot(newFileDto(projectDto, "file-uuid-2"), projectSnapshot);
+ MetricDto ncloc = newMetricDtoWithoutOptimization().setKey("ncloc").setValueType(ValueType.INT.name()).setDirection(1);
+ dbClient.metricDao().insert(dbSession, ncloc);
+ dbClient.measureDao().insert(dbSession,
+ newMeasureDto(ncloc, fileSnapshot1.getId()).setValue(1.0d),
+ newMeasureDto(ncloc, fileSnapshot2.getId()).setValue(2.0d),
+ newMeasureDto(ncloc, fileSnapshot3.getId()).setValue(3.0d));
+ db.commit();
+
+ ComponentTreeWsResponse response = call(ws.newRequest()
+ .setParam(PARAM_BASE_COMPONENT_ID, "project-uuid")
+ .setParam(Param.SORT, METRIC_SORT)
+ .setParam(PARAM_METRIC_SORT, "ncloc")
+ .setParam(PARAM_METRIC_KEYS, "ncloc")
+ .setParam(PARAM_METRIC_SORT_FILTER, WITH_MEASURES_ONLY_METRIC_SORT_FILTER));
+
+ assertThat(response.getComponentsList()).extracting("id")
+ .containsExactly("file-uuid-1", "file-uuid-2", "file-uuid-3")
+ .doesNotContain("file-uuid-4");
}
@Test
ComponentTreeWsResponse result = call(ws.newRequest()
.setParam(PARAM_BASE_COMPONENT_ID, projectUuid)
.setParam(PARAM_STRATEGY, LEAVES_STRATEGY)
- .setParam(PARAM_METRIC_KEYS, "ncloc")
- );
+ .setParam(PARAM_METRIC_KEYS, "ncloc"));
assertThat(result.getBaseComponent().getId()).isEqualTo(projectUuid);
assertThat(result.getComponentsCount()).isEqualTo(0);
.setParam(Param.PAGE_SIZE, "2540"));
}
+ @Test
+ public void fail_when_with_measures_only_and_no_metric_sort() {
+ componentDb.insertProjectAndSnapshot(newProjectDto("project-uuid"));
+ insertNclocMetric();
+ expectedException.expect(BadRequestException.class);
+ expectedException
+ .expectMessage("To filter components based on the sort metric, the 's' parameter must contain 'metric' or 'metricPeriod' and the 'metricSort' parameter must be provided");
+
+ call(ws.newRequest()
+ .setParam(PARAM_BASE_COMPONENT_ID, "project-uuid")
+ .setParam(PARAM_METRIC_KEYS, "ncloc")
+ .setParam(PARAM_METRIC_SORT_FILTER, WITH_MEASURES_ONLY_METRIC_SORT_FILTER));
+ }
+
private static ComponentTreeWsResponse call(TestRequest request) {
TestResponse testResponse = request
.setMediaType(MediaTypes.PROTOBUF)
private Boolean asc;
private String metricSort;
private Integer metricPeriodSort;
+ private String metricSortFilter;
private List<String> metricKeys;
private Integer page;
private Integer pageSize;
return this;
}
+ @CheckForNull
+ public String getMetricSortFilter() {
+ return metricSortFilter;
+ }
+
+ public ComponentTreeWsRequest setMetricSortFilter(@Nullable String metricSortFilter) {
+ this.metricSortFilter = metricSortFilter;
+ return this;
+ }
+
@CheckForNull
public List<String> getMetricKeys() {
return metricKeys;
import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_DEVELOPER_KEY;
import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_METRIC_KEYS;
import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_METRIC_SORT;
+import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_METRIC_SORT_FILTER;
import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_QUALIFIERS;
import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_STRATEGY;
.setParam("ps", request.getPageSize())
.setParam("s", inlineMultipleParamValue(request.getSort()))
.setParam("asc", request.getAsc())
- .setParam(PARAM_METRIC_SORT, request.getMetricSort());
+ .setParam(PARAM_METRIC_SORT, request.getMetricSort())
+ .setParam(PARAM_METRIC_SORT_FILTER, request.getMetricSortFilter());
return call(getRequest, ComponentTreeWsResponse.parser());
}
public static final String PARAM_METRIC_KEYS = "metricKeys";
public static final String PARAM_METRIC_SORT = "metricSort";
public static final String PARAM_METRIC_PERIOD_SORT = "metricPeriodSort";
+ public static final String PARAM_METRIC_SORT_FILTER = "metricSortFilter";
public static final String PARAM_ADDITIONAL_FIELDS = "additionalFields";
public static final String PARAM_COMPONENT_ID = "componentId";
public static final String PARAM_COMPONENT_KEY = "componentKey";
import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_DEVELOPER_KEY;
import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_METRIC_KEYS;
import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_METRIC_SORT;
+import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_METRIC_SORT_FILTER;
import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_QUALIFIERS;
import static org.sonarqube.ws.client.measure.MeasuresWsParameters.PARAM_STRATEGY;
private static final List<String> VALUE_SORT = newArrayList("qualifier", "metric");
private static final boolean VALUE_ASC = false;
private static final String VALUE_METRIC_SORT = "ncloc";
+ private static final String VALUE_METRIC_SORT_FILTER = "all";
private static final int VALUE_PAGE = 42;
private static final int VALUE_PAGE_SIZE = 1984;
private static final String VALUE_QUERY = "query-sq";
.setPageSize(VALUE_PAGE_SIZE)
.setQuery(VALUE_QUERY)
.setDeveloperId(VALUE_DEVELOPER_ID)
- .setDeveloperKey(VALUE_DEVELOPER_KEY);
+ .setDeveloperKey(VALUE_DEVELOPER_KEY)
+ .setMetricSortFilter(VALUE_METRIC_SORT_FILTER);
underTest.componentTree(componentTreeRequest);
GetRequest getRequest = serviceTester.getGetRequest();
.hasParam("q", VALUE_QUERY)
.hasParam(PARAM_DEVELOPER_ID, VALUE_DEVELOPER_ID)
.hasParam(PARAM_DEVELOPER_KEY, VALUE_DEVELOPER_KEY)
+ .hasParam(PARAM_METRIC_SORT_FILTER, VALUE_METRIC_SORT_FILTER)
.andNoOtherParam();
}
}