]> source.dussan.org Git - sonarqube.git/blob
b7400e07f1005a20fc89011b36e32b7151a50f55
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2019 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.platform.db.migration.version.v76;
21
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;
29 import java.util.Map;
30 import java.util.Set;
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;
41
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;
48
49 @SupportsBlueGreen
50 public class MigrateNoMoreUsedQualityGateConditions extends DataChange {
51
52   private static final Logger LOG = Loggers.get(MigrateNoMoreUsedQualityGateConditions.class);
53
54   private static final String OPERATOR_GREATER_THAN = "GT";
55   private static final String OPERATOR_LESS_THAN = "LT";
56
57   private static final int DIRECTION_WORST = -1;
58   private static final int DIRECTION_BETTER = 1;
59   private static final int DIRECTION_NONE = 0;
60
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")
91     .build();
92
93   private final System2 system2;
94
95   public MigrateNoMoreUsedQualityGateConditions(Database db, System2 system2) {
96     super(db);
97     this.system2 = system2;
98   }
99
100   @Override
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)
105       .list(LONG_READER);
106     for (long qualityGateId : qualityGateIds) {
107       List<QualityGateCondition> conditions = loadConditions(context, qualityGateId);
108
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);
117
118       migrationContext.increaseNumberOfProcessedQualityGate();
119     }
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());
123   }
124
125   private static List<Metric> loadMetrics(Context context) throws SQLException {
126     return context
127       .prepareSelect("SELECT m.id, m.name, m.val_type, m.direction FROM metrics m WHERE m.enabled=?")
128       .setBoolean(1, true)
129       .list(row -> new Metric(row.getInt(1), row.getString(2), row.getString(3), row.getInt(4)));
130   }
131
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)
136       .list(
137         row -> new QualityGateCondition(row.getInt(1), row.getInt(2), row.getString(3),
138           row.getString(4), row.getString(5), row.getInt(6)));
139   }
140
141   private static void markNoMoreSupportedConditionsAsToBeDeleted(MigrationContext migrationContext, List<QualityGateCondition> conditions) {
142     conditions.stream()
143       .filter(c -> !c.isToBeDeleted())
144       .filter(c -> !isConditionStillSupported(c, migrationContext.getMetricById(c.getMetricId())))
145       .forEach(QualityGateCondition::setToBeDeleted);
146   }
147
148   private static void markConditionsHavingOnlyWarningAsToBeDeleted(List<QualityGateCondition> conditions) {
149     conditions.stream()
150       .filter(c -> !c.isToBeDeleted())
151       .filter(c -> !isNullOrEmpty(c.getWarning()) && isNullOrEmpty(c.getError()))
152       .forEach(QualityGateCondition::setToBeDeleted);
153   }
154
155   private static void markConditionsUsingLeakPeriodHavingNoRelatedLeakMetricAsToBeDeleted(MigrationContext migrationContext, List<QualityGateCondition> conditions) {
156     conditions
157       .stream()
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();
167         }
168       });
169   }
170
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()) {
175         return;
176       }
177       int metricId = c.getMetricId();
178       if (!conditionsByMetricId.containsKey(metricId)) {
179         conditionsByMetricId.put(metricId, new ArrayList<>(singletonList(c)));
180       } else {
181         List<QualityGateCondition> qualityGateConditions = conditionsByMetricId.get(metricId);
182         qualityGateConditions.add(c);
183         conditionsByMetricId.put(metricId, qualityGateConditions);
184       }
185     }
186
187     for (Map.Entry<Integer, List<QualityGateCondition>> entry : conditionsByMetricId.entrySet()) {
188       List<QualityGateCondition> conditions = entry.getValue();
189       if (conditions.size() > 1) {
190         conditions.stream()
191           .filter(QualityGateCondition::hasLeakPeriod)
192           .forEach(QualityGateCondition::setToBeDeleted);
193       }
194     }
195   }
196
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()));
201
202     conditions
203       .stream()
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();
215           }
216         }
217       });
218   }
219
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)
226       .collect(toSet());
227     if (conditionsToBeUpdated.isEmpty()) {
228       return;
229     }
230     migrationContext.getContext()
231       .prepareUpsert("UPDATE quality_gate_conditions SET value_warning = NULL, updated_at = ? WHERE id IN (" + conditionsToBeUpdated
232         .stream()
233         .map(c -> Integer.toString(c))
234         .collect(Collectors.joining(",")) + ")")
235       .setDate(1, migrationContext.getNow())
236       .execute()
237       .commit();
238     migrationContext.addUpdatedConditions(conditionsToBeUpdated.size());
239   }
240
241   private static void updateConditionsUsingLeakPeriod(MigrationContext migrationContext, List<QualityGateCondition> conditions)
242     throws SQLException {
243
244     Map<String, QualityGateCondition> conditionsByMetricKey = conditions.stream()
245       .filter(c -> !c.isToBeDeleted())
246       .collect(uniqueIndex(c -> migrationContext.getMetricById(c.getMetricId()).getKey()));
247
248     Upsert updateMetricId = migrationContext.getContext()
249       .prepareUpsert("UPDATE quality_gate_conditions SET metric_id = ?, updated_at = ? WHERE id = ? ")
250       .setDate(2, migrationContext.getNow());
251
252     conditions
253       .stream()
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) {
263           try {
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);
270           }
271         }
272       });
273     updateMetricId.commit();
274   }
275
276   private static void dropConditionsIfNeeded(MigrationContext context, List<QualityGateCondition> conditions) throws SQLException {
277     List<QualityGateCondition> conditionsToBeDeleted = conditions.stream()
278       .filter(QualityGateCondition::isToBeDeleted)
279       .collect(toList());
280     if (conditionsToBeDeleted.isEmpty()) {
281       return;
282     }
283     context.getContext()
284       .prepareUpsert("DELETE FROM quality_gate_conditions WHERE id IN (" + conditionsToBeDeleted
285         .stream()
286         .map(c -> Integer.toString(c.getId()))
287         .collect(Collectors.joining(",")) + ")")
288       .execute()
289       .commit();
290     context.addRemovedConditions(conditionsToBeDeleted.size());
291   }
292
293   private static boolean isConditionOnLeakMetric(MigrationContext migrationContext, QualityGateCondition condition) {
294     return LEAK_METRIC_KEY_BY_METRIC_KEY.containsValue(migrationContext.getMetricById(condition.getMetricId()).getKey());
295   }
296
297   private static boolean isConditionStillSupported(QualityGateCondition condition, Metric metric) {
298     return isSupportedMetricType(metric) && isSupportedOperator(condition, metric);
299   }
300
301   private static boolean isSupportedMetricType(Metric metric) {
302     return SUPPORTED_METRIC_TYPES.contains(metric.getType());
303   }
304
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)));
312   }
313
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;
321
322     private boolean toBeDeleted = false;
323
324     public QualityGateCondition(int id, int metricId, String operator, @Nullable String error, @Nullable String warning,
325       @Nullable Integer period) {
326       this.id = id;
327       this.metricId = metricId;
328       this.operator = operator;
329       this.error = error;
330       this.warning = warning;
331       this.period = period;
332     }
333
334     public int getId() {
335       return id;
336     }
337
338     public int getMetricId() {
339       return metricId;
340     }
341
342     public String getOperator() {
343       return operator;
344     }
345
346     @CheckForNull
347     public String getError() {
348       return error;
349     }
350
351     @CheckForNull
352     public String getWarning() {
353       return warning;
354     }
355
356     public boolean hasLeakPeriod() {
357       return period != null && period == 1;
358     }
359
360     public void setToBeDeleted() {
361       toBeDeleted = true;
362     }
363
364     public boolean isToBeDeleted() {
365       return toBeDeleted;
366     }
367   }
368
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;
374
375     public Metric(int id, String key, String type, int direction) {
376       this.id = id;
377       this.key = key;
378       this.type = type;
379       this.direction = direction;
380     }
381
382     public int getId() {
383       return id;
384     }
385
386     public String getKey() {
387       return key;
388     }
389
390     public String getType() {
391       return type;
392     }
393
394     public int getDirection() {
395       return direction;
396     }
397   }
398
399   private static class MigrationContext {
400
401     private final Context context;
402     private final Date now;
403     private final Map<Integer, Metric> metricsById;
404     private final Map<String, Metric> metricsByKey;
405
406     private int nbOfQualityGates;
407     private int nbOfRemovedConditions;
408     private int nbOfUpdatedConditions;
409
410     public MigrationContext(Context context, Date now, List<Metric> metrics) {
411       this.context = context;
412       this.now = now;
413       this.metricsById = metrics.stream().collect(uniqueIndex(Metric::getId));
414       this.metricsByKey = metrics.stream().collect(uniqueIndex(Metric::getKey));
415     }
416
417     public Context getContext() {
418       return context;
419     }
420
421     public Date getNow() {
422       return now;
423     }
424
425     public Metric getMetricByKey(String key) {
426       return metricsByKey.get(key);
427     }
428
429     public Metric getMetricById(int id) {
430       return metricsById.get(id);
431     }
432
433     public void increaseNumberOfProcessedQualityGate() {
434       nbOfQualityGates += 1;
435     }
436
437     public int getNbOfQualityGates() {
438       return nbOfQualityGates;
439     }
440
441     public void addRemovedConditions(int removedConditions) {
442       nbOfRemovedConditions += removedConditions;
443     }
444
445     public int getNbOfRemovedConditions() {
446       return nbOfRemovedConditions;
447     }
448
449     public void addUpdatedConditions(int updatedConditions) {
450       nbOfUpdatedConditions += updatedConditions;
451     }
452
453     public int getNbOfUpdatedConditions() {
454       return nbOfUpdatedConditions;
455     }
456   }
457
458 }