/*
* SonarQube
* Copyright (C) 2009-2017 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.qualitygate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang.ObjectUtils;
import org.sonar.api.measures.CoreMetrics;
import org.sonar.api.measures.Metric.ValueType;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.metric.MetricDto;
import org.sonar.db.qualitygate.QualityGateConditionDto;
import org.sonar.db.qualitygate.QualityGateDto;
import org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating;
import org.sonar.server.exceptions.NotFoundException;
import static com.google.common.base.Strings.isNullOrEmpty;
import static java.lang.Integer.parseInt;
import static java.lang.String.format;
import static java.util.Arrays.stream;
import static org.sonar.api.measures.Metric.ValueType.RATING;
import static org.sonar.api.measures.Metric.ValueType.valueOf;
import static org.sonar.db.qualitygate.QualityGateConditionDto.isOperatorAllowed;
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.E;
import static org.sonar.server.qualitygate.ValidRatingMetrics.isCoreRatingMetric;
import static org.sonar.server.ws.WsUtils.checkRequest;
public class QualityGateConditionsUpdater {
private static final List<String> RATING_VALID_INT_VALUES = stream(Rating.values()).map(r -> Integer.toString(r.getIndex())).collect(Collectors.toList());
private final DbClient dbClient;
public QualityGateConditionsUpdater(DbClient dbClient) {
this.dbClient = dbClient;
}
public QualityGateConditionDto createCondition(DbSession dbSession, long qGateId, String metricKey, String operator,
@Nullable String warningThreshold, @Nullable String errorThreshold, @Nullable Integer period) {
getNonNullQgate(dbSession, qGateId);
MetricDto metric = getNonNullMetric(dbSession, metricKey);
validateCondition(metric, operator, warningThreshold, errorThreshold, period);
checkConditionDoesNotAlreadyExistOnSameMetricAndPeriod(getConditions(dbSession, qGateId, null), metric, period);
QualityGateConditionDto newCondition = new QualityGateConditionDto().setQualityGateId(qGateId)
.setMetricId(metric.getId()).setMetricKey(metric.getKey())
.setOperator(operator)
.setWarningThreshold(warningThreshold)
.setErrorThreshold(errorThreshold)
.setPeriod(period);
dbClient.gateConditionDao().insert(newCondition, dbSession);
return newCondition;
}
public QualityGateConditionDto updateCondition(DbSession dbSession, long condId, String metricKey, String operator,
@Nullable String warningThreshold, @Nullable String errorThreshold, @Nullable Integer period) {
QualityGateConditionDto condition = getNonNullCondition(dbSession, condId);
MetricDto metric = getNonNullMetric(dbSession, metricKey);
validateCondition(metric, operator, warningThreshold, errorThreshold, period);
checkConditionDoesNotAlreadyExistOnSameMetricAndPeriod(getConditions(dbSession, condition.getQualityGateId(), condition.getId()), metric, period);
condition
.setMetricId(metric.getId())
.setMetricKey(metric.getKey())
.setOperator(operator)
.setWarningThreshold(warningThreshold)
.setErrorThreshold(errorThreshold)
.setPeriod(period);
dbClient.gateConditionDao().update(condition, dbSession);
return condition;
}
private QualityGateDto getNonNullQgate(DbSession dbSession, long id) {
QualityGateDto qGate = dbClient.qualityGateDao().selectById(dbSession, id);
if (qGate == null) {
throw new NotFoundException(format("There is no quality gate with id=%s", id));
}
return qGate;
}
private MetricDto getNonNullMetric(DbSession dbSession, String metricKey) {
MetricDto metric = dbClient.metricDao().selectByKey(dbSession, metricKey);
if (metric == null) {
throw new NotFoundException(format("There is no metric with key=%s", metricKey));
}
return metric;
}
private QualityGateConditionDto getNonNullCondition(DbSession dbSession, long id) {
QualityGateConditionDto condition = dbClient.gateConditionDao().selectById(id, dbSession);
if (condition == null) {
throw new NotFoundException("There is no condition with id=" + id);
}
return condition;
}
private Collection<QualityGateConditionDto> getConditions(DbSession dbSession, long qGateId, @Nullable Long conditionId) {
Collection<QualityGateConditionDto> conditions = dbClient.gateConditionDao().selectForQualityGate(dbSession, qGateId);
if (conditionId == null) {
return conditions;
}
return dbClient.gateConditionDao().selectForQualityGate(dbSession, qGateId).stream()
.filter(condition -> condition.getId() != conditionId)
.collect(Collectors.toList());
}
private static void validateCondition(MetricDto metric, String operator, @Nullable String warningThreshold, @Nullable String errorThreshold, @Nullable Integer period) {
List<String> errors = new ArrayList<>();
validateMetric(metric, errors);
checkOperator(metric, operator, errors);
checkThresholds(warningThreshold, errorThreshold, errors);
checkPeriod(metric, period, errors);
checkRatingMetric(metric, warningThreshold, errorThreshold, period, errors);
checkRequest(errors.isEmpty(), errors);
}
private static void validateMetric(MetricDto metric, List<String> errors) {
check(isAlertable(metric), errors, "Metric '%s' cannot be used to define a condition.", metric.getKey());
}
private static boolean isAlertable(MetricDto metric) {
return isAvailableForInit(metric) && BooleanUtils.isFalse(metric.isHidden());
}
private static boolean isAvailableForInit(MetricDto metric) {
return !metric.isDataType() && !CoreMetrics.ALERT_STATUS_KEY.equals(metric.getKey());
}
private static void checkOperator(MetricDto metric, String operator, List<String> errors) {
ValueType valueType = valueOf(metric.getValueType());
check(isOperatorAllowed(operator, valueType), errors, "Operator %s is not allowed for metric type %s.", operator, metric.getValueType());
}
private static void checkThresholds(@Nullable String warningThreshold, @Nullable String errorThreshold, List<String> errors) {
check(warningThreshold != null || errorThreshold != null, errors, "At least one threshold (warning, error) must be set.");
}
private static void checkPeriod(MetricDto metric, @Nullable Integer period, List<String> errors) {
if (period == null) {
check(!metric.getKey().startsWith("new_"), errors, "A period must be selected for differential metrics.");
} else {
check(period == 1, errors, "The only valid quality gate period is 1, the leak period.");
}
}
private static void checkConditionDoesNotAlreadyExistOnSameMetricAndPeriod(Collection<QualityGateConditionDto> conditions, MetricDto metric, @Nullable final Integer period) {
if (conditions.isEmpty()) {
return;
}
boolean conditionExists = conditions.stream().anyMatch(c -> c.getMetricId() == metric.getId() && ObjectUtils.equals(c.getPeriod(), period));
checkRequest(!conditionExists, period == null
? format("Condition on metric '%s' already exists.", metric.getShortName())
: format("Condition on metric '%s' over leak period already exists.", metric.getShortName()));
}
private static void checkRatingMetric(MetricDto metric, @Nullable String warningThreshold, @Nullable String errorThreshold, @Nullable Integer period, List<String> errors) {
if (!metric.getValueType().equals(RATING.name())) {
return;
}
if (!isCoreRatingMetric(metric.getKey())) {
errors.add(format("The metric '%s' cannot be used", metric.getShortName()));
}
if (period != null && !metric.getKey().startsWith("new_")) {
errors.add(format("The metric '%s' cannot be used on the leak period", metric.getShortName()));
}
if (!isValidRating(warningThreshold)) {
addInvalidRatingError(warningThreshold, errors);
return;
}
if (!isValidRating(errorThreshold)) {
addInvalidRatingError(errorThreshold, errors);
return;
}
checkRatingGreaterThanOperator(warningThreshold, errors);
checkRatingGreaterThanOperator(errorThreshold, errors);
}
private static void addInvalidRatingError(@Nullable String value, List<String> errors) {
errors.add(format("'%s' is not a valid rating", value));
}
private static void checkRatingGreaterThanOperator(@Nullable String value, List<String> errors) {
check(isNullOrEmpty(value) || !Objects.equals(toRating(value), E), errors, "There's no worse rating than E (%s)", value);
}
private static Rating toRating(String value) {
return Rating.valueOf(parseInt(value));
}
private static boolean isValidRating(@Nullable String value) {
return isNullOrEmpty(value) || RATING_VALID_INT_VALUES.contains(value);
}
private static boolean check(boolean expression, List<String> errors, String message, String... args) {
if (!expression) {
errors.add(format(message, args));
}
return expression;
}
}