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 }