3 * Copyright (C) 2009-2019 SonarSource SA
4 * mailto:info AT sonarsource DOT com
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.
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.
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.
20 package org.sonar.server.platform.db.migration.version.v76;
22 import com.google.common.collect.ImmutableMap;
23 import com.google.common.collect.ImmutableSet;
24 import java.sql.SQLException;
25 import java.util.ArrayList;
26 import java.util.Date;
27 import java.util.HashMap;
28 import java.util.List;
31 import java.util.stream.Collectors;
32 import javax.annotation.CheckForNull;
33 import javax.annotation.Nullable;
34 import org.sonar.api.utils.System2;
35 import org.sonar.api.utils.log.Logger;
36 import org.sonar.api.utils.log.Loggers;
37 import org.sonar.db.Database;
38 import org.sonar.server.platform.db.migration.SupportsBlueGreen;
39 import org.sonar.server.platform.db.migration.step.DataChange;
40 import org.sonar.server.platform.db.migration.step.Upsert;
42 import static com.google.common.base.Strings.isNullOrEmpty;
43 import static java.util.Collections.singletonList;
44 import static java.util.stream.Collectors.toSet;
45 import static org.sonar.core.util.stream.MoreCollectors.toList;
46 import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
47 import static org.sonar.server.platform.db.migration.step.Select.LONG_READER;
50 public class MigrateNoMoreUsedQualityGateConditions extends DataChange {
52 private static final Logger LOG = Loggers.get(MigrateNoMoreUsedQualityGateConditions.class);
54 private static final String OPERATOR_GREATER_THAN = "GT";
55 private static final String OPERATOR_LESS_THAN = "LT";
57 private static final int DIRECTION_WORST = -1;
58 private static final int DIRECTION_BETTER = 1;
59 private static final int DIRECTION_NONE = 0;
61 private static final Set<String> SUPPORTED_OPERATORS = ImmutableSet.of(OPERATOR_GREATER_THAN, OPERATOR_LESS_THAN);
62 private static final Set<String> SUPPORTED_METRIC_TYPES = ImmutableSet.of("INT", "FLOAT", "PERCENT", "MILLISEC", "LEVEL", "RATING", "WORK_DUR");
63 private static final Map<String, String> LEAK_METRIC_KEY_BY_METRIC_KEY = ImmutableMap.<String, String>builder()
64 .put("branch_coverage", "new_branch_coverage")
65 .put("conditions_to_cover", "new_conditions_to_cover")
66 .put("coverage", "new_coverage")
67 .put("line_coverage", "new_line_coverage")
68 .put("lines_to_cover", "new_lines_to_cover")
69 .put("uncovered_conditions", "new_uncovered_conditions")
70 .put("uncovered_lines", "new_uncovered_lines")
71 .put("duplicated_blocks", "new_duplicated_blocks")
72 .put("duplicated_lines", "new_duplicated_lines")
73 .put("duplicated_lines_density", "new_duplicated_lines_density")
74 .put("blocker_violations", "new_blocker_violations")
75 .put("critical_violations", "new_critical_violations")
76 .put("info_violations", "new_info_violations")
77 .put("violations", "new_violations")
78 .put("major_violations", "new_major_violations")
79 .put("minor_violations", "new_minor_violations")
80 .put("sqale_index", "new_technical_debt")
81 .put("code_smells", "new_code_smells")
82 .put("sqale_rating", "new_maintainability_rating")
83 .put("sqale_debt_ratio", "new_sqale_debt_ratio")
84 .put("bugs", "new_bugs")
85 .put("reliability_rating", "new_reliability_rating")
86 .put("reliability_remediation_effort", "new_reliability_remediation_effort")
87 .put("vulnerabilities", "new_vulnerabilities")
88 .put("security_rating", "new_security_rating")
89 .put("security_remediation_effort", "new_security_remediation_effort")
90 .put("lines", "new_lines")
93 private final System2 system2;
95 public MigrateNoMoreUsedQualityGateConditions(Database db, System2 system2) {
97 this.system2 = system2;
101 protected void execute(Context context) throws SQLException {
102 MigrationContext migrationContext = new MigrationContext(context, new Date(system2.now()), loadMetrics(context));
103 List<Long> qualityGateIds = context.prepareSelect("SELECT id FROM quality_gates qg WHERE qg.is_built_in=?")
104 .setBoolean(1, false)
106 for (long qualityGateId : qualityGateIds) {
107 List<QualityGateCondition> conditions = loadConditions(context, qualityGateId);
109 markNoMoreSupportedConditionsAsToBeDeleted(migrationContext, conditions);
110 markConditionsHavingOnlyWarningAsToBeDeleted(conditions);
111 markConditionsUsingLeakPeriodHavingNoRelatedLeakMetricAsToBeDeleted(migrationContext, conditions);
112 markConditionsUsingLeakPeriodUsingSameMetricAsOtherConditionOnOverallAsToBeDeleted(conditions);
113 markConditionsUsingLeakPeriodHavingAlreadyRelatedConditionAsToBeDeleted(migrationContext, conditions);
114 updateConditionsUsingLeakPeriod(migrationContext, conditions);
115 updateConditionsHavingErrorAndWarningByRemovingWarning(migrationContext, conditions);
116 dropConditionsIfNeeded(migrationContext, conditions);
118 migrationContext.increaseNumberOfProcessedQualityGate();
120 LOG.info("{} custom quality gates have been loaded", migrationContext.getNbOfQualityGates());
121 LOG.info("{} conditions have been removed", migrationContext.getNbOfRemovedConditions());
122 LOG.info("{} conditions have been updated", migrationContext.getNbOfUpdatedConditions());
125 private static List<Metric> loadMetrics(Context context) throws SQLException {
127 .prepareSelect("SELECT m.id, m.name, m.val_type, m.direction FROM metrics m WHERE m.enabled=?")
129 .list(row -> new Metric(row.getInt(1), row.getString(2), row.getString(3), row.getInt(4)));
132 private static List<QualityGateCondition> loadConditions(Context context, long qualityGateId) throws SQLException {
133 return context.prepareSelect("SELECT qgc.id, qgc.metric_id, qgc.operator, qgc.value_error, qgc.value_warning, qgc.period FROM quality_gate_conditions qgc " +
134 "WHERE qgc.qgate_id=? ")
135 .setLong(1, qualityGateId)
137 row -> new QualityGateCondition(row.getInt(1), row.getInt(2), row.getString(3),
138 row.getString(4), row.getString(5), row.getInt(6)));
141 private static void markNoMoreSupportedConditionsAsToBeDeleted(MigrationContext migrationContext, List<QualityGateCondition> conditions) {
143 .filter(c -> !c.isToBeDeleted())
144 .filter(c -> !isConditionStillSupported(c, migrationContext.getMetricById(c.getMetricId())))
145 .forEach(QualityGateCondition::setToBeDeleted);
148 private static void markConditionsHavingOnlyWarningAsToBeDeleted(List<QualityGateCondition> conditions) {
150 .filter(c -> !c.isToBeDeleted())
151 .filter(c -> !isNullOrEmpty(c.getWarning()) && isNullOrEmpty(c.getError()))
152 .forEach(QualityGateCondition::setToBeDeleted);
155 private static void markConditionsUsingLeakPeriodHavingNoRelatedLeakMetricAsToBeDeleted(MigrationContext migrationContext, List<QualityGateCondition> conditions) {
158 .filter(c -> !c.isToBeDeleted())
159 .filter(QualityGateCondition::hasLeakPeriod)
160 .filter(condition -> !isConditionOnLeakMetric(migrationContext, condition))
161 .forEach(condition -> {
162 String metricKey = migrationContext.getMetricById(condition.getMetricId()).getKey();
163 String relatedLeakMetric = LEAK_METRIC_KEY_BY_METRIC_KEY.get(metricKey);
164 // Metric has no related metric on leak period => delete condition
165 if (relatedLeakMetric == null) {
166 condition.setToBeDeleted();
171 private static void markConditionsUsingLeakPeriodUsingSameMetricAsOtherConditionOnOverallAsToBeDeleted(List<QualityGateCondition> allConditions) {
172 Map<Integer, List<QualityGateCondition>> conditionsByMetricId = new HashMap<>();
173 for (QualityGateCondition c : allConditions) {
174 if (c.isToBeDeleted()) {
177 int metricId = c.getMetricId();
178 if (!conditionsByMetricId.containsKey(metricId)) {
179 conditionsByMetricId.put(metricId, new ArrayList<>(singletonList(c)));
181 List<QualityGateCondition> qualityGateConditions = conditionsByMetricId.get(metricId);
182 qualityGateConditions.add(c);
183 conditionsByMetricId.put(metricId, qualityGateConditions);
187 for (Map.Entry<Integer, List<QualityGateCondition>> entry : conditionsByMetricId.entrySet()) {
188 List<QualityGateCondition> conditions = entry.getValue();
189 if (conditions.size() > 1) {
191 .filter(QualityGateCondition::hasLeakPeriod)
192 .forEach(QualityGateCondition::setToBeDeleted);
197 private static void markConditionsUsingLeakPeriodHavingAlreadyRelatedConditionAsToBeDeleted(MigrationContext migrationContext, List<QualityGateCondition> conditions) {
198 Map<String, QualityGateCondition> conditionsByMetricKey = conditions.stream()
199 .filter(c -> !c.isToBeDeleted())
200 .collect(uniqueIndex(c -> migrationContext.getMetricById(c.getMetricId()).getKey()));
204 .filter(condition -> !condition.isToBeDeleted())
205 .filter(QualityGateCondition::hasLeakPeriod)
206 .filter(condition -> !isConditionOnLeakMetric(migrationContext, condition))
207 .forEach(condition -> {
208 String metricKey = migrationContext.getMetricById(condition.getMetricId()).getKey();
209 String relatedLeakMetric = LEAK_METRIC_KEY_BY_METRIC_KEY.get(metricKey);
210 if (relatedLeakMetric != null) {
211 QualityGateCondition existingConditionUsingRelatedLeakPeriod = conditionsByMetricKey.get(relatedLeakMetric);
212 if (existingConditionUsingRelatedLeakPeriod != null) {
213 // Another condition on related leak period metric exist => delete condition
214 condition.setToBeDeleted();
220 private static void updateConditionsHavingErrorAndWarningByRemovingWarning(MigrationContext migrationContext, List<QualityGateCondition> conditions)
221 throws SQLException {
222 Set<Integer> conditionsToBeUpdated = conditions.stream()
223 .filter(c -> !c.isToBeDeleted())
224 .filter(c -> !isNullOrEmpty(c.getWarning()) && !isNullOrEmpty(c.getError()))
225 .map(QualityGateCondition::getId)
227 if (conditionsToBeUpdated.isEmpty()) {
230 migrationContext.getContext()
231 .prepareUpsert("UPDATE quality_gate_conditions SET value_warning = NULL, updated_at = ? WHERE id IN (" + conditionsToBeUpdated
233 .map(c -> Integer.toString(c))
234 .collect(Collectors.joining(",")) + ")")
235 .setDate(1, migrationContext.getNow())
238 migrationContext.addUpdatedConditions(conditionsToBeUpdated.size());
241 private static void updateConditionsUsingLeakPeriod(MigrationContext migrationContext, List<QualityGateCondition> conditions)
242 throws SQLException {
244 Map<String, QualityGateCondition> conditionsByMetricKey = conditions.stream()
245 .filter(c -> !c.isToBeDeleted())
246 .collect(uniqueIndex(c -> migrationContext.getMetricById(c.getMetricId()).getKey()));
248 Upsert updateMetricId = migrationContext.getContext()
249 .prepareUpsert("UPDATE quality_gate_conditions SET metric_id = ?, updated_at = ? WHERE id = ? ")
250 .setDate(2, migrationContext.getNow());
254 .filter(c -> !c.isToBeDeleted())
255 .filter(QualityGateCondition::hasLeakPeriod)
256 .filter(condition -> !isConditionOnLeakMetric(migrationContext, condition))
257 .forEach(condition -> {
258 String metricKey = migrationContext.getMetricById(condition.getMetricId()).getKey();
259 String relatedLeakMetric = LEAK_METRIC_KEY_BY_METRIC_KEY.get(metricKey);
260 QualityGateCondition existingConditionUsingRelatedLeakPeriod = conditionsByMetricKey.get(relatedLeakMetric);
261 // Metric has a related leak period metric => update the condition
262 if (existingConditionUsingRelatedLeakPeriod == null) {
264 updateMetricId.setInt(1, migrationContext.getMetricByKey(relatedLeakMetric).getId());
265 updateMetricId.setInt(3, condition.getId());
266 updateMetricId.execute();
267 migrationContext.addUpdatedConditions(1);
268 } catch (SQLException e) {
269 throw new IllegalStateException("Fail to update quality gate conditions", e);
273 updateMetricId.commit();
276 private static void dropConditionsIfNeeded(MigrationContext context, List<QualityGateCondition> conditions) throws SQLException {
277 List<QualityGateCondition> conditionsToBeDeleted = conditions.stream()
278 .filter(QualityGateCondition::isToBeDeleted)
280 if (conditionsToBeDeleted.isEmpty()) {
284 .prepareUpsert("DELETE FROM quality_gate_conditions WHERE id IN (" + conditionsToBeDeleted
286 .map(c -> Integer.toString(c.getId()))
287 .collect(Collectors.joining(",")) + ")")
290 context.addRemovedConditions(conditionsToBeDeleted.size());
293 private static boolean isConditionOnLeakMetric(MigrationContext migrationContext, QualityGateCondition condition) {
294 return LEAK_METRIC_KEY_BY_METRIC_KEY.containsValue(migrationContext.getMetricById(condition.getMetricId()).getKey());
297 private static boolean isConditionStillSupported(QualityGateCondition condition, Metric metric) {
298 return isSupportedMetricType(metric) && isSupportedOperator(condition, metric);
301 private static boolean isSupportedMetricType(Metric metric) {
302 return SUPPORTED_METRIC_TYPES.contains(metric.getType());
305 private static boolean isSupportedOperator(QualityGateCondition condition, Metric metric) {
306 String operator = condition.getOperator();
307 int direction = metric.getDirection();
308 return SUPPORTED_OPERATORS.contains(operator) &&
309 (direction == DIRECTION_NONE ||
310 (direction == DIRECTION_WORST && operator.equalsIgnoreCase(OPERATOR_GREATER_THAN)) ||
311 (direction == DIRECTION_BETTER && operator.equalsIgnoreCase(OPERATOR_LESS_THAN)));
314 private static class QualityGateCondition {
315 private final int id;
316 private final int metricId;
317 private final String operator;
318 private final String error;
319 private final String warning;
320 private final Integer period;
322 private boolean toBeDeleted = false;
324 public QualityGateCondition(int id, int metricId, String operator, @Nullable String error, @Nullable String warning,
325 @Nullable Integer period) {
327 this.metricId = metricId;
328 this.operator = operator;
330 this.warning = warning;
331 this.period = period;
338 public int getMetricId() {
342 public String getOperator() {
347 public String getError() {
352 public String getWarning() {
356 public boolean hasLeakPeriod() {
357 return period != null && period == 1;
360 public void setToBeDeleted() {
364 public boolean isToBeDeleted() {
369 private static class Metric {
370 private final int id;
371 private final String key;
372 private final String type;
373 private final int direction;
375 public Metric(int id, String key, String type, int direction) {
379 this.direction = direction;
386 public String getKey() {
390 public String getType() {
394 public int getDirection() {
399 private static class MigrationContext {
401 private final Context context;
402 private final Date now;
403 private final Map<Integer, Metric> metricsById;
404 private final Map<String, Metric> metricsByKey;
406 private int nbOfQualityGates;
407 private int nbOfRemovedConditions;
408 private int nbOfUpdatedConditions;
410 public MigrationContext(Context context, Date now, List<Metric> metrics) {
411 this.context = context;
413 this.metricsById = metrics.stream().collect(uniqueIndex(Metric::getId));
414 this.metricsByKey = metrics.stream().collect(uniqueIndex(Metric::getKey));
417 public Context getContext() {
421 public Date getNow() {
425 public Metric getMetricByKey(String key) {
426 return metricsByKey.get(key);
429 public Metric getMetricById(int id) {
430 return metricsById.get(id);
433 public void increaseNumberOfProcessedQualityGate() {
434 nbOfQualityGates += 1;
437 public int getNbOfQualityGates() {
438 return nbOfQualityGates;
441 public void addRemovedConditions(int removedConditions) {
442 nbOfRemovedConditions += removedConditions;
445 public int getNbOfRemovedConditions() {
446 return nbOfRemovedConditions;
449 public void addUpdatedConditions(int updatedConditions) {
450 nbOfUpdatedConditions += updatedConditions;
453 public int getNbOfUpdatedConditions() {
454 return nbOfUpdatedConditions;