]> source.dussan.org Git - sonarqube.git/blob
41490914c5b154ee97dcac7e9a1836a56ef5bf62
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2024 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.Optional;
29 import java.util.Set;
30 import java.util.stream.Collectors;
31 import java.util.stream.Stream;
32 import javax.annotation.Nullable;
33 import org.sonar.api.measures.Metric.ValueType;
34 import org.sonar.core.util.Uuids;
35 import org.sonar.db.DbClient;
36 import org.sonar.db.DbSession;
37 import org.sonar.db.metric.MetricDto;
38 import org.sonar.db.qualitygate.QualityGateConditionDto;
39 import org.sonar.db.qualitygate.QualityGateDto;
40 import org.sonar.server.exceptions.NotFoundException;
41 import org.sonar.server.measure.Rating;
42 import org.sonar.server.qualitygate.ws.StandardToMQRMetrics;
43
44 import static com.google.common.base.Strings.isNullOrEmpty;
45 import static java.lang.Double.parseDouble;
46 import static java.lang.Integer.parseInt;
47 import static java.lang.Long.parseLong;
48 import static java.lang.String.format;
49 import static java.util.Arrays.stream;
50 import static java.util.Objects.requireNonNull;
51 import static org.sonar.api.measures.CoreMetrics.ALERT_STATUS_KEY;
52 import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_HOTSPOTS_KEY;
53 import static org.sonar.api.measures.CoreMetrics.SECURITY_HOTSPOTS_KEY;
54 import static org.sonar.api.measures.Metric.DIRECTION_BETTER;
55 import static org.sonar.api.measures.Metric.DIRECTION_NONE;
56 import static org.sonar.api.measures.Metric.DIRECTION_WORST;
57 import static org.sonar.api.measures.Metric.ValueType.RATING;
58 import static org.sonar.server.exceptions.BadRequestException.checkRequest;
59 import static org.sonar.server.exceptions.BadRequestException.throwBadRequestException;
60 import static org.sonar.server.measure.Rating.E;
61 import static org.sonar.server.qualitygate.Condition.Operator.GREATER_THAN;
62 import static org.sonar.server.qualitygate.Condition.Operator.LESS_THAN;
63 import static org.sonar.server.qualitygate.ValidRatingMetrics.isCoreRatingMetric;
64 import static org.sonar.server.qualitygate.ValidRatingMetrics.isSoftwareQualityRatingMetric;
65
66 public class QualityGateConditionsUpdater {
67   public static final Set<String> INVALID_METRIC_KEYS = Stream.of(ALERT_STATUS_KEY, SECURITY_HOTSPOTS_KEY, NEW_SECURITY_HOTSPOTS_KEY)
68     .collect(Collectors.toUnmodifiableSet());
69
70   private static final Map<Integer, Set<Condition.Operator>> VALID_OPERATORS_BY_DIRECTION = Map.of(
71     DIRECTION_NONE, Set.of(GREATER_THAN, LESS_THAN),
72     DIRECTION_BETTER, Set.of(LESS_THAN),
73     DIRECTION_WORST, Set.of(GREATER_THAN));
74
75   private static final EnumSet<ValueType> VALID_METRIC_TYPES = EnumSet.of(
76     ValueType.INT,
77     ValueType.FLOAT,
78     ValueType.PERCENT,
79     ValueType.MILLISEC,
80     ValueType.LEVEL,
81     ValueType.RATING,
82     ValueType.WORK_DUR);
83
84   private static final List<String> RATING_VALID_INT_VALUES = stream(Rating.values()).map(r -> Integer.toString(r.getIndex())).toList();
85
86   private final DbClient dbClient;
87
88   public QualityGateConditionsUpdater(DbClient dbClient) {
89     this.dbClient = dbClient;
90
91   }
92
93   public QualityGateConditionDto createCondition(DbSession dbSession, QualityGateDto qualityGate, String metricKey, String operator,
94     String errorThreshold) {
95     MetricDto metric = getNonNullMetric(dbSession, metricKey);
96     validateCondition(metric, operator, errorThreshold);
97     Collection<QualityGateConditionDto> conditions = getConditions(dbSession, qualityGate.getUuid());
98     checkConditionDoesNotExistOnSameMetric(conditions, metric);
99     checkConditionDoesNotExistOnEquivalentMetric(dbSession, conditions, metric);
100     QualityGateConditionDto newCondition = new QualityGateConditionDto().setQualityGateUuid(qualityGate.getUuid())
101       .setUuid(Uuids.create())
102       .setMetricUuid(metric.getUuid()).setMetricKey(metric.getKey())
103       .setOperator(operator)
104       .setErrorThreshold(errorThreshold);
105     dbClient.gateConditionDao().insert(newCondition, dbSession);
106     return newCondition;
107   }
108
109   public QualityGateConditionDto updateCondition(DbSession dbSession, QualityGateConditionDto condition, String metricKey, String operator,
110     String errorThreshold) {
111     MetricDto metric = getNonNullMetric(dbSession, metricKey);
112     validateCondition(metric, operator, errorThreshold);
113     Collection<QualityGateConditionDto> otherConditions = getConditions(dbSession, condition.getQualityGateUuid())
114       .stream()
115       .filter(c -> !c.getUuid().equals(condition.getUuid()))
116       .toList();
117     checkConditionDoesNotExistOnEquivalentMetric(dbSession, otherConditions, metric);
118     condition
119       .setMetricUuid(metric.getUuid())
120       .setMetricKey(metric.getKey())
121       .setOperator(operator)
122       .setErrorThreshold(errorThreshold);
123     dbClient.gateConditionDao().update(condition, dbSession);
124     return condition;
125   }
126
127   private MetricDto getNonNullMetric(DbSession dbSession, String metricKey) {
128     MetricDto metric = dbClient.metricDao().selectByKey(dbSession, metricKey);
129     if (metric == null) {
130       throw new NotFoundException(format("There is no metric with key=%s", metricKey));
131     }
132     return metric;
133   }
134
135   private Collection<QualityGateConditionDto> getConditions(DbSession dbSession, String qGateUuid) {
136     return dbClient.gateConditionDao().selectForQualityGate(dbSession, qGateUuid);
137   }
138
139   private static void validateCondition(MetricDto metric, String operator, String errorThreshold) {
140     List<String> errors = new ArrayList<>();
141     validateMetric(metric, errors);
142     checkOperator(metric, operator, errors);
143     checkErrorThreshold(metric, errorThreshold, errors);
144     checkRatingMetric(metric, errorThreshold, errors);
145     checkRequest(errors.isEmpty(), errors);
146   }
147
148   private static void validateMetric(MetricDto metric, List<String> errors) {
149     check(isValid(metric), errors, "Metric '%s' cannot be used to define a condition.", metric.getKey());
150   }
151
152   private static boolean isValid(MetricDto metric) {
153     return !metric.isHidden()
154       && VALID_METRIC_TYPES.contains(ValueType.valueOf(metric.getValueType()))
155       && !INVALID_METRIC_KEYS.contains(metric.getKey());
156   }
157
158   private static void checkOperator(MetricDto metric, String operator, List<String> errors) {
159     check(
160       Condition.Operator.isValid(operator) && isAllowedOperator(operator, metric),
161       errors,
162       "Operator %s is not allowed for this metric.", operator);
163   }
164
165   private static void checkErrorThreshold(MetricDto metric, String errorThreshold, List<String> errors) {
166     requireNonNull(errorThreshold, "errorThreshold can not be null");
167     validateErrorThresholdValue(metric, errorThreshold, errors);
168   }
169
170   private static void checkConditionDoesNotExistOnSameMetric(Collection<QualityGateConditionDto> conditions, MetricDto metric) {
171     if (conditions.isEmpty()) {
172       return;
173     }
174
175     boolean conditionExists = conditions.stream().anyMatch(c -> c.getMetricUuid().equals(metric.getUuid()));
176     checkRequest(!conditionExists, format("Condition on metric '%s' already exists.", metric.getShortName()));
177   }
178
179   private void checkConditionDoesNotExistOnEquivalentMetric(DbSession dbSession, Collection<QualityGateConditionDto> conditions, MetricDto metric) {
180     Optional<String> equivalentMetric = StandardToMQRMetrics.getEquivalentMetric(metric.getKey());
181
182     if (conditions.isEmpty() || equivalentMetric.isEmpty()) {
183       return;
184     }
185
186     MetricDto equivalentMetricDto = dbClient.metricDao().selectByKey(dbSession, equivalentMetric.get());
187     boolean conditionExists = conditions.stream()
188       .anyMatch(c -> equivalentMetricDto != null && c.getMetricUuid().equals(equivalentMetricDto.getUuid()));
189
190     if (conditionExists) {
191       throwBadRequestException(
192         format("Condition for metric '%s' already exists on equivalent metric '%s''.", metric.getKey(), equivalentMetricDto.getKey()));
193     }
194   }
195
196   private static boolean isAllowedOperator(String operator, MetricDto metric) {
197     if (VALID_OPERATORS_BY_DIRECTION.containsKey(metric.getDirection())) {
198       return VALID_OPERATORS_BY_DIRECTION.get(metric.getDirection()).contains(Condition.Operator.fromDbValue(operator));
199     }
200
201     return false;
202   }
203
204   private static void validateErrorThresholdValue(MetricDto metric, String errorThreshold, List<String> errors) {
205     try {
206       ValueType valueType = ValueType.valueOf(metric.getValueType());
207       switch (valueType) {
208         case BOOL, INT, RATING:
209           parseInt(errorThreshold);
210           return;
211         case MILLISEC, WORK_DUR:
212           parseLong(errorThreshold);
213           return;
214         case FLOAT, PERCENT:
215           parseDouble(errorThreshold);
216           return;
217         case STRING, LEVEL:
218           return;
219         default:
220           throw new IllegalArgumentException(format("Unsupported value type %s. Cannot convert condition value", valueType));
221       }
222     } catch (Exception e) {
223       errors.add(format("Invalid value '%s' for metric '%s'", errorThreshold, metric.getShortName()));
224     }
225   }
226
227   private static void checkRatingMetric(MetricDto metric, String errorThreshold, List<String> errors) {
228     if (!metric.getValueType().equals(RATING.name())) {
229       return;
230     }
231     if (!isCoreRatingMetric(metric.getKey()) && !isSoftwareQualityRatingMetric(metric.getKey())) {
232       errors.add(format("The metric '%s' cannot be used", metric.getShortName()));
233     }
234     if (!isValidRating(errorThreshold)) {
235       addInvalidRatingError(errorThreshold, errors);
236       return;
237     }
238     checkRatingGreaterThanOperator(errorThreshold, errors);
239   }
240
241   private static void addInvalidRatingError(@Nullable String value, List<String> errors) {
242     errors.add(format("'%s' is not a valid rating", value));
243   }
244
245   private static void checkRatingGreaterThanOperator(@Nullable String value, List<String> errors) {
246     check(isNullOrEmpty(value) || !Objects.equals(toRating(value), E), errors, "There's no worse rating than E (%s)", value);
247   }
248
249   private static Rating toRating(String value) {
250     return Rating.valueOf(parseInt(value));
251   }
252
253   private static boolean isValidRating(@Nullable String value) {
254     return isNullOrEmpty(value) || RATING_VALID_INT_VALUES.contains(value);
255   }
256
257   private static boolean check(boolean expression, List<String> errors, String message, String... args) {
258     if (!expression) {
259       errors.add(format(message, args));
260     }
261     return expression;
262   }
263 }