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.base.Function;
024    import com.google.common.collect.Ordering;
025    import org.apache.commons.lang.time.DateUtils;
026    import org.sonar.api.BatchComponent;
027    import org.sonar.api.issue.Issue;
028    import org.sonar.api.issue.internal.DefaultIssue;
029    import org.sonar.api.issue.internal.FieldDiffs;
030    import org.sonar.core.issue.IssueUpdater;
031    
032    import javax.annotation.CheckForNull;
033    import javax.annotation.Nullable;
034    
035    import java.util.*;
036    
037    import static com.google.common.collect.Lists.newArrayList;
038    
039    /**
040     * Warning, before modifying this class, please do not forget that it's used by the Dev Cockpit plugin
041     */
042    public class IssueChangelogDebtCalculator implements BatchComponent {
043    
044      @CheckForNull
045      public Long calculateNewTechnicalDebt(Issue issue, @Nullable Date periodDate) {
046        Long debt = ((DefaultIssue) issue).debtInMinutes();
047        Date periodDatePlusOneSecond = periodDate != null ? DateUtils.addSeconds(periodDate, 1) : null;
048        if (isAfter(issue.creationDate(), periodDatePlusOneSecond)) {
049          return debt;
050        } else {
051          return calculateNewTechnicalDebtValueFromChangelog(debt, issue, periodDate);
052        }
053      }
054    
055      @CheckForNull
056      private Long calculateNewTechnicalDebtValueFromChangelog(@Nullable Long currentTechnicalDebtValue, Issue issue, Date periodDate) {
057        List<FieldDiffs> changelog = technicalDebtHistory(issue);
058        for (Iterator<FieldDiffs> iterator = changelog.iterator(); iterator.hasNext(); ) {
059          FieldDiffs diff = iterator.next();
060          Date date = diff.creationDate();
061          if (isLesserOrEqual(date, periodDate)) {
062            // return new value from the change that is just before the period date
063            return subtractNeverNegative(currentTechnicalDebtValue, newValue(diff));
064          }
065          if (!iterator.hasNext()) {
066            // return old value from the change that is just after the period date when there's no more element in changelog
067            return subtractNeverNegative(currentTechnicalDebtValue, oldValue(diff));
068          }
069        }
070        // Return null when no changelog
071        return null;
072      }
073    
074      /**
075       * SONAR-5059
076       */
077      @CheckForNull
078      private Long subtractNeverNegative(@Nullable Long value, Long with) {
079        Long result = (value != null ? value : 0) - (with != null ? with : 0);
080        return result > 0 ? result : null;
081      }
082    
083      private List<FieldDiffs> technicalDebtHistory(Issue issue) {
084        List<FieldDiffs> technicalDebtChangelog = changesOnField(((DefaultIssue) issue).changes());
085        if (!technicalDebtChangelog.isEmpty()) {
086          // Changelog have to be sorted from newest to oldest.
087          // Null date should be the first as this happen when technical debt has changed since previous analysis.
088          Ordering<FieldDiffs> ordering = Ordering.natural().reverse().nullsFirst().onResultOf(new Function<FieldDiffs, Date>() {
089            public Date apply(FieldDiffs diff) {
090              return diff.creationDate();
091            }
092          });
093          return ordering.immutableSortedCopy(technicalDebtChangelog);
094        }
095        return Collections.emptyList();
096      }
097    
098      private List<FieldDiffs> changesOnField(Collection<FieldDiffs> fieldDiffs) {
099        List<FieldDiffs> diffs = newArrayList();
100        for (FieldDiffs fieldDiff : fieldDiffs) {
101          if (fieldDiff.diffs().containsKey(IssueUpdater.TECHNICAL_DEBT)) {
102            diffs.add(fieldDiff);
103          }
104        }
105        return diffs;
106      }
107    
108      @CheckForNull
109      private Long newValue(FieldDiffs fieldDiffs) {
110        for (Map.Entry<String, FieldDiffs.Diff> entry : fieldDiffs.diffs().entrySet()) {
111          if (entry.getKey().equals(IssueUpdater.TECHNICAL_DEBT)) {
112            return entry.getValue().newValueLong();
113          }
114        }
115        return null;
116      }
117    
118      @CheckForNull
119      private Long oldValue(FieldDiffs fieldDiffs) {
120        for (Map.Entry<String, FieldDiffs.Diff> entry : fieldDiffs.diffs().entrySet()) {
121          if (entry.getKey().equals(IssueUpdater.TECHNICAL_DEBT)) {
122            return entry.getValue().oldValueLong();
123          }
124        }
125        return null;
126      }
127    
128      private boolean isAfter(@Nullable Date currentDate, @Nullable Date pastDate) {
129        return pastDate == null || (currentDate != null && DateUtils.truncatedCompareTo(currentDate, pastDate, Calendar.SECOND) > 0);
130      }
131    
132      private boolean isLesserOrEqual(@Nullable Date currentDate, @Nullable Date pastDate) {
133        return (currentDate != null) && (pastDate == null || (DateUtils.truncatedCompareTo(currentDate, pastDate, Calendar.SECOND) <= 0));
134      }
135    
136    }