/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube 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.
*
* SonarQube 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.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multiset;
import com.google.common.io.Resources;
import org.apache.commons.lang.StringUtils;
import org.sonar.api.i18n.I18n;
import org.sonar.api.measures.CoreMetrics;
import org.sonar.api.measures.Metric;
import org.sonar.api.server.ws.Request;
import org.sonar.api.server.ws.RequestHandler;
import org.sonar.api.server.ws.Response;
import org.sonar.api.server.ws.WebService;
import org.sonar.api.utils.DateUtils;
import org.sonar.api.utils.Durations;
import org.sonar.api.utils.text.JsonWriter;
import org.sonar.api.web.NavigationSection;
import org.sonar.api.web.Page;
import org.sonar.api.web.UserRole;
import org.sonar.core.component.ComponentDto;
import org.sonar.core.component.SnapshotDto;
import org.sonar.core.measure.db.MeasureDto;
import org.sonar.core.persistence.DbSession;
import org.sonar.core.persistence.MyBatis;
import org.sonar.core.properties.PropertyDto;
import org.sonar.core.properties.PropertyQuery;
import org.sonar.core.timemachine.Periods;
import org.sonar.server.db.DbClient;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.issue.IssueService;
import org.sonar.server.issue.RulesAggregation;
import org.sonar.server.rule.Rule;
import org.sonar.server.rule.RuleService;
import org.sonar.server.rule.index.RuleDoc;
import org.sonar.server.rule.index.RuleQuery;
import org.sonar.server.search.QueryContext;
import org.sonar.server.search.Result;
import org.sonar.server.ui.ViewProxy;
import org.sonar.server.ui.Views;
import org.sonar.server.user.UserSession;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import java.util.Date;
import java.util.List;
import java.util.Map;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Maps.newHashMap;
public class ComponentAppAction implements RequestHandler {
private static final String PARAM_KEY = "key";
private static final String PARAM_PERIOD = "period";
private final DbClient dbClient;
private final IssueService issueService;
private final Views views;
private final RuleService ruleService;
private final Periods periods;
private final Durations durations;
private final I18n i18n;
public ComponentAppAction(DbClient dbClient, IssueService issueService, Views views, RuleService ruleService, Periods periods, Durations durations, I18n i18n) {
this.dbClient = dbClient;
this.issueService = issueService;
this.views = views;
this.ruleService = ruleService;
this.periods = periods;
this.durations = durations;
this.i18n = i18n;
}
void define(WebService.NewController controller) {
WebService.NewAction action = controller.createAction("app")
.setDescription("Coverage data required for rendering the component viewer")
.setSince("4.4")
.setInternal(true)
.setHandler(this)
.setResponseExample(Resources.getResource(this.getClass(), "components-example-app.json"));
action
.createParam(PARAM_KEY)
.setRequired(true)
.setDescription("File key")
.setExampleValue("org.codehaus.sonar:sonar-plugin-api:src/main/java/org/sonar/api/Plugin.java");
action
.createParam(PARAM_PERIOD)
.setDescription("Period index in order to get differential measures")
.setPossibleValues(1, 2, 3, 4, 5);
}
@Override
public void handle(Request request, Response response) {
String fileKey = request.mandatoryParam(PARAM_KEY);
UserSession userSession = UserSession.get();
JsonWriter json = response.newJsonWriter();
json.beginObject();
DbSession session = dbClient.openSession(false);
try {
ComponentDto component = dbClient.componentDao().getNullableByKey(session, fileKey);
if (component == null) {
throw new NotFoundException(String.format("Component '%s' does not exist", fileKey));
}
userSession.checkComponentPermission(UserRole.USER, fileKey);
List<Period> periodList = periods(component.projectUuid(), session);
Integer periodIndex = request.paramAsInt(PARAM_PERIOD);
Date periodDate = periodDate(periodIndex, periodList);
RulesAggregation rulesAggregation = issueService.findRulesByComponent(component.key(), periodDate, session);
Multiset<String> severitiesAggregation = issueService.findSeveritiesByComponent(component.key(), periodDate, session);
Map<String, MeasureDto> measuresByMetricKey = measuresByMetricKey(component, session);
appendComponent(json, component, userSession, session);
appendPermissions(json, component, userSession);
appendPeriods(json, periodList);
appendIssuesAggregation(json, rulesAggregation, severitiesAggregation);
appendMeasures(json, measuresByMetricKey, severitiesAggregation, periodIndex);
appendTabs(json, measuresByMetricKey);
appendExtensions(json, component, userSession);
appendManualRules(json);
} finally {
MyBatis.closeQuietly(session);
}
json.endObject();
json.close();
}
private void appendComponent(JsonWriter json, ComponentDto component, UserSession userSession, DbSession session) {
List<PropertyDto> propertyDtos = dbClient.propertiesDao().selectByQuery(PropertyQuery.builder()
.setKey("favourite")
.setComponentId(component.getId())
.setUserId(userSession.userId())
.build(),
session
);
boolean isFavourite = propertyDtos.size() == 1;
json.prop("key", component.key());
json.prop("path", component.path());
json.prop("name", component.name());
json.prop("longName", component.longName());
json.prop("q", component.qualifier());
ComponentDto subProject = nullableComponentById(component.subProjectId(), session);
ComponentDto project = dbClient.componentDao().getByUuid(session, component.projectUuid());
// Do not display sub project if sub project and project are the same
boolean displaySubProject = subProject != null && !subProject.getId().equals(project.getId());
json.prop("subProject", displaySubProject ? subProject.key() : null);
json.prop("subProjectName", displaySubProject ? subProject.longName() : null);
json.prop("project", project.key());
json.prop("projectName", project.longName());
json.prop("fav", isFavourite);
}
private void appendTabs(JsonWriter json, Map<String, MeasureDto> measuresByMetricKey) {
List<String> tabs = newArrayList();
if (measuresByMetricKey.get(CoreMetrics.SCM_AUTHORS_BY_LINE_KEY) != null) {
tabs.add("scm");
}
if (hasCoverage(measuresByMetricKey)) {
tabs.add("coverage");
}
if (measuresByMetricKey.get(CoreMetrics.DUPLICATED_LINES_KEY) != null) {
tabs.add("duplications");
}
if (!tabs.isEmpty()) {
json.name("tabs").beginArray().values(tabs).endArray();
}
}
private boolean hasCoverage(Map<String, MeasureDto> measuresByMetricKey) {
return measuresByMetricKey.get(CoreMetrics.OVERALL_COVERAGE_KEY) != null
|| measuresByMetricKey.get(CoreMetrics.IT_COVERAGE_KEY) != null
|| measuresByMetricKey.get(CoreMetrics.COVERAGE_KEY) != null;
}
private void appendPermissions(JsonWriter json, ComponentDto component, UserSession userSession) {
boolean hasBrowsePermission = userSession.hasComponentPermission(UserRole.USER, component.key());
json.prop("canMarkAsFavourite", userSession.isLoggedIn() && hasBrowsePermission);
json.prop("canBulkChange", userSession.isLoggedIn());
json.prop("canCreateManualIssue", userSession.isLoggedIn() && hasBrowsePermission);
}
private void appendMeasures(JsonWriter json, Map<String, MeasureDto> measuresByMetricKey, Multiset<String> severitiesAggregation, Integer periodIndex) {
json.name("measures").beginObject();
json.prop("fNcloc", formatMeasureOrVariation(measuresByMetricKey.get(CoreMetrics.NCLOC_KEY), periodIndex));
json.prop("fCoverage", formatMeasureOrVariation(coverageMeasure(measuresByMetricKey), periodIndex));
json.prop("fDuplicationDensity", formatMeasureOrVariation(measuresByMetricKey.get(CoreMetrics.DUPLICATED_LINES_DENSITY_KEY), periodIndex));
json.prop("fDebt", formatMeasureOrVariation(measuresByMetricKey.get(CoreMetrics.TECHNICAL_DEBT_KEY), periodIndex));
json.prop("fSqaleRating", formatMeasureOrVariation(measuresByMetricKey.get(CoreMetrics.SQALE_RATING_KEY), periodIndex));
json.prop("fSqaleDebtRatio", formatMeasureOrVariation(measuresByMetricKey.get(CoreMetrics.SQALE_DEBT_RATIO_KEY), periodIndex));
json.prop("fTests", formatMeasureOrVariation(measuresByMetricKey.get(CoreMetrics.TESTS_KEY), periodIndex));
json.prop("fIssues", i18n.formatInteger(UserSession.get().locale(), severitiesAggregation.size()));
for (String severity : severitiesAggregation.elementSet()) {
json.prop("f" + StringUtils.capitalize(severity.toLowerCase()) + "Issues", i18n.formatInteger(UserSession.get().locale(), severitiesAggregation.count(severity)));
}
json.endObject();
}
private MeasureDto coverageMeasure(Map<String, MeasureDto> measuresByMetricKey) {
MeasureDto overallCoverage = measuresByMetricKey.get(CoreMetrics.OVERALL_COVERAGE_KEY);
MeasureDto itCoverage = measuresByMetricKey.get(CoreMetrics.IT_COVERAGE_KEY);
MeasureDto utCoverage = measuresByMetricKey.get(CoreMetrics.COVERAGE_KEY);
if (overallCoverage != null) {
return overallCoverage;
} else if (utCoverage != null) {
return utCoverage;
} else {
return itCoverage;
}
}
private void appendPeriods(JsonWriter json, List<Period> periodList) {
json.name("periods").beginArray();
for (Period period : periodList) {
Date periodDate = period.date();
json.beginArray()
.value(period.index())
.value(period.label())
.value(periodDate != null ? DateUtils.formatDateTime(periodDate) : null)
.endArray();
}
json.endArray();
}
private void appendIssuesAggregation(JsonWriter json, RulesAggregation rulesAggregation, Multiset<String> severitiesAggregation) {
json.name("severities").beginArray();
for (String severity : severitiesAggregation.elementSet()) {
json.beginArray()
.value(severity)
.value(i18n.message(UserSession.get().locale(), "severity." + severity, null))
.value(severitiesAggregation.count(severity))
.endArray();
}
json.endArray();
json.name("rules").beginArray();
for (RulesAggregation.Rule rule : rulesAggregation.rules()) {
json.beginArray()
.value(rule.ruleKey().toString())
.value(rule.name())
.value(rulesAggregation.countRule(rule))
.endArray();
}
json.endArray();
}
private void appendManualRules(JsonWriter json) {
Result<Rule> result = ruleService.search(new RuleQuery().setRepositories(newArrayList(RuleDoc.MANUAL_REPOSITORY)), new QueryContext().setMaxLimit());
if (result != null && !result.getHits().isEmpty()) {
List<Rule> manualRules = result.getHits();
json.name("manual_rules").beginArray();
for (Rule manualRule : manualRules) {
json.beginObject()
.prop("key", manualRule.key().toString())
.prop("name", manualRule.name())
.endObject();
}
json.endArray();
}
}
private void appendExtensions(JsonWriter json, ComponentDto component, UserSession userSession) {
List<ViewProxy<Page>> extensionPages = views.getPages(NavigationSection.RESOURCE_TAB, component.scope(), component.qualifier(), component.language(), null);
Map<String, String> extensions = extensions(extensionPages, component, userSession);
if (!extensions.isEmpty()) {
json.name("extensions").beginArray();
for (Map.Entry<String, String> entry : extensions.entrySet()) {
json.beginArray().value(entry.getKey()).value(entry.getValue()).endArray();
}
json.endArray();
}
}
private Map<String, String> extensions(List<ViewProxy<Page>> extensions, ComponentDto component, UserSession userSession) {
Map<String, String> result = newHashMap();
List<String> providedExtensions = newArrayList("tests_viewer", "coverage", "duplications", "issues", "source");
for (ViewProxy<Page> page : extensions) {
if (!providedExtensions.contains(page.getId())) {
addExtension(page, result, component, userSession);
}
}
return result;
}
private void addExtension(ViewProxy<Page> page, Map<String, String> result, ComponentDto component, UserSession userSession) {
if (page.getUserRoles().length == 0) {
result.put(page.getId(), page.getTitle());
} else {
for (String userRole : page.getUserRoles()) {
if (userSession.hasComponentPermission(userRole, component.key())) {
result.put(page.getId(), page.getTitle());
}
}
}
}
private List<Period> periods(String projectUuid, DbSession session) {
List<Period> periodList = newArrayList();
SnapshotDto snapshotDto = dbClient.resourceDao().getLastSnapshotByResourceUuid(projectUuid, session);
if (snapshotDto != null) {
for (int i = 1; i <= 5; i++) {
String mode = snapshotDto.getPeriodMode(i);
if (mode != null) {
Date periodDate = snapshotDto.getPeriodDate(i);
String label = periods.label(mode, snapshotDto.getPeriodModeParameter(i), periodDate);
if (label != null) {
periodList.add(new Period(i, label, periodDate));
}
}
}
}
return periodList;
}
private Map<String, MeasureDto> measuresByMetricKey(ComponentDto component, DbSession session) {
Map<String, MeasureDto> measuresByMetricKey = newHashMap();
String fileKey = component.getKey();
for (MeasureDto measureDto : dbClient.measureDao().findByComponentKeyAndMetricKeys(fileKey,
newArrayList(CoreMetrics.NCLOC_KEY, CoreMetrics.COVERAGE_KEY, CoreMetrics.IT_COVERAGE_KEY, CoreMetrics.OVERALL_COVERAGE_KEY,
CoreMetrics.DUPLICATED_LINES_KEY, CoreMetrics.DUPLICATED_LINES_DENSITY_KEY, CoreMetrics.TECHNICAL_DEBT_KEY, CoreMetrics.TESTS_KEY,
CoreMetrics.SCM_AUTHORS_BY_LINE_KEY, CoreMetrics.SQALE_RATING_KEY, CoreMetrics.SQALE_DEBT_RATIO_KEY),
session)) {
measuresByMetricKey.put(measureDto.getKey().metricKey(), measureDto);
}
return measuresByMetricKey;
}
@CheckForNull
private Date periodDate(@Nullable final Integer periodIndex, List<Period> periodList) {
if (periodIndex != null) {
Period period = Iterables.find(periodList, new Predicate<Period>() {
@Override
public boolean apply(@Nullable Period input) {
return input != null && periodIndex.equals(input.index());
}
}, null);
return period != null ? period.date() : null;
}
return null;
}
@CheckForNull
private ComponentDto nullableComponentById(@Nullable Long componentId, DbSession session) {
if (componentId != null) {
return dbClient.componentDao().getById(componentId, session);
}
return null;
}
@CheckForNull
private String formatMeasureOrVariation(@Nullable MeasureDto measure, @Nullable Integer periodIndex) {
if (periodIndex == null) {
return formatMeasure(measure);
} else {
return formatVariation(measure, periodIndex);
}
}
@CheckForNull
private String formatMeasure(@Nullable MeasureDto measure) {
if (measure != null) {
Metric metric = CoreMetrics.getMetric(measure.getKey().metricKey());
Metric.ValueType metricType = metric.getType();
Double value = measure.getValue();
String data = measure.getData();
if (metricType.equals(Metric.ValueType.FLOAT) && value != null) {
return i18n.formatDouble(UserSession.get().locale(), value);
}
if (metricType.equals(Metric.ValueType.INT) && value != null) {
return i18n.formatInteger(UserSession.get().locale(), value.intValue());
}
if (metricType.equals(Metric.ValueType.PERCENT) && value != null) {
return i18n.formatDouble(UserSession.get().locale(), value) + "%";
}
if (metricType.equals(Metric.ValueType.WORK_DUR) && value != null) {
return durations.format(UserSession.get().locale(), durations.create(value.longValue()), Durations.DurationFormat.SHORT);
}
if ((metricType.equals(Metric.ValueType.STRING) || metricType.equals(Metric.ValueType.RATING)) && data != null) {
return data;
}
}
return null;
}
@CheckForNull
private String formatVariation(@Nullable MeasureDto measure, Integer periodIndex) {
if (measure != null) {
Double variation = measure.getVariation(periodIndex);
if (variation != null) {
Metric metric = CoreMetrics.getMetric(measure.getKey().metricKey());
Metric.ValueType metricType = metric.getType();
if (metricType.equals(Metric.ValueType.FLOAT) || metricType.equals(Metric.ValueType.PERCENT)) {
return i18n.formatDouble(UserSession.get().locale(), variation);
}
if (metricType.equals(Metric.ValueType.INT)) {
return i18n.formatInteger(UserSession.get().locale(), variation.intValue());
}
if (metricType.equals(Metric.ValueType.WORK_DUR)) {
return durations.format(UserSession.get().locale(), durations.create(variation.longValue()), Durations.DurationFormat.SHORT);
}
}
}
return null;
}
protected static class Period {
Integer index;
String label;
Date date;
protected Period(Integer index, String label, @Nullable Date date) {
this.index = index;
this.label = label;
this.date = date;
}
public Integer index() {
return index;
}
public String label() {
return label;
}
@CheckForNull
public Date date() {
return date;
}
}
}