]> source.dussan.org Git - sonarqube.git/blob
fcc2235c5d71124d631303eea698a041bf02b1e5
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2023 SonarSource SA
4  * mailto:info AT sonarsource DOT com
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU Lesser General Public
8  * License as published by the Free Software Foundation; either
9  * version 3 of the License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * Lesser General Public License for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public License
17  * along with this program; if not, write to the Free Software Foundation,
18  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19  */
20 package org.sonar.server.qualitygate;
21
22 import java.util.ArrayList;
23 import java.util.Collection;
24 import java.util.EnumSet;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.Objects;
28 import java.util.Set;
29 import java.util.stream.Collectors;
30 import javax.annotation.Nullable;
31 import org.sonar.api.measures.Metric.ValueType;
32 import org.sonar.core.util.Uuids;
33 import org.sonar.db.DbClient;
34 import org.sonar.db.DbSession;
35 import org.sonar.db.metric.MetricDto;
36 import org.sonar.db.qualitygate.QualityGateConditionDto;
37 import org.sonar.db.qualitygate.QualityGateDto;
38 import org.sonar.server.exceptions.NotFoundException;
39 import org.sonar.server.measure.Rating;
40
41 import static com.google.common.base.Strings.isNullOrEmpty;
42 import static java.lang.Double.parseDouble;
43 import static java.lang.Integer.parseInt;
44 import static java.lang.Long.parseLong;
45 import static java.lang.String.format;
46 import static java.util.Arrays.stream;
47 import static java.util.Objects.requireNonNull;
48 import static org.sonar.api.measures.CoreMetrics.ALERT_STATUS_KEY;
49 import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_HOTSPOTS_KEY;
50 import static org.sonar.api.measures.CoreMetrics.SECURITY_HOTSPOTS_KEY;
51 import static org.sonar.api.measures.Metric.DIRECTION_BETTER;
52 import static org.sonar.api.measures.Metric.DIRECTION_NONE;
53 import static org.sonar.api.measures.Metric.DIRECTION_WORST;
54 import static org.sonar.api.measures.Metric.ValueType.RATING;
55 import static org.sonar.server.exceptions.BadRequestException.checkRequest;
56 import static org.sonar.server.measure.Rating.E;
57 import static org.sonar.server.qualitygate.Condition.Operator.GREATER_THAN;
58 import static org.sonar.server.qualitygate.Condition.Operator.LESS_THAN;
59 import static org.sonar.server.qualitygate.ValidRatingMetrics.isCoreRatingMetric;
60
61 public class QualityGateConditionsUpdater {
62   public static final Set<String> INVALID_METRIC_KEYS = Set.of(ALERT_STATUS_KEY, SECURITY_HOTSPOTS_KEY, NEW_SECURITY_HOTSPOTS_KEY);
63
64   private static final Map<Integer, Set<Condition.Operator>> VALID_OPERATORS_BY_DIRECTION = Map.of(
65     DIRECTION_NONE, Set.of(GREATER_THAN, LESS_THAN),
66     DIRECTION_BETTER, Set.of(LESS_THAN),
67     DIRECTION_WORST, Set.of(GREATER_THAN));
68
69   private static final EnumSet<ValueType> VALID_METRIC_TYPES = EnumSet.of(
70     ValueType.INT,
71     ValueType.FLOAT,
72     ValueType.PERCENT,
73     ValueType.MILLISEC,
74     ValueType.LEVEL,
75     ValueType.RATING,
76     ValueType.WORK_DUR);
77
78   private static final List<String> RATING_VALID_INT_VALUES = stream(Rating.values()).map(r -> Integer.toString(r.getIndex())).collect(Collectors.toList());
79
80   private final DbClient dbClient;
81
82   public QualityGateConditionsUpdater(DbClient dbClient) {
83     this.dbClient = dbClient;
84   }
85
86   public QualityGateConditionDto createCondition(DbSession dbSession, QualityGateDto qualityGate, String metricKey, String operator,
87     String errorThreshold) {
88     MetricDto metric = getNonNullMetric(dbSession, metricKey);
89     validateCondition(metric, operator, errorThreshold);
90     checkConditionDoesNotExistOnSameMetric(getConditions(dbSession, qualityGate.getUuid()), metric);
91
92     QualityGateConditionDto newCondition = new QualityGateConditionDto().setQualityGateUuid(qualityGate.getUuid())
93       .setUuid(Uuids.create())
94       .setMetricUuid(metric.getUuid()).setMetricKey(metric.getKey())
95       .setOperator(operator)
96       .setErrorThreshold(errorThreshold);
97     dbClient.gateConditionDao().insert(newCondition, dbSession);
98     return newCondition;
99   }
100
101   public QualityGateConditionDto updateCondition(DbSession dbSession, QualityGateConditionDto condition, String metricKey, String operator,
102     String errorThreshold) {
103     MetricDto metric = getNonNullMetric(dbSession, metricKey);
104     validateCondition(metric, operator, errorThreshold);
105
106     condition
107       .setMetricUuid(metric.getUuid())
108       .setMetricKey(metric.getKey())
109       .setOperator(operator)
110       .setErrorThreshold(errorThreshold);
111     dbClient.gateConditionDao().update(condition, dbSession);
112     return condition;
113   }
114
115   private MetricDto getNonNullMetric(DbSession dbSession, String metricKey) {
116     MetricDto metric = dbClient.metricDao().selectByKey(dbSession, metricKey);
117     if (metric == null) {
118       throw new NotFoundException(format("There is no metric with key=%s", metricKey));
119     }
120     return metric;
121   }
122
123   private Collection<QualityGateConditionDto> getConditions(DbSession dbSession, String qGateUuid) {
124     return dbClient.gateConditionDao().selectForQualityGate(dbSession, qGateUuid);
125   }
126
127   private static void validateCondition(MetricDto metric, String operator, String errorThreshold) {
128     List<String> errors = new ArrayList<>();
129     validateMetric(metric, errors);
130     checkOperator(metric, operator, errors);
131     checkErrorThreshold(metric, errorThreshold, errors);
132     checkRatingMetric(metric, errorThreshold, errors);
133     checkRequest(errors.isEmpty(), errors);
134   }
135
136   private static void validateMetric(MetricDto metric, List<String> errors) {
137     check(isValid(metric), errors, "Metric '%s' cannot be used to define a condition.", metric.getKey());
138   }
139
140   private static boolean isValid(MetricDto metric) {
141     return !metric.isHidden()
142       && VALID_METRIC_TYPES.contains(ValueType.valueOf(metric.getValueType()))
143       && !INVALID_METRIC_KEYS.contains(metric.getKey());
144   }
145
146   private static void checkOperator(MetricDto metric, String operator, List<String> errors) {
147     check(
148       Condition.Operator.isValid(operator) && isAllowedOperator(operator, metric),
149       errors,
150       "Operator %s is not allowed for this metric.", operator);
151   }
152
153   private static void checkErrorThreshold(MetricDto metric, String errorThreshold, List<String> errors) {
154     requireNonNull(errorThreshold, "errorThreshold can not be null");
155     validateErrorThresholdValue(metric, errorThreshold, errors);
156   }
157
158   private static void checkConditionDoesNotExistOnSameMetric(Collection<QualityGateConditionDto> conditions, MetricDto metric) {
159     if (conditions.isEmpty()) {
160       return;
161     }
162
163     boolean conditionExists = conditions.stream().anyMatch(c -> c.getMetricUuid().equals(metric.getUuid()));
164     checkRequest(!conditionExists, format("Condition on metric '%s' already exists.", metric.getShortName()));
165   }
166
167   private static boolean isAllowedOperator(String operator, MetricDto metric) {
168     if (VALID_OPERATORS_BY_DIRECTION.containsKey(metric.getDirection())) {
169       return VALID_OPERATORS_BY_DIRECTION.get(metric.getDirection()).contains(Condition.Operator.fromDbValue(operator));
170     }
171
172     return false;
173   }
174
175   private static void validateErrorThresholdValue(MetricDto metric, String errorThreshold, List<String> errors) {
176     try {
177       ValueType valueType = ValueType.valueOf(metric.getValueType());
178       switch (valueType) {
179         case BOOL:
180         case INT:
181         case RATING:
182           parseInt(errorThreshold);
183           return;
184         case MILLISEC:
185         case WORK_DUR:
186           parseLong(errorThreshold);
187           return;
188         case FLOAT:
189         case PERCENT:
190           parseDouble(errorThreshold);
191           return;
192         case STRING:
193         case LEVEL:
194           return;
195         default:
196           throw new IllegalArgumentException(format("Unsupported value type %s. Cannot convert condition value", valueType));
197       }
198     } catch (Exception e) {
199       errors.add(format("Invalid value '%s' for metric '%s'", errorThreshold, metric.getShortName()));
200     }
201   }
202
203   private static void checkRatingMetric(MetricDto metric, String errorThreshold, List<String> errors) {
204     if (!metric.getValueType().equals(RATING.name())) {
205       return;
206     }
207     if (!isCoreRatingMetric(metric.getKey())) {
208       errors.add(format("The metric '%s' cannot be used", metric.getShortName()));
209     }
210     if (!isValidRating(errorThreshold)) {
211       addInvalidRatingError(errorThreshold, errors);
212       return;
213     }
214     checkRatingGreaterThanOperator(errorThreshold, errors);
215   }
216
217   private static void addInvalidRatingError(@Nullable String value, List<String> errors) {
218     errors.add(format("'%s' is not a valid rating", value));
219   }
220
221   private static void checkRatingGreaterThanOperator(@Nullable String value, List<String> errors) {
222     check(isNullOrEmpty(value) || !Objects.equals(toRating(value), E), errors, "There's no worse rating than E (%s)"  , value);
223   }
224
225   private static Rating toRating(String value) {
226     return Rating.valueOf(parseInt(value));
227   }
228
229   private static boolean isValidRating(@Nullable String value) {
230     return isNullOrEmpty(value) || RATING_VALID_INT_VALUES.contains(value);
231   }
232
233   private static boolean check(boolean expression, List<String> errors, String message, String... args) {
234     if (!expression) {
235       errors.add(format(message, args));
236     }
237     return expression;
238   }
239 }