001 /*
002 * SonarQube, open source software quality management tool.
003 * Copyright (C) 2008-2014 SonarSource
004 * mailto:contact AT sonarsource DOT com
005 *
006 * SonarQube is free software; you can redistribute it and/or
007 * modify it under the terms of the GNU Lesser General Public
008 * License as published by the Free Software Foundation; either
009 * version 3 of the License, or (at your option) any later version.
010 *
011 * SonarQube is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014 * Lesser General Public License for more details.
015 *
016 * You should have received a copy of the GNU Lesser General Public License
017 * along with this program; if not, write to the Free Software Foundation,
018 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
019 */
020
021 package org.sonar.batch.debt;
022
023 import com.google.common.annotations.VisibleForTesting;
024 import org.sonar.api.batch.Decorator;
025 import org.sonar.api.batch.DecoratorBarriers;
026 import org.sonar.api.batch.DecoratorContext;
027 import org.sonar.api.batch.DependedUpon;
028 import org.sonar.api.batch.DependsUpon;
029 import org.sonar.api.batch.rule.Rule;
030 import org.sonar.api.batch.rule.Rules;
031 import org.sonar.api.component.ResourcePerspectives;
032 import org.sonar.api.issue.Issuable;
033 import org.sonar.api.issue.Issue;
034 import org.sonar.api.issue.internal.DefaultIssue;
035 import org.sonar.api.measures.CoreMetrics;
036 import org.sonar.api.measures.Measure;
037 import org.sonar.api.measures.MeasuresFilters;
038 import org.sonar.api.measures.Metric;
039 import org.sonar.api.measures.PersistenceMode;
040 import org.sonar.api.measures.RuleMeasure;
041 import org.sonar.api.resources.Project;
042 import org.sonar.api.resources.Resource;
043 import org.sonar.api.resources.ResourceUtils;
044 import org.sonar.api.rule.RuleKey;
045 import org.sonar.api.rules.RuleFinder;
046 import org.sonar.api.technicaldebt.batch.Characteristic;
047 import org.sonar.api.technicaldebt.batch.TechnicalDebtModel;
048
049 import javax.annotation.CheckForNull;
050 import javax.annotation.Nullable;
051
052 import java.util.Arrays;
053 import java.util.List;
054 import java.util.Map;
055 import java.util.Set;
056
057 import static com.google.common.collect.Lists.newArrayList;
058 import static com.google.common.collect.Maps.newHashMap;
059
060 /**
061 * Decorator that computes the technical debt metric
062 */
063 @DependsUpon(DecoratorBarriers.ISSUES_TRACKED)
064 public final class DebtDecorator implements Decorator {
065
066 private final ResourcePerspectives perspectives;
067 private final TechnicalDebtModel model;
068 private final Rules rules;
069
070 /**
071 * ruleFinder is needed to load "old" rule in order to persist rule measure
072 */
073 private final RuleFinder ruleFinder;
074
075 public DebtDecorator(ResourcePerspectives perspectives, TechnicalDebtModel model, Rules rules, RuleFinder ruleFinder) {
076 this.perspectives = perspectives;
077 this.model = model;
078 this.rules = rules;
079 this.ruleFinder = ruleFinder;
080 }
081
082 public boolean shouldExecuteOnProject(Project project) {
083 return true;
084 }
085
086 @DependedUpon
087 public List<Metric> generatesMetrics() {
088 return Arrays.<Metric>asList(CoreMetrics.TECHNICAL_DEBT);
089 }
090
091 public void decorate(Resource resource, DecoratorContext context) {
092 Issuable issuable = perspectives.as(Issuable.class, resource);
093 if (issuable != null && shouldSaveMeasure(context)) {
094 List<Issue> issues = newArrayList(issuable.issues());
095 saveMeasures(context, issues);
096 }
097 }
098
099 private void saveMeasures(DecoratorContext context, List<Issue> issues) {
100 Long total = 0L;
101 SumMap<RuleKey> ruleDebts = new SumMap<RuleKey>();
102 SumMap<Characteristic> characteristicDebts = new SumMap<Characteristic>();
103
104 // Aggregate rules debt from current issues (and populate current characteristic debt)
105 for (Issue issue : issues) {
106 Long debt = ((DefaultIssue) issue).debtInMinutes();
107 total += computeDebt(debt, issue.ruleKey(), ruleDebts, characteristicDebts);
108 }
109
110 // Aggregate rules debt from children (and populate children characteristics debt)
111 for (Measure measure : context.getChildrenMeasures(MeasuresFilters.rules(CoreMetrics.TECHNICAL_DEBT))) {
112 Long debt = measure.getValue().longValue();
113 RuleMeasure ruleMeasure = (RuleMeasure) measure;
114 total += computeDebt(debt, ruleMeasure.ruleKey(), ruleDebts, characteristicDebts);
115 }
116
117 context.saveMeasure(CoreMetrics.TECHNICAL_DEBT, total.doubleValue());
118 saveOnRule(context, ruleDebts);
119 for (Characteristic characteristic : model.characteristics()) {
120 Long debt = characteristicDebts.get(characteristic);
121 saveCharacteristicMeasure(context, characteristic, debt != null ? debt.doubleValue() : 0d, false);
122 }
123 }
124
125 private Long computeDebt(@Nullable Long debt, RuleKey ruleKey, SumMap<RuleKey> ruleDebts, SumMap<Characteristic> characteristicDebts) {
126 if (debt != null) {
127 Rule rule = rules.find(ruleKey);
128 if (rule != null) {
129 String characteristicKey = rule.debtSubCharacteristic();
130 if (characteristicKey != null) {
131 Characteristic characteristic = model.characteristicByKey(characteristicKey);
132 if (characteristic != null) {
133 ruleDebts.add(ruleKey, debt);
134 characteristicDebts.add(characteristic, debt);
135 propagateTechnicalDebtInParents(characteristic.parent(), debt, characteristicDebts);
136 return debt;
137 }
138 }
139 }
140 }
141 return 0L;
142 }
143
144 private void propagateTechnicalDebtInParents(@Nullable Characteristic characteristic, long value, SumMap<Characteristic> characteristicDebts) {
145 if (characteristic != null) {
146 characteristicDebts.add(characteristic, value);
147 propagateTechnicalDebtInParents(characteristic.parent(), value, characteristicDebts);
148 }
149 }
150
151 private void saveOnRule(DecoratorContext context, SumMap<RuleKey> ruleDebts) {
152 for (Map.Entry<RuleKey, Long> entry : ruleDebts.entrySet()) {
153 org.sonar.api.rules.Rule oldRule = ruleFinder.findByKey(entry.getKey());
154 if (oldRule != null) {
155 saveRuleMeasure(context, oldRule, entry.getValue().doubleValue(), ResourceUtils.isEntity(context.getResource()));
156 }
157 }
158 }
159
160 @VisibleForTesting
161 void saveCharacteristicMeasure(DecoratorContext context, Characteristic characteristic, Double value, boolean inMemory) {
162 // we need the value on projects (root or module) even if value==0 in order to display correctly the SQALE history chart (see SQALE-122)
163 // BUT we don't want to save zero-values for non top-characteristics (see SQALE-147)
164 if (value > 0.0 || (ResourceUtils.isProject(context.getResource()) && characteristic.isRoot())) {
165 Measure measure = new Measure(CoreMetrics.TECHNICAL_DEBT);
166 measure.setCharacteristic(characteristic);
167 saveMeasure(context, measure, value, inMemory);
168 }
169 }
170
171 @VisibleForTesting
172 void saveRuleMeasure(DecoratorContext context, org.sonar.api.rules.Rule rule, Double value, boolean inMemory) {
173 // we need the value on projects (root or module) even if value==0 in order to display correctly the SQALE history chart (see SQALE-122)
174 // BUT we don't want to save zero-values for non top-characteristics (see SQALE-147)
175 if (value > 0.0) {
176 RuleMeasure measure = new RuleMeasure(CoreMetrics.TECHNICAL_DEBT, rule, null, null);
177 saveMeasure(context, measure, value, inMemory);
178 }
179 }
180
181 private void saveMeasure(DecoratorContext context, Measure measure, Double value, boolean inMemory) {
182 measure.setValue(value);
183 if (inMemory) {
184 measure.setPersistenceMode(PersistenceMode.MEMORY);
185 }
186 context.saveMeasure(measure);
187 }
188
189 private boolean shouldSaveMeasure(DecoratorContext context) {
190 return context.getMeasure(CoreMetrics.TECHNICAL_DEBT) == null;
191 }
192
193 private static class SumMap<E> {
194 private Map<E, Long> sumByKeys;
195
196 public SumMap() {
197 sumByKeys = newHashMap();
198 }
199
200 public void add(@Nullable E key, Long value) {
201 if (key != null) {
202 Long currentValue = sumByKeys.get(key);
203 sumByKeys.put(key, currentValue != null ? currentValue + value : value);
204 }
205 }
206
207 @CheckForNull
208 public Long get(E key) {
209 return sumByKeys.get(key);
210 }
211
212 public Set<Map.Entry<E, Long>> entrySet() {
213 return sumByKeys.entrySet();
214 }
215 }
216 }