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    }