aboutsummaryrefslogtreecommitdiffstats
path: root/sonar-core
diff options
context:
space:
mode:
authorSimon Brandhof <simon.brandhof@sonarsource.com>2015-06-03 10:31:16 +0200
committerSimon Brandhof <simon.brandhof@sonarsource.com>2015-07-02 16:06:08 +0200
commitbf4118d6a9ceb9ad24274cdc6537d4a607121815 (patch)
tree8f758ccb7a205da3eae96b05b74f79cade8ceae0 /sonar-core
parent2f948758eebec934beb54701792cf2d558319251 (diff)
downloadsonarqube-bf4118d6a9ceb9ad24274cdc6537d4a607121815.tar.gz
sonarqube-bf4118d6a9ceb9ad24274cdc6537d4a607121815.zip
SONAR-6623 extract issue tracking algorithm from batch
Diffstat (limited to 'sonar-core')
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java613
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/DefaultIssueBuilder.java3
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/DefaultIssueComment.java130
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/FieldDiffs.java195
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/IssueChangeContext.java65
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/IssueUpdater.java3
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/db/IssueChangeDao.java4
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/db/IssueChangeDto.java4
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/db/IssueDao.java11
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/db/IssueDto.java5
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/db/IssueMapper.java6
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/db/IssueStorage.java6
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/db/UpdateConflictResolver.java10
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/tracking/BlockHashSequence.java90
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/tracking/BlockRecognizer.java177
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/tracking/Input.java30
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/tracking/LazyInput.java58
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/tracking/LineHashSequence.java119
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/tracking/Trackable.java40
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/tracking/Tracker.java298
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/tracking/Tracking.java116
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/tracking/package-info.java23
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/workflow/FunctionExecutor.java4
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/workflow/IsBeingClosed.java (renamed from sonar-core/src/main/java/org/sonar/core/issue/workflow/IsEndOfLife.java)13
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/workflow/IsManual.java11
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/workflow/IssueWorkflow.java53
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/workflow/SetCloseDate.java2
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/workflow/SetClosed.java (renamed from sonar-core/src/main/java/org/sonar/core/issue/workflow/SetEndOfLife.java)8
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/workflow/UnsetAssignee.java (renamed from sonar-core/src/main/java/org/sonar/core/issue/workflow/SetAssignee.java)16
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/workflow/UnsetLine.java29
-rw-r--r--sonar-core/src/main/resources/org/sonar/core/issue/db/IssueMapper.xml43
-rw-r--r--sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueBuilderTest.java1
-rw-r--r--sonar-core/src/test/java/org/sonar/core/issue/IssueChangeContextTest.java1
-rw-r--r--sonar-core/src/test/java/org/sonar/core/issue/IssueUpdaterTest.java3
-rw-r--r--sonar-core/src/test/java/org/sonar/core/issue/db/IssueChangeDaoTest.java4
-rw-r--r--sonar-core/src/test/java/org/sonar/core/issue/db/IssueChangeDtoTest.java4
-rw-r--r--sonar-core/src/test/java/org/sonar/core/issue/db/IssueDaoTest.java35
-rw-r--r--sonar-core/src/test/java/org/sonar/core/issue/db/IssueDtoTest.java2
-rw-r--r--sonar-core/src/test/java/org/sonar/core/issue/db/IssueStorageTest.java6
-rw-r--r--sonar-core/src/test/java/org/sonar/core/issue/db/UpdateConflictResolverTest.java2
-rw-r--r--sonar-core/src/test/java/org/sonar/core/issue/tracking/BlockHashSequenceTest.java42
-rw-r--r--sonar-core/src/test/java/org/sonar/core/issue/tracking/BlockRecognizerTest.java56
-rw-r--r--sonar-core/src/test/java/org/sonar/core/issue/tracking/TrackerTest.java456
-rw-r--r--sonar-core/src/test/java/org/sonar/core/issue/workflow/IsBeingClosedTest.java (renamed from sonar-core/src/test/java/org/sonar/core/issue/workflow/IsEndOfLifeTest.java)19
-rw-r--r--sonar-core/src/test/java/org/sonar/core/issue/workflow/IsManualTest.java17
-rw-r--r--sonar-core/src/test/java/org/sonar/core/issue/workflow/IssueWorkflowTest.java20
-rw-r--r--sonar-core/src/test/java/org/sonar/core/issue/workflow/SetClosedTest.java (renamed from sonar-core/src/test/java/org/sonar/core/issue/workflow/SetEndOfLifeTest.java)22
-rw-r--r--sonar-core/src/test/java/org/sonar/core/issue/workflow/TransitionTest.java2
-rw-r--r--sonar-core/src/test/java/org/sonar/core/issue/workflow/UnsetAssigneeTest.java (renamed from sonar-core/src/test/java/org/sonar/core/issue/workflow/SetAssigneeTest.java)19
-rw-r--r--sonar-core/src/test/resources/org/sonar/core/issue/db/IssueDaoTest/should_select_by_key.xml28
50 files changed, 2696 insertions, 228 deletions
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java b/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java
new file mode 100644
index 00000000000..9439aad99c5
--- /dev/null
+++ b/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java
@@ -0,0 +1,613 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.core.issue;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang.builder.ToStringBuilder;
+import org.apache.commons.lang.builder.ToStringStyle;
+import org.apache.commons.lang.time.DateUtils;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.issue.IssueComment;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rule.Severity;
+import org.sonar.api.utils.Duration;
+import org.sonar.core.issue.tracking.Trackable;
+
+/**
+ * PLUGINS MUST NOT BE USED THIS CLASS, EXCEPT FOR UNIT TESTING.
+ *
+ * @since 3.6
+ */
+public class DefaultIssue implements Issue, Trackable {
+
+ private String key;
+
+ private String componentUuid;
+ private String componentKey;
+
+ private String moduleUuid;
+ private String moduleUuidPath;
+
+ private String projectUuid;
+ private String projectKey;
+
+ private RuleKey ruleKey;
+ private String language;
+ private String severity;
+ private boolean manualSeverity = false;
+ private String message;
+ private Integer line;
+ private Double effortToFix;
+ private Duration debt;
+ private String status;
+ private String resolution;
+ private String reporter;
+ private String assignee;
+ private String checksum;
+ private Map<String, String> attributes = null;
+ private String authorLogin = null;
+ private String actionPlanKey;
+ private List<IssueComment> comments = null;
+ private Set<String> tags = null;
+
+ // FUNCTIONAL DATES
+ private Date creationDate;
+ private Date updateDate;
+ private Date closeDate;
+
+ // FOLLOWING FIELDS ARE AVAILABLE ONLY DURING SCAN
+
+ // Current changes
+ private FieldDiffs currentChange = null;
+
+ // all changes
+ private List<FieldDiffs> changes = null;
+
+ // true if the the issue did not exist in the previous scan.
+ private boolean isNew = true;
+
+ // True if the the issue did exist in the previous scan but not in the current one. That means
+ // that this issue should be closed.
+ private boolean beingClosed = false;
+
+ private boolean onDisabledRule = false;
+
+ // true if some fields have been changed since the previous scan
+ private boolean isChanged = false;
+
+ // true if notifications have to be sent
+ private boolean sendNotifications = false;
+
+ // Date when issue was loaded from db (only when isNew=false)
+ private Long selectedAt;
+
+ @Override
+ public String key() {
+ return key;
+ }
+
+ public DefaultIssue setKey(String key) {
+ this.key = key;
+ return this;
+ }
+
+ /**
+ * Can be null on Views or Devs
+ */
+ @Override
+ @CheckForNull
+ public String componentUuid() {
+ return componentUuid;
+ }
+
+ public DefaultIssue setComponentUuid(@CheckForNull String componentUuid) {
+ this.componentUuid = componentUuid;
+ return this;
+ }
+
+ @Override
+ public String componentKey() {
+ return componentKey;
+ }
+
+ public DefaultIssue setComponentKey(String s) {
+ this.componentKey = s;
+ return this;
+ }
+
+ @CheckForNull
+ public String moduleUuid() {
+ return moduleUuid;
+ }
+
+ public DefaultIssue setModuleUuid(@Nullable String moduleUuid) {
+ this.moduleUuid = moduleUuid;
+ return this;
+ }
+
+ @CheckForNull
+ public String moduleUuidPath() {
+ return moduleUuidPath;
+ }
+
+ public DefaultIssue setModuleUuidPath(@Nullable String moduleUuidPath) {
+ this.moduleUuidPath = moduleUuidPath;
+ return this;
+ }
+
+ /**
+ * Can be null on Views or Devs
+ */
+ @Override
+ @CheckForNull
+ public String projectUuid() {
+ return projectUuid;
+ }
+
+ public DefaultIssue setProjectUuid(@Nullable String projectUuid) {
+ this.projectUuid = projectUuid;
+ return this;
+ }
+
+ @Override
+ public String projectKey() {
+ return projectKey;
+ }
+
+ public DefaultIssue setProjectKey(String projectKey) {
+ this.projectKey = projectKey;
+ return this;
+ }
+
+ @Override
+ public RuleKey ruleKey() {
+ return ruleKey;
+ }
+
+ public DefaultIssue setRuleKey(RuleKey k) {
+ this.ruleKey = k;
+ return this;
+ }
+
+ @Override
+ public String language() {
+ return language;
+ }
+
+ public DefaultIssue setLanguage(String l) {
+ this.language = l;
+ return this;
+ }
+
+ @Override
+ public String severity() {
+ return severity;
+ }
+
+ public DefaultIssue setSeverity(@Nullable String s) {
+ Preconditions.checkArgument(s == null || Severity.ALL.contains(s), "Not a valid severity: " + s);
+ this.severity = s;
+ return this;
+ }
+
+ public boolean manualSeverity() {
+ return manualSeverity;
+ }
+
+ public DefaultIssue setManualSeverity(boolean b) {
+ this.manualSeverity = b;
+ return this;
+ }
+
+ @Override
+ @CheckForNull
+ public String message() {
+ return message;
+ }
+
+ public DefaultIssue setMessage(@Nullable String s) {
+ this.message = StringUtils.abbreviate(StringUtils.trim(s), MESSAGE_MAX_SIZE);
+ return this;
+ }
+
+ @Override
+ @CheckForNull
+ public Integer line() {
+ return line;
+ }
+
+ public DefaultIssue setLine(@Nullable Integer l) {
+ Preconditions.checkArgument(l == null || l > 0, "Line must be null or greater than zero (got " + l + ")");
+ this.line = l;
+ return this;
+ }
+
+ @Override
+ @CheckForNull
+ public Double effortToFix() {
+ return effortToFix;
+ }
+
+ public DefaultIssue setEffortToFix(@Nullable Double d) {
+ Preconditions.checkArgument(d == null || d >= 0, "Effort to fix must be greater than or equal 0 (got " + d + ")");
+ this.effortToFix = d;
+ return this;
+ }
+
+ /**
+ * Elapsed time to fix the issue
+ */
+ @Override
+ @CheckForNull
+ public Duration debt() {
+ return debt;
+ }
+
+ @CheckForNull
+ public Long debtInMinutes() {
+ return debt != null ? debt.toMinutes() : null;
+ }
+
+ public DefaultIssue setDebt(@Nullable Duration t) {
+ this.debt = t;
+ return this;
+ }
+
+ @Override
+ public String status() {
+ return status;
+ }
+
+ public DefaultIssue setStatus(String s) {
+ Preconditions.checkArgument(!Strings.isNullOrEmpty(s), "Status must be set");
+ this.status = s;
+ return this;
+ }
+
+ @Override
+ @CheckForNull
+ public String resolution() {
+ return resolution;
+ }
+
+ public DefaultIssue setResolution(@Nullable String s) {
+ this.resolution = s;
+ return this;
+ }
+
+ @Override
+ @CheckForNull
+ public String reporter() {
+ return reporter;
+ }
+
+ public DefaultIssue setReporter(@Nullable String s) {
+ this.reporter = s;
+ return this;
+ }
+
+ @Override
+ @CheckForNull
+ public String assignee() {
+ return assignee;
+ }
+
+ public DefaultIssue setAssignee(@Nullable String s) {
+ this.assignee = s;
+ return this;
+ }
+
+ @Override
+ public Date creationDate() {
+ return creationDate;
+ }
+
+ public DefaultIssue setCreationDate(Date d) {
+ // d is not marked as Nullable but we still allow null parameter for unit testing.
+ this.creationDate = (d != null ? DateUtils.truncate(d, Calendar.SECOND) : null);
+ return this;
+ }
+
+ @Override
+ @CheckForNull
+ public Date updateDate() {
+ return updateDate;
+ }
+
+ public DefaultIssue setUpdateDate(@Nullable Date d) {
+ this.updateDate = (d != null ? DateUtils.truncate(d, Calendar.SECOND) : null);
+ return this;
+ }
+
+ @Override
+ @CheckForNull
+ public Date closeDate() {
+ return closeDate;
+ }
+
+ public DefaultIssue setCloseDate(@Nullable Date d) {
+ this.closeDate = (d != null ? DateUtils.truncate(d, Calendar.SECOND) : null);
+ return this;
+ }
+
+ @CheckForNull
+ public String checksum() {
+ return checksum;
+ }
+
+ public DefaultIssue setChecksum(@Nullable String s) {
+ this.checksum = s;
+ return this;
+ }
+
+ @Override
+ public boolean isNew() {
+ return isNew;
+ }
+
+ public DefaultIssue setNew(boolean b) {
+ isNew = b;
+ return this;
+ }
+
+ /**
+ * True when one of the following conditions is true :
+ * <ul>
+ * <li>the related component has been deleted or renamed</li>
+ * <li>the rule has been deleted (eg. on plugin uninstall)</li>
+ * <li>the rule has been disabled in the Quality profile</li>
+ * </ul>
+ */
+ public boolean isBeingClosed() {
+ return beingClosed;
+ }
+
+ public DefaultIssue setBeingClosed(boolean b) {
+ beingClosed = b;
+ return this;
+ }
+
+ public boolean isOnDisabledRule() {
+ return onDisabledRule;
+ }
+
+ public DefaultIssue setOnDisabledRule(boolean b) {
+ onDisabledRule = b;
+ return this;
+ }
+
+ public boolean isChanged() {
+ return isChanged;
+ }
+
+ public DefaultIssue setChanged(boolean b) {
+ isChanged = b;
+ return this;
+ }
+
+ public boolean mustSendNotifications() {
+ return sendNotifications;
+ }
+
+ public DefaultIssue setSendNotifications(boolean b) {
+ sendNotifications = b;
+ return this;
+ }
+
+ @Override
+ @CheckForNull
+ public String attribute(String key) {
+ return attributes == null ? null : attributes.get(key);
+ }
+
+ public DefaultIssue setAttribute(String key, @Nullable String value) {
+ if (attributes == null) {
+ attributes = Maps.newHashMap();
+ }
+ if (value == null) {
+ attributes.remove(key);
+ } else {
+ attributes.put(key, value);
+ }
+ return this;
+ }
+
+ @Override
+ public Map<String, String> attributes() {
+ return attributes == null ? Collections.<String, String>emptyMap() : ImmutableMap.copyOf(attributes);
+ }
+
+ public DefaultIssue setAttributes(@Nullable Map<String, String> map) {
+ if (map != null) {
+ if (attributes == null) {
+ attributes = Maps.newHashMap();
+ }
+ attributes.putAll(map);
+ }
+ return this;
+ }
+
+ @Override
+ @CheckForNull
+ public String authorLogin() {
+ return authorLogin;
+ }
+
+ public DefaultIssue setAuthorLogin(@Nullable String s) {
+ this.authorLogin = s;
+ return this;
+ }
+
+ @Override
+ @CheckForNull
+ public String actionPlanKey() {
+ return actionPlanKey;
+ }
+
+ public DefaultIssue setActionPlanKey(@Nullable String actionPlanKey) {
+ this.actionPlanKey = actionPlanKey;
+ return this;
+ }
+
+ public DefaultIssue setFieldChange(IssueChangeContext context, String field, @Nullable Serializable oldValue, @Nullable Serializable newValue) {
+ if (!Objects.equal(oldValue, newValue)) {
+ if (currentChange == null) {
+ currentChange = new FieldDiffs();
+ currentChange.setUserLogin(context.login());
+ currentChange.setCreationDate(context.date());
+ }
+ currentChange.setDiff(field, oldValue, newValue);
+ }
+ addChange(currentChange);
+ return this;
+ }
+
+ public DefaultIssue setCurrentChange(FieldDiffs currentChange) {
+ this.currentChange = currentChange;
+ addChange(currentChange);
+ return this;
+ }
+
+ @CheckForNull
+ public FieldDiffs currentChange() {
+ return currentChange;
+ }
+
+ public DefaultIssue addChange(FieldDiffs change) {
+ if (changes == null) {
+ changes = new ArrayList<>();
+ }
+ changes.add(change);
+ return this;
+ }
+
+ public DefaultIssue setChanges(List<FieldDiffs> changes) {
+ this.changes = changes;
+ return this;
+ }
+
+ public List<FieldDiffs> changes() {
+ if (changes == null) {
+ return Collections.emptyList();
+ }
+ return ImmutableList.copyOf(changes);
+ }
+
+ public DefaultIssue addComment(DefaultIssueComment comment) {
+ if (comments == null) {
+ comments = new ArrayList<>();
+ }
+ comments.add(comment);
+ return this;
+ }
+
+ @Override
+ public List<IssueComment> comments() {
+ if (comments == null) {
+ return Collections.emptyList();
+ }
+ return ImmutableList.copyOf(comments);
+ }
+
+ @CheckForNull
+ public Long selectedAt() {
+ return selectedAt;
+ }
+
+ public DefaultIssue setSelectedAt(@Nullable Long d) {
+ this.selectedAt = d;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ DefaultIssue that = (DefaultIssue) o;
+ return !(key != null ? !key.equals(that.key) : (that.key != null));
+ }
+
+ @Override
+ public int hashCode() {
+ return key != null ? key.hashCode() : 0;
+ }
+
+ @Override
+ public String toString() {
+ return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
+ }
+
+ @Override
+ public Set<String> tags() {
+ if (tags == null) {
+ return ImmutableSet.of();
+ } else {
+ return ImmutableSet.copyOf(tags);
+ }
+ }
+
+ public DefaultIssue setTags(Collection<String> tags) {
+ this.tags = new LinkedHashSet<>(tags);
+ return this;
+ }
+
+ @Override
+ public Integer getLine() {
+ return line;
+ }
+
+ @Override
+ public String getMessage() {
+ return message;
+ }
+
+ @Override
+ public String getLineHash() {
+ return checksum;
+ }
+
+ @Override
+ public RuleKey getRuleKey() {
+ return ruleKey;
+ }
+}
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssueBuilder.java b/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssueBuilder.java
index 3c39b5dadde..962a4591916 100644
--- a/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssueBuilder.java
+++ b/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssueBuilder.java
@@ -23,7 +23,6 @@ import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
import org.sonar.api.issue.Issuable;
import org.sonar.api.issue.Issue;
-import org.sonar.api.issue.internal.DefaultIssue;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.utils.internal.Uuids;
@@ -132,7 +131,7 @@ public class DefaultIssueBuilder implements Issuable.IssueBuilder {
issue.setStatus(Issue.STATUS_OPEN);
issue.setCloseDate(null);
issue.setNew(true);
- issue.setEndOfLife(false);
+ issue.setBeingClosed(false);
issue.setOnDisabledRule(false);
return issue;
}
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssueComment.java b/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssueComment.java
new file mode 100644
index 00000000000..3a5b23b0056
--- /dev/null
+++ b/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssueComment.java
@@ -0,0 +1,130 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.core.issue;
+
+import org.sonar.api.issue.IssueComment;
+import org.sonar.api.utils.internal.Uuids;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * PLUGINS MUST NOT BE USED THIS CLASS, EXCEPT FOR UNIT TESTING.
+ *
+ * @since 3.6
+ */
+public class DefaultIssueComment implements Serializable, IssueComment {
+
+ private String issueKey;
+ private String userLogin;
+ private Date createdAt;
+ private Date updatedAt;
+ private String key;
+ private String markdownText;
+ private boolean isNew;
+
+ public static DefaultIssueComment create(String issueKey, @Nullable String login, String markdownText) {
+ DefaultIssueComment comment = new DefaultIssueComment();
+ comment.setIssueKey(issueKey);
+ comment.setKey(Uuids.create());
+ Date now = new Date();
+ comment.setUserLogin(login);
+ comment.setMarkdownText(markdownText);
+ comment.setCreatedAt(now).setUpdatedAt(now);
+ comment.setNew(true);
+ return comment;
+ }
+
+ @Override
+ public String markdownText() {
+ return markdownText;
+ }
+
+ public DefaultIssueComment setMarkdownText(String s) {
+ this.markdownText = s;
+ return this;
+ }
+
+ @Override
+ public String issueKey() {
+ return issueKey;
+ }
+
+ public DefaultIssueComment setIssueKey(String s) {
+ this.issueKey = s;
+ return this;
+ }
+
+ @Override
+ public String key() {
+ return key;
+ }
+
+ public DefaultIssueComment setKey(String key) {
+ this.key = key;
+ return this;
+ }
+
+ /**
+ * The user who created the comment. Null if it was automatically generated during project scan.
+ */
+ @Override
+ @CheckForNull
+ public String userLogin() {
+ return userLogin;
+ }
+
+ public DefaultIssueComment setUserLogin(@Nullable String userLogin) {
+ this.userLogin = userLogin;
+ return this;
+ }
+
+ @Override
+ public Date createdAt() {
+ return createdAt;
+ }
+
+ public DefaultIssueComment setCreatedAt(Date createdAt) {
+ this.createdAt = createdAt;
+ return this;
+ }
+
+ @Override
+ public Date updatedAt() {
+ return updatedAt;
+ }
+
+ public DefaultIssueComment setUpdatedAt(@Nullable Date updatedAt) {
+ this.updatedAt = updatedAt;
+ return this;
+ }
+
+ public boolean isNew() {
+ return isNew;
+ }
+
+ public DefaultIssueComment setNew(boolean b) {
+ isNew = b;
+ return this;
+ }
+}
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/FieldDiffs.java b/sonar-core/src/main/java/org/sonar/core/issue/FieldDiffs.java
new file mode 100644
index 00000000000..d162056a795
--- /dev/null
+++ b/sonar-core/src/main/java/org/sonar/core/issue/FieldDiffs.java
@@ -0,0 +1,195 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.core.issue;
+
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import com.google.common.collect.Maps;
+import java.io.Serializable;
+import java.util.Date;
+import java.util.Map;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+
+/**
+ * PLUGINS MUST NOT USE THIS CLASS, EXCEPT FOR UNIT TESTING.
+ *
+ * @since 3.6
+ */
+public class FieldDiffs implements Serializable {
+
+ public static final Splitter FIELDS_SPLITTER = Splitter.on(',').omitEmptyStrings();
+
+ private String issueKey;
+ private String userLogin;
+ private Date creationDate;
+
+ private final Map<String, Diff> diffs = Maps.newLinkedHashMap();
+
+ public Map<String, Diff> diffs() {
+ return diffs;
+ }
+
+ public Diff get(String field) {
+ return diffs.get(field);
+ }
+
+ @CheckForNull
+ public String userLogin() {
+ return userLogin;
+ }
+
+ public FieldDiffs setUserLogin(@Nullable String s) {
+ this.userLogin = s;
+ return this;
+ }
+
+ public Date creationDate() {
+ return creationDate;
+ }
+
+ public FieldDiffs setCreationDate(Date creationDate) {
+ this.creationDate = creationDate;
+ return this;
+ }
+
+ public String issueKey() {
+ return issueKey;
+ }
+
+ public FieldDiffs setIssueKey(String issueKey) {
+ this.issueKey = issueKey;
+ return this;
+ }
+
+ @SuppressWarnings("unchecked")
+ public FieldDiffs setDiff(String field, @Nullable Serializable oldValue, @Nullable Serializable newValue) {
+ Diff diff = diffs.get(field);
+ if (diff == null) {
+ diff = new Diff(oldValue, newValue);
+ diffs.put(field, diff);
+ } else {
+ diff.setNewValue(newValue);
+ }
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ boolean notFirst = false;
+ for (Map.Entry<String, Diff> entry : diffs.entrySet()) {
+ if (notFirst) {
+ sb.append(',');
+ } else {
+ notFirst = true;
+ }
+ sb.append(entry.getKey());
+ sb.append('=');
+ sb.append(entry.getValue().toString());
+ }
+ return sb.toString();
+ }
+
+ public static FieldDiffs parse(@Nullable String s) {
+ FieldDiffs diffs = new FieldDiffs();
+ if (!Strings.isNullOrEmpty(s)) {
+ Iterable<String> fields = FIELDS_SPLITTER.split(s);
+ for (String field : fields) {
+ String[] keyValues = field.split("=");
+ if (keyValues.length == 2) {
+ String[] values = keyValues[1].split("\\|");
+ String oldValue = "";
+ String newValue = "";
+ if(values.length == 1) {
+ newValue = Strings.nullToEmpty(values[0]);
+ } else if(values.length == 2) {
+ oldValue = Strings.nullToEmpty(values[0]);
+ newValue = Strings.nullToEmpty(values[1]);
+ }
+ diffs.setDiff(keyValues[0], oldValue, newValue);
+ } else {
+ diffs.setDiff(keyValues[0], "", "");
+ }
+ }
+ }
+ return diffs;
+ }
+
+ public static class Diff<T extends Serializable> implements Serializable {
+ private T oldValue;
+ private T newValue;
+
+ public Diff(@Nullable T oldValue, @Nullable T newValue) {
+ this.oldValue = oldValue;
+ this.newValue = newValue;
+ }
+
+ @CheckForNull
+ public T oldValue() {
+ return oldValue;
+ }
+
+ @CheckForNull
+ public Long oldValueLong() {
+ return toLong(oldValue);
+ }
+
+ @CheckForNull
+ public T newValue() {
+ return newValue;
+ }
+
+ @CheckForNull
+ public Long newValueLong() {
+ return toLong(newValue);
+ }
+
+ void setNewValue(T t) {
+ this.newValue = t;
+ }
+
+ @CheckForNull
+ private static Long toLong(@Nullable Serializable value) {
+ if (value != null && !"".equals(value)) {
+ try {
+ return Long.valueOf((String) value);
+ } catch (ClassCastException e) {
+ return (Long) value;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ //TODO escape , and | characters
+ StringBuilder sb = new StringBuilder();
+ if(newValue != null) {
+ if(oldValue != null) {
+ sb.append(oldValue.toString());
+ sb.append('|');
+ }
+ sb.append(newValue.toString());
+ }
+ return sb.toString();
+ }
+ }
+}
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/IssueChangeContext.java b/sonar-core/src/main/java/org/sonar/core/issue/IssueChangeContext.java
new file mode 100644
index 00000000000..1df5bd5e3eb
--- /dev/null
+++ b/sonar-core/src/main/java/org/sonar/core/issue/IssueChangeContext.java
@@ -0,0 +1,65 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.core.issue;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * PLUGINS MUST NOT BE USED THIS CLASS, EXCEPT FOR UNIT TESTING.
+ *
+ * @since 3.6
+ */
+public class IssueChangeContext implements Serializable {
+
+ private final String login;
+ private final Date date;
+ private final boolean scan;
+
+ private IssueChangeContext(@Nullable String login, Date date, boolean scan) {
+ this.login = login;
+ this.date = date;
+ this.scan = scan;
+ }
+
+ @CheckForNull
+ public String login() {
+ return login;
+ }
+
+ public Date date() {
+ return date;
+ }
+
+ public boolean scan() {
+ return scan;
+ }
+
+ public static IssueChangeContext createScan(Date date) {
+ return new IssueChangeContext(null, date, true);
+ }
+
+ public static IssueChangeContext createUser(Date date, @Nullable String login) {
+ return new IssueChangeContext(login, date, false);
+ }
+}
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/IssueUpdater.java b/sonar-core/src/main/java/org/sonar/core/issue/IssueUpdater.java
index e91e7bbdbb4..cb3bb6079aa 100644
--- a/sonar-core/src/main/java/org/sonar/core/issue/IssueUpdater.java
+++ b/sonar-core/src/main/java/org/sonar/core/issue/IssueUpdater.java
@@ -30,9 +30,6 @@ import org.apache.commons.lang.time.DateUtils;
import org.sonar.api.batch.BatchSide;
import org.sonar.api.server.ServerSide;
import org.sonar.api.issue.ActionPlan;
-import org.sonar.api.issue.internal.DefaultIssue;
-import org.sonar.api.issue.internal.DefaultIssueComment;
-import org.sonar.api.issue.internal.IssueChangeContext;
import org.sonar.api.server.rule.RuleTagFormat;
import org.sonar.api.user.User;
import org.sonar.api.utils.Duration;
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/db/IssueChangeDao.java b/sonar-core/src/main/java/org/sonar/core/issue/db/IssueChangeDao.java
index 1155e70c62d..a400a04c9cd 100644
--- a/sonar-core/src/main/java/org/sonar/core/issue/db/IssueChangeDao.java
+++ b/sonar-core/src/main/java/org/sonar/core/issue/db/IssueChangeDao.java
@@ -29,8 +29,8 @@ import java.util.Map;
import javax.annotation.CheckForNull;
import org.apache.ibatis.session.ResultHandler;
import org.sonar.api.batch.BatchSide;
-import org.sonar.api.issue.internal.DefaultIssueComment;
-import org.sonar.api.issue.internal.FieldDiffs;
+import org.sonar.core.issue.DefaultIssueComment;
+import org.sonar.core.issue.FieldDiffs;
import org.sonar.api.server.ServerSide;
import org.sonar.core.persistence.DaoComponent;
import org.sonar.core.persistence.DbSession;
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/db/IssueChangeDto.java b/sonar-core/src/main/java/org/sonar/core/issue/db/IssueChangeDto.java
index 18b731018f1..0d79cd47ca4 100644
--- a/sonar-core/src/main/java/org/sonar/core/issue/db/IssueChangeDto.java
+++ b/sonar-core/src/main/java/org/sonar/core/issue/db/IssueChangeDto.java
@@ -21,8 +21,8 @@ package org.sonar.core.issue.db;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
-import org.sonar.api.issue.internal.DefaultIssueComment;
-import org.sonar.api.issue.internal.FieldDiffs;
+import org.sonar.core.issue.DefaultIssueComment;
+import org.sonar.core.issue.FieldDiffs;
import org.sonar.api.utils.System2;
import javax.annotation.CheckForNull;
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/db/IssueDao.java b/sonar-core/src/main/java/org/sonar/core/issue/db/IssueDao.java
index 1a0208b7756..c4e2e516cf2 100644
--- a/sonar-core/src/main/java/org/sonar/core/issue/db/IssueDao.java
+++ b/sonar-core/src/main/java/org/sonar/core/issue/db/IssueDao.java
@@ -20,7 +20,6 @@
package org.sonar.core.issue.db;
-import javax.annotation.CheckForNull;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.SqlSession;
import org.sonar.api.batch.BatchSide;
@@ -41,16 +40,6 @@ public class IssueDao {
this.mybatis = mybatis;
}
- @CheckForNull
- public IssueDto selectByKey(String key) {
- DbSession session = mybatis.openSession(false);
- try {
- return mapper(session).selectByKey(key);
- } finally {
- MyBatis.closeQuietly(session);
- }
- }
-
public void selectNonClosedIssuesByModule(long componentId, ResultHandler handler) {
SqlSession session = mybatis.openSession(false);
try {
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/db/IssueDto.java b/sonar-core/src/main/java/org/sonar/core/issue/db/IssueDto.java
index 9b726f4b4e3..44baff06b8c 100644
--- a/sonar-core/src/main/java/org/sonar/core/issue/db/IssueDto.java
+++ b/sonar-core/src/main/java/org/sonar/core/issue/db/IssueDto.java
@@ -24,9 +24,10 @@ import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableSet;
+import java.util.Set;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
-import org.sonar.api.issue.internal.DefaultIssue;
+import org.sonar.core.issue.DefaultIssue;
import org.sonar.api.resources.Project;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.utils.Duration;
@@ -624,7 +625,7 @@ public final class IssueDto implements Serializable {
return this;
}
- public Collection<String> getTags() {
+ public Set<String> getTags() {
return ImmutableSet.copyOf(TAGS_SPLITTER.split(tags == null ? "" : tags));
}
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/db/IssueMapper.java b/sonar-core/src/main/java/org/sonar/core/issue/db/IssueMapper.java
index 569fa659449..aba2531071e 100644
--- a/sonar-core/src/main/java/org/sonar/core/issue/db/IssueMapper.java
+++ b/sonar-core/src/main/java/org/sonar/core/issue/db/IssueMapper.java
@@ -20,11 +20,16 @@
package org.sonar.core.issue.db;
import java.util.List;
+import java.util.Set;
public interface IssueMapper {
IssueDto selectByKey(String key);
+ List<IssueDto> selectOpenByComponentUuid(String componentUuid);
+
+ Set<String> selectComponentUuidsOfOpenIssuesForProjectUuid(String projectUuid);
+
List<IssueDto> selectByKeys(List<String> keys);
List<IssueDto> selectByActionPlan(String actionPlan);
@@ -34,5 +39,4 @@ public interface IssueMapper {
int update(IssueDto issue);
int updateIfBeforeSelectedDate(IssueDto issue);
-
}
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/db/IssueStorage.java b/sonar-core/src/main/java/org/sonar/core/issue/db/IssueStorage.java
index c4533c9bf0d..fb443d7cc9e 100644
--- a/sonar-core/src/main/java/org/sonar/core/issue/db/IssueStorage.java
+++ b/sonar-core/src/main/java/org/sonar/core/issue/db/IssueStorage.java
@@ -21,9 +21,9 @@ package org.sonar.core.issue.db;
import org.sonar.api.issue.Issue;
import org.sonar.api.issue.IssueComment;
-import org.sonar.api.issue.internal.DefaultIssue;
-import org.sonar.api.issue.internal.DefaultIssueComment;
-import org.sonar.api.issue.internal.FieldDiffs;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.core.issue.DefaultIssueComment;
+import org.sonar.core.issue.FieldDiffs;
import org.sonar.api.rules.Rule;
import org.sonar.api.rules.RuleFinder;
import org.sonar.core.persistence.BatchSession;
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/db/UpdateConflictResolver.java b/sonar-core/src/main/java/org/sonar/core/issue/db/UpdateConflictResolver.java
index 4fbd892d499..7c6b48a1603 100644
--- a/sonar-core/src/main/java/org/sonar/core/issue/db/UpdateConflictResolver.java
+++ b/sonar-core/src/main/java/org/sonar/core/issue/db/UpdateConflictResolver.java
@@ -20,17 +20,19 @@
package org.sonar.core.issue.db;
import com.google.common.annotations.VisibleForTesting;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.sonar.api.issue.internal.DefaultIssue;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.core.issue.DefaultIssue;
/**
* Support concurrent modifications on issues made by analysis and users at the same time
* See https://jira.sonarsource.com/browse/SONAR-4309
+ *
+ * TODO move to compute engine
*/
public class UpdateConflictResolver {
- private static final Logger LOG = LoggerFactory.getLogger(IssueStorage.class);
+ private static final Logger LOG = Loggers.get(UpdateConflictResolver.class);
public void resolve(DefaultIssue issue, IssueMapper mapper) {
LOG.debug("Resolve conflict on issue " + issue.key());
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/tracking/BlockHashSequence.java b/sonar-core/src/main/java/org/sonar/core/issue/tracking/BlockHashSequence.java
new file mode 100644
index 00000000000..7a4d38671ca
--- /dev/null
+++ b/sonar-core/src/main/java/org/sonar/core/issue/tracking/BlockHashSequence.java
@@ -0,0 +1,90 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.core.issue.tracking;
+
+import java.util.List;
+
+public class BlockHashSequence {
+
+ public static final int DEFAULT_HALF_BLOCK_SIZE = 5;
+ /**
+ * Hashes of blocks around lines. Line 1 is at index 0.
+ */
+ private final int[] blockHashes;
+
+ BlockHashSequence(LineHashSequence lineHashSequence, int halfBlockSize) {
+ this.blockHashes = new int[lineHashSequence.length()];
+
+ BlockHashFactory blockHashFactory = new BlockHashFactory(lineHashSequence.getHashes(), halfBlockSize);
+ for (int line = 1; line <= lineHashSequence.length(); line++) {
+ blockHashes[line - 1] = blockHashFactory.getHash();
+ if (line - halfBlockSize > 0) {
+ blockHashFactory.remove(lineHashSequence.getHashForLine(line - halfBlockSize).hashCode());
+ }
+ if (line + 1 + halfBlockSize <= lineHashSequence.length()) {
+ blockHashFactory.add(lineHashSequence.getHashForLine(line + 1 + halfBlockSize).hashCode());
+ } else {
+ blockHashFactory.add(0);
+ }
+ }
+ }
+
+ public static BlockHashSequence create(LineHashSequence lineHashSequence) {
+ return new BlockHashSequence(lineHashSequence, DEFAULT_HALF_BLOCK_SIZE);
+ }
+
+ /**
+ * Hash of block around line. Line must be in range of valid lines. It starts with 1.
+ */
+ public int getBlockHashForLine(int line) {
+ return blockHashes[line - 1];
+ }
+
+ private static class BlockHashFactory {
+ private static final int PRIME_BASE = 31;
+
+ private final int power;
+ private int hash = 0;
+
+ public BlockHashFactory(List<String> hashes, int halfBlockSize) {
+ int pow = 1;
+ for (int i = 0; i < halfBlockSize * 2; i++) {
+ pow = pow * PRIME_BASE;
+ }
+ this.power = pow;
+ for (int i = 1; i <= Math.min(hashes.size(), halfBlockSize + 1); i++) {
+ add(hashes.get(i - 1).hashCode());
+ }
+ }
+
+ public void add(int value) {
+ hash = hash * PRIME_BASE + value;
+ }
+
+ public void remove(int value) {
+ hash = hash - power * value;
+ }
+
+ public int getHash() {
+ return hash;
+ }
+
+ }
+}
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/tracking/BlockRecognizer.java b/sonar-core/src/main/java/org/sonar/core/issue/tracking/BlockRecognizer.java
new file mode 100644
index 00000000000..d4361819fbb
--- /dev/null
+++ b/sonar-core/src/main/java/org/sonar/core/issue/tracking/BlockRecognizer.java
@@ -0,0 +1,177 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.core.issue.tracking;
+
+import com.google.common.collect.LinkedHashMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Multimap;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+class BlockRecognizer<RAW extends Trackable, BASE extends Trackable> {
+
+ /**
+ * If base source code is available, then detect code moves through block hashes.
+ * Only the issues associated to a line can be matched here.
+ */
+ void match(Input<RAW> rawInput, Input<BASE> baseInput, Tracking<RAW, BASE> tracking) {
+ BlockHashSequence baseHashSequence = baseInput.getBlockHashSequence();
+ BlockHashSequence rawHashSequence = rawInput.getBlockHashSequence();
+
+ Multimap<Integer, RAW> rawsByLine = groupByLine(tracking.getUnmatchedRaws());
+ Multimap<Integer, BASE> basesByLine = groupByLine(tracking.getUnmatchedBases());
+ Map<Integer, HashOccurrence> map = new HashMap<>();
+
+ for (Integer line : basesByLine.keySet()) {
+ int hash = baseHashSequence.getBlockHashForLine(line);
+ HashOccurrence hashOccurrence = map.get(hash);
+ if (hashOccurrence == null) {
+ // first occurrence in base
+ hashOccurrence = new HashOccurrence();
+ hashOccurrence.baseLine = line;
+ hashOccurrence.baseCount = 1;
+ map.put(hash, hashOccurrence);
+ } else {
+ hashOccurrence.baseCount++;
+ }
+ }
+
+ for (Integer line : rawsByLine.keySet()) {
+ int hash = rawHashSequence.getBlockHashForLine(line);
+ HashOccurrence hashOccurrence = map.get(hash);
+ if (hashOccurrence != null) {
+ hashOccurrence.rawLine = line;
+ hashOccurrence.rawCount++;
+ }
+ }
+
+ for (HashOccurrence hashOccurrence : map.values()) {
+ if (hashOccurrence.baseCount == 1 && hashOccurrence.rawCount == 1) {
+ // Guaranteed that baseLine has been moved to rawLine, so we can map all issues on baseLine to all issues on rawLine
+ map(rawsByLine.get(hashOccurrence.rawLine), basesByLine.get(hashOccurrence.baseLine), tracking);
+ basesByLine.removeAll(hashOccurrence.baseLine);
+ rawsByLine.removeAll(hashOccurrence.rawLine);
+ }
+ }
+
+ // Check if remaining number of lines exceeds threshold
+ if (basesByLine.keySet().size() * rawsByLine.keySet().size() < 250000) {
+ List<LinePair> possibleLinePairs = Lists.newArrayList();
+ for (Integer baseLine : basesByLine.keySet()) {
+ for (Integer rawLine : rawsByLine.keySet()) {
+ int weight = lengthOfMaximalBlock(baseInput.getLineHashSequence(), baseLine, rawInput.getLineHashSequence(), rawLine);
+ possibleLinePairs.add(new LinePair(baseLine, rawLine, weight));
+ }
+ }
+ Collections.sort(possibleLinePairs, LinePairComparator.INSTANCE);
+ for (LinePair linePair : possibleLinePairs) {
+ // High probability that baseLine has been moved to rawLine, so we can map all Issues on baseLine to all Issues on rawLine
+ map(rawsByLine.get(linePair.rawLine), basesByLine.get(linePair.baseLine), tracking);
+ }
+ }
+ }
+
+ /**
+ * @param startLineA number of line from first version of text (numbering starts from 1)
+ * @param startLineB number of line from second version of text (numbering starts from 1)
+ */
+ static int lengthOfMaximalBlock(LineHashSequence hashesA, int startLineA, LineHashSequence hashesB, int startLineB) {
+ if (!hashesA.getHashForLine(startLineA).equals(hashesB.getHashForLine(startLineB))) {
+ return 0;
+ }
+ int length = 0;
+ int ai = startLineA;
+ int bi = startLineB;
+ while (ai <= hashesA.length() && bi <= hashesB.length() && hashesA.getHashForLine(ai).equals(hashesB.getHashForLine(bi))) {
+ ai++;
+ bi++;
+ length++;
+ }
+ ai = startLineA;
+ bi = startLineB;
+ while (ai > 0 && bi > 0 && hashesA.getHashForLine(ai).equals(hashesB.getHashForLine(bi))) {
+ ai--;
+ bi--;
+ length++;
+ }
+ // Note that position (startA, startB) was counted twice
+ return length - 1;
+ }
+
+ private void map(Collection<RAW> raws, Collection<BASE> bases, Tracking<RAW, BASE> result) {
+ for (RAW raw : raws) {
+ for (BASE base : bases) {
+ if (result.containsUnmatchedBase(base) && base.getRuleKey().equals(raw.getRuleKey())) {
+ result.associateRawToBase(raw, base);
+ result.markRawAsAssociated(raw);
+ break;
+ }
+ }
+ }
+ }
+
+ private static <T extends Trackable> Multimap<Integer, T> groupByLine(Collection<T> trackables) {
+ Multimap<Integer, T> result = LinkedHashMultimap.create();
+ for (T trackable : trackables) {
+ Integer line = trackable.getLine();
+ if (line != null) {
+ result.put(line, trackable);
+ }
+ }
+ return result;
+ }
+
+ private static class LinePair {
+ int baseLine;
+ int rawLine;
+ int weight;
+
+ public LinePair(int baseLine, int rawLine, int weight) {
+ this.baseLine = baseLine;
+ this.rawLine = rawLine;
+ this.weight = weight;
+ }
+ }
+
+ private static class HashOccurrence {
+ int baseLine;
+ int rawLine;
+ int baseCount;
+ int rawCount;
+ }
+
+ private enum LinePairComparator implements Comparator<LinePair> {
+ INSTANCE;
+
+ @Override
+ public int compare(LinePair o1, LinePair o2) {
+ int weightDiff = o2.weight - o1.weight;
+ if (weightDiff != 0) {
+ return weightDiff;
+ } else {
+ return Math.abs(o1.baseLine - o1.rawLine) - Math.abs(o2.baseLine - o2.rawLine);
+ }
+ }
+ }
+}
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/tracking/Input.java b/sonar-core/src/main/java/org/sonar/core/issue/tracking/Input.java
new file mode 100644
index 00000000000..b0681c67c94
--- /dev/null
+++ b/sonar-core/src/main/java/org/sonar/core/issue/tracking/Input.java
@@ -0,0 +1,30 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.core.issue.tracking;
+
+import java.util.Collection;
+
+public interface Input<ISSUE extends Trackable> {
+
+ LineHashSequence getLineHashSequence();
+ BlockHashSequence getBlockHashSequence();
+ Collection<ISSUE> getIssues();
+
+}
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/tracking/LazyInput.java b/sonar-core/src/main/java/org/sonar/core/issue/tracking/LazyInput.java
new file mode 100644
index 00000000000..0406c631f09
--- /dev/null
+++ b/sonar-core/src/main/java/org/sonar/core/issue/tracking/LazyInput.java
@@ -0,0 +1,58 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.core.issue.tracking;
+
+import java.util.Collection;
+import java.util.List;
+
+public abstract class LazyInput<ISSUE extends Trackable> implements Input<ISSUE> {
+
+ private List<ISSUE> issues;
+ private LineHashSequence lineHashSeq;
+ private BlockHashSequence blockHashSeq;
+
+ @Override
+ public LineHashSequence getLineHashSequence() {
+ if (lineHashSeq == null) {
+ lineHashSeq = LineHashSequence.createForLines(loadSourceLines());
+ }
+ return lineHashSeq;
+ }
+
+ @Override
+ public BlockHashSequence getBlockHashSequence() {
+ if (blockHashSeq == null) {
+ blockHashSeq = BlockHashSequence.create(getLineHashSequence());
+ }
+ return blockHashSeq;
+ }
+
+ @Override
+ public Collection<ISSUE> getIssues() {
+ if (issues == null) {
+ issues = loadIssues();
+ }
+ return issues;
+ }
+
+ protected abstract Iterable<String> loadSourceLines();
+
+ protected abstract List<ISSUE> loadIssues();
+}
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/tracking/LineHashSequence.java b/sonar-core/src/main/java/org/sonar/core/issue/tracking/LineHashSequence.java
new file mode 100644
index 00000000000..b6be6b731af
--- /dev/null
+++ b/sonar-core/src/main/java/org/sonar/core/issue/tracking/LineHashSequence.java
@@ -0,0 +1,119 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.core.issue.tracking;
+
+import com.google.common.base.Strings;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * Sequence of hash of lines for a given file
+ */
+public class LineHashSequence {
+
+ private static final int[] EMPTY_INTS = new int[0];
+
+ /**
+ * Hashes of lines. Line 1 is at index 0. No null elements.
+ */
+ private final List<String> hashes;
+ private final Map<String, int[]> linesByHash;
+
+ public LineHashSequence(List<String> hashes) {
+ this.hashes = hashes;
+ this.linesByHash = new HashMap<>(hashes.size());
+ for (int line = 1; line <= hashes.size(); line++) {
+ String hash = hashes.get(line - 1);
+ int[] lines = linesByHash.get(hash);
+ linesByHash.put(hash, appendLineTo(line, lines));
+ }
+ }
+
+ /**
+ * Number of lines
+ */
+ public int length() {
+ return hashes.size();
+ }
+
+ /**
+ * Checks if the line, starting with 1, is defined.
+ */
+ public boolean hasLine(int line) {
+ return 0 < line && line <= hashes.size();
+ }
+
+ /**
+ * The lines, starting with 1, that matches the given hash.
+ */
+ public int[] getLinesForHash(String hash) {
+ int[] lines = linesByHash.get(hash);
+ return lines == null ? EMPTY_INTS : lines;
+ }
+
+ /**
+ * Hash of the given line, which starts with 1. Return empty string
+ * is the line does not exist.
+ */
+ public String getHashForLine(int line) {
+ if (line > 0 && line <= hashes.size()) {
+ return Strings.nullToEmpty(hashes.get(line - 1));
+ }
+ return "";
+ }
+
+ List<String> getHashes() {
+ return hashes;
+ }
+
+ private static int[] appendLineTo(int line, @Nullable int[] to) {
+ int[] result;
+ if (to == null) {
+ result = new int[] {line};
+ } else {
+ result = new int[to.length + 1];
+ System.arraycopy(to, 0, result, 0, to.length);
+ result[result.length - 1] = line;
+ }
+ return result;
+ }
+
+ public static LineHashSequence createForLines(Iterable<String> lines) {
+ List<String> hashes = new ArrayList<>();
+ for (String line : lines) {
+ hashes.add(hash(line));
+ }
+ return new LineHashSequence(hashes);
+ }
+
+ // FIXME duplicates ComputeFileSourceData
+ private static String hash(String line) {
+ String reducedLine = StringUtils.replaceChars(line, "\t ", "");
+ if (reducedLine.isEmpty()) {
+ return "";
+ }
+ return DigestUtils.md5Hex(reducedLine);
+ }
+}
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/tracking/Trackable.java b/sonar-core/src/main/java/org/sonar/core/issue/tracking/Trackable.java
new file mode 100644
index 00000000000..c6e5512d19c
--- /dev/null
+++ b/sonar-core/src/main/java/org/sonar/core/issue/tracking/Trackable.java
@@ -0,0 +1,40 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.core.issue.tracking;
+
+import javax.annotation.CheckForNull;
+import org.sonar.api.rule.RuleKey;
+
+public interface Trackable {
+
+ /**
+ * The line index, starting with 1. Null means that
+ * issue does not relate to a line (file issue for example).
+ */
+ @CheckForNull
+ Integer getLine();
+
+ String getMessage();
+
+ @CheckForNull
+ String getLineHash();
+
+ RuleKey getRuleKey();
+}
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/tracking/Tracker.java b/sonar-core/src/main/java/org/sonar/core/issue/tracking/Tracker.java
new file mode 100644
index 00000000000..8691757440c
--- /dev/null
+++ b/sonar-core/src/main/java/org/sonar/core/issue/tracking/Tracker.java
@@ -0,0 +1,298 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.core.issue.tracking;
+
+import com.google.common.base.Predicate;
+import com.google.common.base.Strings;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Multimap;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Objects;
+import javax.annotation.Nonnull;
+import org.sonar.api.rule.RuleKey;
+
+import static com.google.common.collect.FluentIterable.from;
+
+public class Tracker<RAW extends Trackable, BASE extends Trackable> {
+
+ public Tracking<RAW, BASE> track(Input<RAW> rawInput, Input<BASE> baseInput) {
+ Tracking<RAW, BASE> tracking = new Tracking<>(rawInput, baseInput);
+
+ // 1. match issues with same rule, same line and same line hash, but not necessarily with same message
+ match(tracking, LineAndLineHashKeyFactory.INSTANCE);
+
+ // 2. detect code moves by comparing blocks of codes
+ detectCodeMoves(rawInput, baseInput, tracking);
+
+ // 3. match issues with same rule, same message and same line hash
+ match(tracking, LineHashAndMessagekeyFactory.INSTANCE);
+
+ // 4. match issues with same rule, same line and same message
+ match(tracking, LineAndMessageKeyFactory.INSTANCE);
+
+ // 5. match issues with same rule and same line hash but different line and different message.
+ // See SONAR-2812
+ match(tracking, LineHashKeyFactory.INSTANCE);
+
+ // TODO what about issues on line 0 ?
+ relocateManualIssues(rawInput, tracking);
+
+ return tracking;
+ }
+
+ private void detectCodeMoves(Input<RAW> rawInput, Input<BASE> baseInput, Tracking<RAW, BASE> tracking) {
+ if (!tracking.isComplete()) {
+ new BlockRecognizer<RAW, BASE>().match(rawInput, baseInput, tracking);
+ }
+ }
+
+ private void match(Tracking<RAW, BASE> tracking, SearchKeyFactory factory) {
+ if (tracking.isComplete()) {
+ return;
+ }
+
+ Multimap<SearchKey, BASE> baseSearch = ArrayListMultimap.create();
+ for (BASE base : tracking.getUnmatchedBases()) {
+ baseSearch.put(factory.create(base), base);
+ }
+
+ Collection<RAW> trackedRaws = new ArrayList<>();
+ for (RAW raw : tracking.getUnmatchedRaws()) {
+ SearchKey rawKey = factory.create(raw);
+ Collection<BASE> bases = baseSearch.get(rawKey);
+ if (!bases.isEmpty()) {
+ // TODO taking the first one. Could be improved if there are more than 2 issues on the same line.
+ // Message could be checked to take the best one.
+ BASE match = bases.iterator().next();
+ tracking.associateRawToBase(raw, match);
+ baseSearch.remove(rawKey, match);
+ trackedRaws.add(raw);
+ }
+ }
+ tracking.markRawsAsAssociated(trackedRaws);
+ }
+
+ private void relocateManualIssues(Input<RAW> rawInput, Tracking<RAW, BASE> tracking) {
+ Iterable<BASE> manualIssues = from(tracking.getUnmatchedBases()).filter(IsManual.INSTANCE);
+ for (BASE base : manualIssues) {
+ if (base.getLine() == null) {
+ // no need to relocate. Location is unchanged.
+ tracking.associateManualIssueToLine(base, 0);
+ } else {
+ String lineHash = base.getLineHash();
+ if (!Strings.isNullOrEmpty(lineHash)) {
+ int[] rawLines = rawInput.getLineHashSequence().getLinesForHash(lineHash);
+ if (rawLines.length == 1) {
+ tracking.associateManualIssueToLine(base, rawLines[0]);
+ } else if (rawLines.length == 0 && base.getLine() <= rawInput.getLineHashSequence().length()) {
+ // still valid (???). We didn't manage to correctly detect code move, so the
+ // issue is kept at the same location, even if code changes
+ tracking.associateManualIssueToLine(base, base.getLine());
+ }
+ }
+ }
+ }
+ }
+
+ private enum IsManual implements Predicate<Trackable> {
+ INSTANCE;
+ @Override
+ public boolean apply(Trackable input) {
+ return input.getRuleKey().isManual();
+ }
+ }
+
+ private interface SearchKey {
+ }
+
+ private interface SearchKeyFactory {
+ SearchKey create(Trackable trackable);
+ }
+
+ private static class LineAndLineHashKey implements SearchKey {
+ private final RuleKey ruleKey;
+ private final String lineHash;
+ private final Integer line;
+
+ LineAndLineHashKey(Trackable trackable) {
+ this.ruleKey = trackable.getRuleKey();
+ this.line = trackable.getLine();
+ this.lineHash = trackable.getLineHash();
+ }
+
+ @Override
+ public boolean equals(@Nonnull Object o) {
+ if (this == o) {
+ return true;
+ }
+ LineAndLineHashKey that = (LineAndLineHashKey) o;
+ // start with most discriminant field
+ if (!Objects.equals(line, that.line)) {
+ return false;
+ }
+ if (!lineHash.equals(that.lineHash)) {
+ return false;
+ }
+ return ruleKey.equals(that.ruleKey);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = ruleKey.hashCode();
+ result = 31 * result + lineHash.hashCode();
+ result = 31 * result + (line != null ? line.hashCode() : 0);
+ return result;
+ }
+ }
+
+ private enum LineAndLineHashKeyFactory implements SearchKeyFactory {
+ INSTANCE;
+ @Override
+ public SearchKey create(Trackable t) {
+ return new LineAndLineHashKey(t);
+ }
+ }
+
+ private static class LineHashAndMessageKey implements SearchKey {
+ private final RuleKey ruleKey;
+ private final String message, lineHash;
+
+ LineHashAndMessageKey(Trackable trackable) {
+ this.ruleKey = trackable.getRuleKey();
+ this.message = trackable.getMessage();
+ this.lineHash = trackable.getLineHash();
+ }
+
+ @Override
+ public boolean equals(@Nonnull Object o) {
+ if (this == o) {
+ return true;
+ }
+ LineHashAndMessageKey that = (LineHashAndMessageKey) o;
+ // start with most discriminant field
+ if (!lineHash.equals(that.lineHash)) {
+ return false;
+ }
+ if (!message.equals(that.message)) {
+ return false;
+ }
+ return ruleKey.equals(that.ruleKey);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = ruleKey.hashCode();
+ result = 31 * result + message.hashCode();
+ result = 31 * result + lineHash.hashCode();
+ return result;
+ }
+ }
+
+ private enum LineHashAndMessagekeyFactory implements SearchKeyFactory {
+ INSTANCE;
+ @Override
+ public SearchKey create(Trackable t) {
+ return new LineHashAndMessageKey(t);
+ }
+ }
+
+ private static class LineAndMessageKey implements SearchKey {
+ private final RuleKey ruleKey;
+ private final String message;
+ private final Integer line;
+
+ LineAndMessageKey(Trackable trackable) {
+ this.ruleKey = trackable.getRuleKey();
+ this.message = trackable.getMessage();
+ this.line = trackable.getLine();
+ }
+
+ @Override
+ public boolean equals(@Nonnull Object o) {
+ if (this == o) {
+ return true;
+ }
+ LineAndMessageKey that = (LineAndMessageKey) o;
+ // start with most discriminant field
+ if (!Objects.equals(line, that.line)) {
+ return false;
+ }
+ if (!message.equals(that.message)) {
+ return false;
+ }
+ return ruleKey.equals(that.ruleKey);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = ruleKey.hashCode();
+ result = 31 * result + message.hashCode();
+ result = 31 * result + (line != null ? line.hashCode() : 0);
+ return result;
+ }
+ }
+
+ private enum LineAndMessageKeyFactory implements SearchKeyFactory {
+ INSTANCE;
+ @Override
+ public SearchKey create(Trackable t) {
+ return new LineAndMessageKey(t);
+ }
+ }
+
+ private static class LineHashKey implements SearchKey {
+ private final RuleKey ruleKey;
+ private final String lineHash;
+
+ LineHashKey(Trackable trackable) {
+ this.ruleKey = trackable.getRuleKey();
+ this.lineHash = trackable.getLineHash();
+ }
+
+ @Override
+ public boolean equals(@Nonnull Object o) {
+ if (this == o) {
+ return true;
+ }
+ LineAndLineHashKey that = (LineAndLineHashKey) o;
+ // start with most discriminant field
+ if (!lineHash.equals(that.lineHash)) {
+ return false;
+ }
+ return ruleKey.equals(that.ruleKey);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = ruleKey.hashCode();
+ result = 31 * result + lineHash.hashCode();
+ return result;
+ }
+ }
+
+ private enum LineHashKeyFactory implements SearchKeyFactory {
+ INSTANCE;
+ @Override
+ public SearchKey create(Trackable t) {
+ return new LineHashKey(t);
+ }
+ }
+}
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/tracking/Tracking.java b/sonar-core/src/main/java/org/sonar/core/issue/tracking/Tracking.java
new file mode 100644
index 00000000000..27f34ac3887
--- /dev/null
+++ b/sonar-core/src/main/java/org/sonar/core/issue/tracking/Tracking.java
@@ -0,0 +1,116 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.core.issue.tracking;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Multimap;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.Set;
+import javax.annotation.CheckForNull;
+
+public class Tracking<RAW extends Trackable, BASE extends Trackable> {
+
+ /**
+ * Tracked issues -> a raw issue is associated to a base issue
+ */
+ private final IdentityHashMap<RAW, BASE> rawToBase = new IdentityHashMap<>();
+
+ /**
+ * The raw issues that are not associated to a base issue.
+ */
+ private final Set<RAW> unmatchedRaws = Collections.newSetFromMap(new IdentityHashMap<RAW, Boolean>());
+
+ /**
+ * IdentityHashSet of the base issues that are not associated to a raw issue.
+ */
+ private final Set<BASE> unmatchedBases = Collections.newSetFromMap(new IdentityHashMap<BASE, Boolean>());
+
+ /**
+ * The manual issues that are still valid (related source code still exists). They
+ * are grouped by line. Lines start with 1. The key 0 references the manual
+ * issues that do not relate to a line.
+ */
+ private final Multimap<Integer, BASE> openManualIssues = ArrayListMultimap.create();
+
+ public Tracking(Input<RAW> rawInput, Input<BASE> baseInput) {
+ for (RAW raw : rawInput.getIssues()) {
+ // Extra verification if some plugins create issues on wrong lines
+ Integer rawLine = raw.getLine();
+ if (rawLine != null && !rawInput.getLineHashSequence().hasLine(rawLine)) {
+ throw new IllegalArgumentException("Issue line is not valid: " + raw);
+ }
+ this.unmatchedRaws.add(raw);
+ }
+ this.unmatchedBases.addAll(baseInput.getIssues());
+ }
+
+ public Set<RAW> getUnmatchedRaws() {
+ return unmatchedRaws;
+ }
+
+ public Map<RAW, BASE> getMatchedRaws() {
+ return rawToBase;
+ }
+
+ @CheckForNull
+ public BASE baseFor(RAW raw) {
+ return rawToBase.get(raw);
+ }
+
+ /**
+ * The base issues that are not matched by a raw issue and that need to be closed. Manual
+ */
+ public Set<BASE> getUnmatchedBases() {
+ return unmatchedBases;
+ }
+
+ boolean containsUnmatchedBase(BASE base) {
+ return unmatchedBases.contains(base);
+ }
+
+ void associateRawToBase(RAW raw, BASE base) {
+ rawToBase.put(raw, base);
+ unmatchedBases.remove(base);
+ }
+
+ void markRawAsAssociated(RAW raw) {
+ unmatchedRaws.remove(raw);
+ }
+
+ void markRawsAsAssociated(Collection<RAW> c) {
+ unmatchedRaws.removeAll(c);
+ }
+
+ boolean isComplete() {
+ return unmatchedRaws.isEmpty() || unmatchedBases.isEmpty();
+ }
+
+ public Multimap<Integer, BASE> getOpenManualIssuesByLine() {
+ return openManualIssues;
+ }
+
+ void associateManualIssueToLine(BASE manualIssue, int line) {
+ openManualIssues.put(line, manualIssue);
+ unmatchedBases.remove(manualIssue);
+ }
+}
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/tracking/package-info.java b/sonar-core/src/main/java/org/sonar/core/issue/tracking/package-info.java
new file mode 100644
index 00000000000..1438e8aee5a
--- /dev/null
+++ b/sonar-core/src/main/java/org/sonar/core/issue/tracking/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.core.issue.tracking;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/workflow/FunctionExecutor.java b/sonar-core/src/main/java/org/sonar/core/issue/workflow/FunctionExecutor.java
index 51d12b7e36d..dfeac403458 100644
--- a/sonar-core/src/main/java/org/sonar/core/issue/workflow/FunctionExecutor.java
+++ b/sonar-core/src/main/java/org/sonar/core/issue/workflow/FunctionExecutor.java
@@ -20,10 +20,10 @@
package org.sonar.core.issue.workflow;
import org.sonar.api.batch.BatchSide;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.core.issue.IssueChangeContext;
import org.sonar.api.server.ServerSide;
import org.sonar.api.issue.Issue;
-import org.sonar.api.issue.internal.DefaultIssue;
-import org.sonar.api.issue.internal.IssueChangeContext;
import org.sonar.api.user.User;
import org.sonar.core.issue.IssueUpdater;
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/workflow/IsEndOfLife.java b/sonar-core/src/main/java/org/sonar/core/issue/workflow/IsBeingClosed.java
index e8d877dbd13..a9f4f210de0 100644
--- a/sonar-core/src/main/java/org/sonar/core/issue/workflow/IsEndOfLife.java
+++ b/sonar-core/src/main/java/org/sonar/core/issue/workflow/IsBeingClosed.java
@@ -21,18 +21,13 @@ package org.sonar.core.issue.workflow;
import org.sonar.api.issue.Issue;
import org.sonar.api.issue.condition.Condition;
-import org.sonar.api.issue.internal.DefaultIssue;
+import org.sonar.core.issue.DefaultIssue;
-class IsEndOfLife implements Condition {
-
- private final boolean endOfLife;
-
- IsEndOfLife(boolean endOfLife) {
- this.endOfLife = endOfLife;
- }
+enum IsBeingClosed implements Condition {
+ INSTANCE;
@Override
public boolean matches(Issue issue) {
- return ((DefaultIssue) issue).isEndOfLife() == endOfLife;
+ return ((DefaultIssue) issue).isBeingClosed();
}
}
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/workflow/IsManual.java b/sonar-core/src/main/java/org/sonar/core/issue/workflow/IsManual.java
index cd448ee8ce6..6bdda1fc117 100644
--- a/sonar-core/src/main/java/org/sonar/core/issue/workflow/IsManual.java
+++ b/sonar-core/src/main/java/org/sonar/core/issue/workflow/IsManual.java
@@ -22,16 +22,11 @@ package org.sonar.core.issue.workflow;
import org.sonar.api.issue.Issue;
import org.sonar.api.issue.condition.Condition;
-class IsManual implements Condition {
-
- private final boolean manual;
-
- IsManual(boolean manual) {
- this.manual = manual;
- }
+enum IsManual implements Condition {
+ INSTANCE;
@Override
public boolean matches(Issue issue) {
- return manual==(issue.reporter()!=null);
+ return issue.ruleKey().isManual();
}
}
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/workflow/IssueWorkflow.java b/sonar-core/src/main/java/org/sonar/core/issue/workflow/IssueWorkflow.java
index 5131a7def4d..5b54986c84f 100644
--- a/sonar-core/src/main/java/org/sonar/core/issue/workflow/IssueWorkflow.java
+++ b/sonar-core/src/main/java/org/sonar/core/issue/workflow/IssueWorkflow.java
@@ -19,19 +19,19 @@
*/
package org.sonar.core.issue.workflow;
+import java.util.List;
import org.picocontainer.Startable;
import org.sonar.api.batch.BatchSide;
-import org.sonar.api.server.ServerSide;
import org.sonar.api.issue.DefaultTransitions;
import org.sonar.api.issue.Issue;
import org.sonar.api.issue.condition.HasResolution;
-import org.sonar.api.issue.internal.DefaultIssue;
-import org.sonar.api.issue.internal.IssueChangeContext;
+import org.sonar.api.issue.condition.NotCondition;
+import org.sonar.api.server.ServerSide;
import org.sonar.api.web.UserRole;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.core.issue.IssueChangeContext;
import org.sonar.core.issue.IssueUpdater;
-import java.util.List;
-
@BatchSide
@ServerSide
public class IssueWorkflow implements Startable {
@@ -86,7 +86,7 @@ public class IssueWorkflow implements Startable {
.functions(new SetResolution(null))
.build())
.transition(Transition.builder(DefaultTransitions.REOPEN)
- .conditions(new IsManual(true))
+ .conditions(IsManual.INSTANCE)
.from(Issue.STATUS_CLOSED).to(Issue.STATUS_REOPENED)
.functions(new SetResolution(null), new SetCloseDate(false))
.build())
@@ -94,34 +94,34 @@ public class IssueWorkflow implements Startable {
// resolve as false-positive
.transition(Transition.builder(DefaultTransitions.FALSE_POSITIVE)
.from(Issue.STATUS_OPEN).to(Issue.STATUS_RESOLVED)
- .functions(new SetResolution(Issue.RESOLUTION_FALSE_POSITIVE), SetAssignee.UNASSIGN)
+ .functions(new SetResolution(Issue.RESOLUTION_FALSE_POSITIVE), UnsetAssignee.INSTANCE)
.requiredProjectPermission(UserRole.ISSUE_ADMIN)
.build())
.transition(Transition.builder(DefaultTransitions.FALSE_POSITIVE)
.from(Issue.STATUS_REOPENED).to(Issue.STATUS_RESOLVED)
- .functions(new SetResolution(Issue.RESOLUTION_FALSE_POSITIVE), SetAssignee.UNASSIGN)
+ .functions(new SetResolution(Issue.RESOLUTION_FALSE_POSITIVE), UnsetAssignee.INSTANCE)
.requiredProjectPermission(UserRole.ISSUE_ADMIN)
.build())
.transition(Transition.builder(DefaultTransitions.FALSE_POSITIVE)
.from(Issue.STATUS_CONFIRMED).to(Issue.STATUS_RESOLVED)
- .functions(new SetResolution(Issue.RESOLUTION_FALSE_POSITIVE), SetAssignee.UNASSIGN)
+ .functions(new SetResolution(Issue.RESOLUTION_FALSE_POSITIVE), UnsetAssignee.INSTANCE)
.requiredProjectPermission(UserRole.ISSUE_ADMIN)
.build())
// resolve as won't fix
.transition(Transition.builder(DefaultTransitions.WONT_FIX)
.from(Issue.STATUS_OPEN).to(Issue.STATUS_RESOLVED)
- .functions(new SetResolution(Issue.RESOLUTION_WONT_FIX), SetAssignee.UNASSIGN)
+ .functions(new SetResolution(Issue.RESOLUTION_WONT_FIX), UnsetAssignee.INSTANCE)
.requiredProjectPermission(UserRole.ISSUE_ADMIN)
.build())
.transition(Transition.builder(DefaultTransitions.WONT_FIX)
.from(Issue.STATUS_REOPENED).to(Issue.STATUS_RESOLVED)
- .functions(new SetResolution(Issue.RESOLUTION_WONT_FIX), SetAssignee.UNASSIGN)
+ .functions(new SetResolution(Issue.RESOLUTION_WONT_FIX), UnsetAssignee.INSTANCE)
.requiredProjectPermission(UserRole.ISSUE_ADMIN)
.build())
.transition(Transition.builder(DefaultTransitions.WONT_FIX)
.from(Issue.STATUS_CONFIRMED).to(Issue.STATUS_RESOLVED)
- .functions(new SetResolution(Issue.RESOLUTION_WONT_FIX), SetAssignee.UNASSIGN)
+ .functions(new SetResolution(Issue.RESOLUTION_WONT_FIX), UnsetAssignee.INSTANCE)
.requiredProjectPermission(UserRole.ISSUE_ADMIN)
.build()
);
@@ -130,33 +130,34 @@ public class IssueWorkflow implements Startable {
private void buildAutomaticTransitions(StateMachine.Builder builder) {
// Close the "end of life" issues (disabled/deleted rule, deleted component)
- builder.transition(Transition.builder("automaticclose")
- .from(Issue.STATUS_OPEN).to(Issue.STATUS_CLOSED)
- .conditions(new IsEndOfLife(true))
- .functions(new SetEndOfLife(), new SetCloseDate(true))
- .automatic()
- .build())
+ builder
+ .transition(Transition.builder("automaticclose")
+ .from(Issue.STATUS_OPEN).to(Issue.STATUS_CLOSED)
+ .conditions(IsBeingClosed.INSTANCE)
+ .functions(SetClosed.INSTANCE, new SetCloseDate(true))
+ .automatic()
+ .build())
.transition(Transition.builder("automaticclose")
.from(Issue.STATUS_REOPENED).to(Issue.STATUS_CLOSED)
- .conditions(new IsEndOfLife(true))
- .functions(new SetEndOfLife(), new SetCloseDate(true))
+ .conditions(IsBeingClosed.INSTANCE)
+ .functions(SetClosed.INSTANCE, new SetCloseDate(true))
.automatic()
.build())
.transition(Transition.builder("automaticclose")
.from(Issue.STATUS_CONFIRMED).to(Issue.STATUS_CLOSED)
- .conditions(new IsEndOfLife(true))
- .functions(new SetEndOfLife(), new SetCloseDate(true))
+ .conditions(IsBeingClosed.INSTANCE)
+ .functions(SetClosed.INSTANCE, new SetCloseDate(true))
.automatic()
.build())
.transition(Transition.builder("automaticclose")
.from(Issue.STATUS_RESOLVED).to(Issue.STATUS_CLOSED)
- .conditions(new IsEndOfLife(true))
- .functions(new SetEndOfLife(), new SetCloseDate(true))
+ .conditions(IsBeingClosed.INSTANCE)
+ .functions(SetClosed.INSTANCE, new SetCloseDate(true))
.automatic()
.build())
.transition(Transition.builder("automaticclosemanual")
.from(Issue.STATUS_RESOLVED).to(Issue.STATUS_CLOSED)
- .conditions(new IsEndOfLife(false), new IsManual(true))
+ .conditions(new NotCondition(IsBeingClosed.INSTANCE), IsManual.INSTANCE)
.functions(new SetCloseDate(true))
.automatic()
.build())
@@ -165,7 +166,7 @@ public class IssueWorkflow implements Startable {
// Manual issues are kept resolved.
.transition(Transition.builder("automaticreopen")
.from(Issue.STATUS_RESOLVED).to(Issue.STATUS_REOPENED)
- .conditions(new IsEndOfLife(false), new HasResolution(Issue.RESOLUTION_FIXED), new IsManual(false))
+ .conditions(new NotCondition(IsBeingClosed.INSTANCE), new HasResolution(Issue.RESOLUTION_FIXED), new NotCondition(IsManual.INSTANCE))
.functions(new SetResolution(null), new SetCloseDate(false))
.automatic()
.build()
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/workflow/SetCloseDate.java b/sonar-core/src/main/java/org/sonar/core/issue/workflow/SetCloseDate.java
index 3d83ac27c36..36da29e0c68 100644
--- a/sonar-core/src/main/java/org/sonar/core/issue/workflow/SetCloseDate.java
+++ b/sonar-core/src/main/java/org/sonar/core/issue/workflow/SetCloseDate.java
@@ -19,7 +19,7 @@
*/
package org.sonar.core.issue.workflow;
-public class SetCloseDate implements Function {
+class SetCloseDate implements Function {
private final boolean set;
public SetCloseDate(boolean set) {
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/workflow/SetEndOfLife.java b/sonar-core/src/main/java/org/sonar/core/issue/workflow/SetClosed.java
index ae45caa9390..cee06ac4181 100644
--- a/sonar-core/src/main/java/org/sonar/core/issue/workflow/SetEndOfLife.java
+++ b/sonar-core/src/main/java/org/sonar/core/issue/workflow/SetClosed.java
@@ -20,13 +20,15 @@
package org.sonar.core.issue.workflow;
import org.sonar.api.issue.Issue;
-import org.sonar.api.issue.internal.DefaultIssue;
+import org.sonar.core.issue.DefaultIssue;
+
+public enum SetClosed implements Function {
+ INSTANCE;
-public class SetEndOfLife implements Function {
@Override
public void execute(Context context) {
DefaultIssue issue = (DefaultIssue) context.issue();
- if (!issue.isEndOfLife()) {
+ if (!issue.isBeingClosed()) {
throw new IllegalStateException("Issue is still alive: " + issue);
}
if (issue.isOnDisabledRule()) {
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/workflow/SetAssignee.java b/sonar-core/src/main/java/org/sonar/core/issue/workflow/UnsetAssignee.java
index 7a5c778ce61..b7f9da103d8 100644
--- a/sonar-core/src/main/java/org/sonar/core/issue/workflow/SetAssignee.java
+++ b/sonar-core/src/main/java/org/sonar/core/issue/workflow/UnsetAssignee.java
@@ -19,21 +19,11 @@
*/
package org.sonar.core.issue.workflow;
-import org.sonar.api.user.User;
-
-import javax.annotation.Nullable;
-
-public class SetAssignee implements Function {
- public static final SetAssignee UNASSIGN = new SetAssignee(null);
-
- private final User assignee;
-
- public SetAssignee(@Nullable User assignee) {
- this.assignee = assignee;
- }
+enum UnsetAssignee implements Function {
+ INSTANCE;
@Override
public void execute(Context context) {
- context.setAssignee(assignee);
+ context.setAssignee(null);
}
}
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/workflow/UnsetLine.java b/sonar-core/src/main/java/org/sonar/core/issue/workflow/UnsetLine.java
new file mode 100644
index 00000000000..8073e179280
--- /dev/null
+++ b/sonar-core/src/main/java/org/sonar/core/issue/workflow/UnsetLine.java
@@ -0,0 +1,29 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.core.issue.workflow;
+
+enum UnsetLine implements Function {
+ INSTANCE;
+ @Override
+ public void execute(Context context) {
+ context.setLine(null);
+ }
+
+}
diff --git a/sonar-core/src/main/resources/org/sonar/core/issue/db/IssueMapper.xml b/sonar-core/src/main/resources/org/sonar/core/issue/db/IssueMapper.xml
index c1bfead8323..50eb94db4d3 100644
--- a/sonar-core/src/main/resources/org/sonar/core/issue/db/IssueMapper.xml
+++ b/sonar-core/src/main/resources/org/sonar/core/issue/db/IssueMapper.xml
@@ -148,7 +148,42 @@
where i.kee=#{kee}
</select>
- <select id="selectNonClosedIssuesByModule" parameterType="Long" resultType="Issue">
+ <select id="selectOpenByComponentUuid" parameterType="String" resultType="Issue">
+ select
+ i.id,
+ i.kee as kee,
+ i.rule_id as ruleId,
+ i.action_plan_key as actionPlanKey,
+ i.severity as severity,
+ i.manual_severity as manualSeverity,
+ i.message as message,
+ i.line as line,
+ i.effort_to_fix as effortToFix,
+ i.technical_debt as debt,
+ i.status as status,
+ i.resolution as resolution,
+ i.checksum as checksum,
+ i.reporter as reporter,
+ i.assignee as assignee,
+ i.author_login as authorLogin,
+ i.tags as tagsString,
+ i.issue_attributes as issueAttributes,
+ i.issue_creation_date as issueCreationTime,
+ i.issue_update_date as issueUpdateTime,
+ i.issue_close_date as issueCloseTime,
+ i.created_at as createdAt,
+ i.updated_at as updatedAt,
+ r.plugin_rule_key as ruleKey,
+ i.component_uuid as componentUuid,
+ i.project_uuid as projectUuid
+ from issues i
+ inner join rules r on r.id=i.rule_id
+ where
+ i.component_uuid=#{componentUuid} and
+ i.status &lt;&gt; 'CLOSED'
+ </select>
+
+ <select id="selectNonClosedIssuesByModule" parameterType="long" resultType="Issue">
select
i.id,
i.kee as kee,
@@ -186,6 +221,12 @@
where i.status &lt;&gt; 'CLOSED'
</select>
+ <select id="selectComponentUuidsOfOpenIssuesForProjectUuid" parameterType="string" resultType="string">
+ select distinct(i.component_uuid)
+ from issues i
+ where i.project_uuid=#{projectUuid} and i.status &lt;&gt; 'CLOSED'
+ </select>
+
<select id="selectByKeys" parameterType="map" resultType="Issue">
select
<include refid="issueColumns"/>
diff --git a/sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueBuilderTest.java b/sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueBuilderTest.java
index a84e1111836..136c5a262b5 100644
--- a/sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueBuilderTest.java
+++ b/sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueBuilderTest.java
@@ -21,7 +21,6 @@ package org.sonar.core.issue;
import org.junit.Test;
import org.sonar.api.issue.Issue;
-import org.sonar.api.issue.internal.DefaultIssue;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.rule.Severity;
diff --git a/sonar-core/src/test/java/org/sonar/core/issue/IssueChangeContextTest.java b/sonar-core/src/test/java/org/sonar/core/issue/IssueChangeContextTest.java
index d6e304a309d..ed424565037 100644
--- a/sonar-core/src/test/java/org/sonar/core/issue/IssueChangeContextTest.java
+++ b/sonar-core/src/test/java/org/sonar/core/issue/IssueChangeContextTest.java
@@ -20,7 +20,6 @@
package org.sonar.core.issue;
import org.junit.Test;
-import org.sonar.api.issue.internal.IssueChangeContext;
import java.util.Date;
diff --git a/sonar-core/src/test/java/org/sonar/core/issue/IssueUpdaterTest.java b/sonar-core/src/test/java/org/sonar/core/issue/IssueUpdaterTest.java
index 8a4181f47b5..c3d927cbe3c 100644
--- a/sonar-core/src/test/java/org/sonar/core/issue/IssueUpdaterTest.java
+++ b/sonar-core/src/test/java/org/sonar/core/issue/IssueUpdaterTest.java
@@ -22,9 +22,6 @@ package org.sonar.core.issue;
import org.junit.Before;
import org.junit.Test;
import org.sonar.api.issue.ActionPlan;
-import org.sonar.api.issue.internal.DefaultIssue;
-import org.sonar.api.issue.internal.FieldDiffs;
-import org.sonar.api.issue.internal.IssueChangeContext;
import org.sonar.api.user.User;
import org.sonar.api.utils.Duration;
import org.sonar.core.user.DefaultUser;
diff --git a/sonar-core/src/test/java/org/sonar/core/issue/db/IssueChangeDaoTest.java b/sonar-core/src/test/java/org/sonar/core/issue/db/IssueChangeDaoTest.java
index d9de4aa528f..411048b421e 100644
--- a/sonar-core/src/test/java/org/sonar/core/issue/db/IssueChangeDaoTest.java
+++ b/sonar-core/src/test/java/org/sonar/core/issue/db/IssueChangeDaoTest.java
@@ -23,8 +23,8 @@ import org.apache.ibatis.executor.result.DefaultResultHandler;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
-import org.sonar.api.issue.internal.DefaultIssueComment;
-import org.sonar.api.issue.internal.FieldDiffs;
+import org.sonar.core.issue.DefaultIssueComment;
+import org.sonar.core.issue.FieldDiffs;
import org.sonar.api.utils.DateUtils;
import org.sonar.core.persistence.AbstractDaoTestCase;
import org.sonar.core.persistence.DbSession;
diff --git a/sonar-core/src/test/java/org/sonar/core/issue/db/IssueChangeDtoTest.java b/sonar-core/src/test/java/org/sonar/core/issue/db/IssueChangeDtoTest.java
index 3b383c1c974..2d3ece83f18 100644
--- a/sonar-core/src/test/java/org/sonar/core/issue/db/IssueChangeDtoTest.java
+++ b/sonar-core/src/test/java/org/sonar/core/issue/db/IssueChangeDtoTest.java
@@ -20,8 +20,8 @@
package org.sonar.core.issue.db;
import org.junit.Test;
-import org.sonar.api.issue.internal.DefaultIssueComment;
-import org.sonar.api.issue.internal.FieldDiffs;
+import org.sonar.core.issue.DefaultIssueComment;
+import org.sonar.core.issue.FieldDiffs;
import org.sonar.api.utils.System2;
import static org.assertj.core.api.Assertions.assertThat;
diff --git a/sonar-core/src/test/java/org/sonar/core/issue/db/IssueDaoTest.java b/sonar-core/src/test/java/org/sonar/core/issue/db/IssueDaoTest.java
index 7958aa26674..7738ca20486 100644
--- a/sonar-core/src/test/java/org/sonar/core/issue/db/IssueDaoTest.java
+++ b/sonar-core/src/test/java/org/sonar/core/issue/db/IssueDaoTest.java
@@ -47,41 +47,6 @@ public class IssueDaoTest extends AbstractDaoTestCase {
}
@Test
- public void should_select_by_key() {
- setupData("shared", "should_select_by_key");
-
- IssueDto issue = dao.selectByKey("ABCDE");
- assertThat(issue.getKee()).isEqualTo("ABCDE");
- assertThat(issue.getId()).isEqualTo(100L);
- assertThat(issue.getRuleId()).isEqualTo(500);
- assertThat(issue.getSeverity()).isEqualTo("BLOCKER");
- assertThat(issue.isManualSeverity()).isFalse();
- assertThat(issue.getMessage()).isNull();
- assertThat(issue.getLine()).isEqualTo(200);
- assertThat(issue.getEffortToFix()).isEqualTo(4.2);
- assertThat(issue.getStatus()).isEqualTo("OPEN");
- assertThat(issue.getResolution()).isEqualTo("FIXED");
- assertThat(issue.getChecksum()).isEqualTo("XXX");
- assertThat(issue.getAuthorLogin()).isEqualTo("karadoc");
- assertThat(issue.getReporter()).isEqualTo("arthur");
- assertThat(issue.getAssignee()).isEqualTo("perceval");
- assertThat(issue.getIssueAttributes()).isEqualTo("JIRA=FOO-1234");
- assertThat(issue.getIssueCreationDate()).isNotNull();
- assertThat(issue.getIssueUpdateDate()).isNotNull();
- assertThat(issue.getIssueCloseDate()).isNotNull();
- assertThat(issue.getCreatedAt()).isNotNull();
- assertThat(issue.getUpdatedAt()).isNotNull();
- assertThat(issue.getRuleRepo()).isEqualTo("squid");
- assertThat(issue.getRule()).isEqualTo("AvoidCycle");
- assertThat(issue.getComponentUuid()).isEqualTo("CDEF");
- assertThat(issue.getComponentKey()).isEqualTo("Action.java");
- assertThat(issue.getModuleUuid()).isEqualTo("BCDE");
- assertThat(issue.getModuleUuidPath()).isEqualTo(".ABCD.BCDE.");
- assertThat(issue.getProjectKey()).isEqualTo("struts"); // ABCD
- assertThat(issue.getProjectUuid()).isEqualTo("ABCD"); // null
- }
-
- @Test
public void select_non_closed_issues_by_module() {
setupData("shared", "should_select_non_closed_issues_by_module");
diff --git a/sonar-core/src/test/java/org/sonar/core/issue/db/IssueDtoTest.java b/sonar-core/src/test/java/org/sonar/core/issue/db/IssueDtoTest.java
index edac8a2dd84..0ee3ae23416 100644
--- a/sonar-core/src/test/java/org/sonar/core/issue/db/IssueDtoTest.java
+++ b/sonar-core/src/test/java/org/sonar/core/issue/db/IssueDtoTest.java
@@ -24,7 +24,7 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.sonar.api.issue.Issue;
-import org.sonar.api.issue.internal.DefaultIssue;
+import org.sonar.core.issue.DefaultIssue;
import org.sonar.api.utils.Duration;
import org.sonar.core.rule.RuleDto;
diff --git a/sonar-core/src/test/java/org/sonar/core/issue/db/IssueStorageTest.java b/sonar-core/src/test/java/org/sonar/core/issue/db/IssueStorageTest.java
index 62e10059629..259f751c12e 100644
--- a/sonar-core/src/test/java/org/sonar/core/issue/db/IssueStorageTest.java
+++ b/sonar-core/src/test/java/org/sonar/core/issue/db/IssueStorageTest.java
@@ -22,9 +22,9 @@ package org.sonar.core.issue.db;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
-import org.sonar.api.issue.internal.DefaultIssue;
-import org.sonar.api.issue.internal.DefaultIssueComment;
-import org.sonar.api.issue.internal.IssueChangeContext;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.core.issue.DefaultIssueComment;
+import org.sonar.core.issue.IssueChangeContext;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.rules.Rule;
import org.sonar.api.rules.RuleFinder;
diff --git a/sonar-core/src/test/java/org/sonar/core/issue/db/UpdateConflictResolverTest.java b/sonar-core/src/test/java/org/sonar/core/issue/db/UpdateConflictResolverTest.java
index 2dc79d30b8d..9a4d5037901 100644
--- a/sonar-core/src/test/java/org/sonar/core/issue/db/UpdateConflictResolverTest.java
+++ b/sonar-core/src/test/java/org/sonar/core/issue/db/UpdateConflictResolverTest.java
@@ -22,7 +22,7 @@ package org.sonar.core.issue.db;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.sonar.api.issue.Issue;
-import org.sonar.api.issue.internal.DefaultIssue;
+import org.sonar.core.issue.DefaultIssue;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.rule.Severity;
import org.sonar.api.utils.DateUtils;
diff --git a/sonar-core/src/test/java/org/sonar/core/issue/tracking/BlockHashSequenceTest.java b/sonar-core/src/test/java/org/sonar/core/issue/tracking/BlockHashSequenceTest.java
new file mode 100644
index 00000000000..f3646150846
--- /dev/null
+++ b/sonar-core/src/test/java/org/sonar/core/issue/tracking/BlockHashSequenceTest.java
@@ -0,0 +1,42 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.core.issue.tracking;
+
+import org.junit.Test;
+
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class BlockHashSequenceTest {
+
+ @Test
+ public void test() {
+ BlockHashSequence a = new BlockHashSequence(LineHashSequence.createForLines(asList("line0", "line1", "line2")), 1);
+ BlockHashSequence b = new BlockHashSequence(LineHashSequence.createForLines(asList("line0", "line1", "line2", "line3")), 1);
+
+ assertThat(a.getBlockHashForLine(1)).isEqualTo(b.getBlockHashForLine(1));
+ assertThat(a.getBlockHashForLine(2)).isEqualTo(b.getBlockHashForLine(2));
+ assertThat(a.getBlockHashForLine(3)).isNotEqualTo(b.getBlockHashForLine(3));
+
+ BlockHashSequence c = new BlockHashSequence(LineHashSequence.createForLines(asList("line-1", "line0", "line1", "line2", "line3")), 1);
+ assertThat(a.getBlockHashForLine(1)).isNotEqualTo(c.getBlockHashForLine(2));
+ assertThat(a.getBlockHashForLine(2)).isEqualTo(c.getBlockHashForLine(3));
+ }
+}
diff --git a/sonar-core/src/test/java/org/sonar/core/issue/tracking/BlockRecognizerTest.java b/sonar-core/src/test/java/org/sonar/core/issue/tracking/BlockRecognizerTest.java
new file mode 100644
index 00000000000..d265e70d687
--- /dev/null
+++ b/sonar-core/src/test/java/org/sonar/core/issue/tracking/BlockRecognizerTest.java
@@ -0,0 +1,56 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.core.issue.tracking;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class BlockRecognizerTest {
+
+ @Test
+ public void lengthOfMaximalBlock() {
+ /**
+ * - line 4 of first sequence is "d"
+ * - line 4 of second sequence is "d"
+ * - in each sequence, the 3 lines before and the line after are similar -> block size is 5
+ */
+ assertThat(compute(seq("abcde"), seq("abcde"), 4, 4)).isEqualTo(5);
+
+ assertThat(compute(seq("abcde"), seq("abcd"), 4, 4)).isEqualTo(4);
+ assertThat(compute(seq("bcde"), seq("abcde"), 4, 4)).isEqualTo(0);
+ assertThat(compute(seq("bcde"), seq("abcde"), 3, 4)).isEqualTo(4);
+ }
+
+ private int compute(LineHashSequence seqA, LineHashSequence seqB, int ai, int bi) {
+ return BlockRecognizer.lengthOfMaximalBlock(seqA, ai, seqB, bi);
+ }
+
+ private static LineHashSequence seq(String text) {
+ List<String> hashes = new ArrayList<>();
+ for (int i = 0; i < text.length(); i++) {
+ hashes.add("" + text.charAt(i));
+ }
+ return new LineHashSequence(hashes);
+ }
+
+}
diff --git a/sonar-core/src/test/java/org/sonar/core/issue/tracking/TrackerTest.java b/sonar-core/src/test/java/org/sonar/core/issue/tracking/TrackerTest.java
new file mode 100644
index 00000000000..37391225a84
--- /dev/null
+++ b/sonar-core/src/test/java/org/sonar/core/issue/tracking/TrackerTest.java
@@ -0,0 +1,456 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.core.issue.tracking;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.rule.RuleKey;
+
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class TrackerTest {
+
+ public static final RuleKey RULE_SYSTEM_PRINT = RuleKey.of("java", "SystemPrint");
+ public static final RuleKey RULE_UNUSED_LOCAL_VARIABLE = RuleKey.of("java", "UnusedLocalVariable");
+ public static final RuleKey RULE_UNUSED_PRIVATE_METHOD = RuleKey.of("java", "UnusedPrivateMethod");
+ public static final RuleKey RULE_NOT_DESIGNED_FOR_EXTENSION = RuleKey.of("java", "NotDesignedForExtension");
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ Tracker<Issue, Issue> tracker = new Tracker<>();
+
+ /**
+ * Of course rule must match
+ */
+ @Test
+ public void similar_issues_except_rule_do_not_match() {
+ FakeInput baseInput = new FakeInput("H1");
+ baseInput.createIssueOnLine(1, RULE_SYSTEM_PRINT, "msg");
+
+ FakeInput rawInput = new FakeInput("H1");
+ Issue raw = rawInput.createIssueOnLine(1, RULE_UNUSED_LOCAL_VARIABLE, "msg");
+
+ Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput);
+ assertThat(tracking.baseFor(raw)).isNull();
+ }
+
+ @Test
+ @Ignore
+ public void different_issues_do_not_match() {
+ FakeInput baseInput = new FakeInput("H1");
+ Issue base = baseInput.createIssueOnLine(1, RULE_SYSTEM_PRINT, "msg1");
+
+ FakeInput rawInput = new FakeInput("H2", "H3", "H4", "H5", "H6");
+ Issue raw = rawInput.createIssueOnLine(5, RULE_SYSTEM_PRINT, "msg2");
+
+ Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput);
+ assertThat(tracking.baseFor(raw)).isNull();
+ assertThat(tracking.getUnmatchedBases()).containsOnly(base);
+ }
+
+ @Test
+ public void line_hash_has_greater_priority_than_line() {
+ FakeInput baseInput = new FakeInput("H1", "H2", "H3");
+ Issue base1 = baseInput.createIssueOnLine(1, RULE_SYSTEM_PRINT, "msg");
+ Issue base2 = baseInput.createIssueOnLine(3, RULE_SYSTEM_PRINT, "msg");
+
+ FakeInput rawInput = new FakeInput("a", "b", "H1", "H2", "H3");
+ Issue raw1 = rawInput.createIssueOnLine(3, RULE_SYSTEM_PRINT, "msg");
+ Issue raw2 = rawInput.createIssueOnLine(5, RULE_SYSTEM_PRINT, "msg");
+
+ Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput);
+ assertThat(tracking.baseFor(raw1)).isSameAs(base1);
+ assertThat(tracking.baseFor(raw2)).isSameAs(base2);
+ }
+
+ /**
+ * SONAR-2928
+ */
+ @Test
+ public void no_lines_and_different_messages_match() {
+ FakeInput baseInput = new FakeInput("H1", "H2", "H3");
+ Issue base = baseInput.createIssue(RULE_SYSTEM_PRINT, "msg1");
+
+ FakeInput rawInput = new FakeInput("H10", "H11", "H12");
+ Issue raw = rawInput.createIssue(RULE_SYSTEM_PRINT, "msg2");
+
+ Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput);
+ assertThat(tracking.baseFor(raw)).isSameAs(base);
+ }
+
+ @Test
+ public void similar_issues_except_message_match() {
+ FakeInput baseInput = new FakeInput("H1");
+ Issue base = baseInput.createIssueOnLine(1, RULE_SYSTEM_PRINT, "msg1");
+
+ FakeInput rawInput = new FakeInput("H1");
+ Issue raw = rawInput.createIssueOnLine(1, RULE_SYSTEM_PRINT, "msg2");
+
+ Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput);
+ assertThat(tracking.baseFor(raw)).isSameAs(base);
+ }
+
+ @Test
+ public void similar_issues_if_trimmed_messages_match() {
+ FakeInput baseInput = new FakeInput("H1");
+ Issue base = baseInput.createIssueOnLine(1, RULE_SYSTEM_PRINT, " message ");
+
+ FakeInput rawInput = new FakeInput("H2");
+ Issue raw = rawInput.createIssueOnLine(1, RULE_SYSTEM_PRINT, "message");
+
+ Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput);
+ assertThat(tracking.baseFor(raw)).isSameAs(base);
+ }
+
+ /**
+ * Source code of this line was changed, but line and message still match
+ */
+ @Test
+ public void similar_issues_except_line_hash_match() {
+ FakeInput baseInput = new FakeInput("H1");
+ Issue base = baseInput.createIssueOnLine(1, RULE_SYSTEM_PRINT, "msg");
+
+ FakeInput rawInput = new FakeInput("H2");
+ Issue raw = rawInput.createIssueOnLine(1, RULE_SYSTEM_PRINT, "msg");
+
+ Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput);
+ assertThat(tracking.baseFor(raw)).isSameAs(base);
+ }
+
+ @Test
+ public void similar_issues_except_line_match() {
+ FakeInput baseInput = new FakeInput("H1", "H2");
+ Issue base = baseInput.createIssueOnLine(1, RULE_SYSTEM_PRINT, "msg");
+
+ FakeInput rawInput = new FakeInput("H2", "H1");
+ Issue raw = rawInput.createIssueOnLine(2, RULE_SYSTEM_PRINT, "msg");
+
+ Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput);
+ assertThat(tracking.baseFor(raw)).isSameAs(base);
+ }
+
+ /**
+ * SONAR-2812
+ */
+ @Test
+ public void only_same_line_hash_match_match() {
+ FakeInput baseInput = new FakeInput("H1", "H2");
+ Issue base = baseInput.createIssueOnLine(1, RULE_SYSTEM_PRINT, "msg");
+
+ FakeInput rawInput = new FakeInput("H3", "H4", "H1");
+ Issue raw = rawInput.createIssueOnLine(3, RULE_SYSTEM_PRINT, "other message");
+
+ Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput);
+ assertThat(tracking.baseFor(raw)).isSameAs(base);
+ }
+
+ @Test
+ public void do_not_fail_if_base_issue_without_line() throws Exception {
+ FakeInput baseInput = new FakeInput("H1", "H2");
+ Issue base = baseInput.createIssueOnLine(1, RULE_SYSTEM_PRINT, "msg1");
+
+ FakeInput rawInput = new FakeInput("H3", "H4", "H5");
+ Issue raw = rawInput.createIssue(RULE_UNUSED_LOCAL_VARIABLE, "msg2");
+
+ Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput);
+ assertThat(tracking.baseFor(raw)).isNull();
+ assertThat(tracking.getUnmatchedBases()).containsOnly(base);
+ }
+
+ @Test
+ public void do_not_fail_if_raw_issue_without_line() throws Exception {
+ FakeInput baseInput = new FakeInput("H1", "H2");
+ Issue base = baseInput.createIssue(RULE_SYSTEM_PRINT, "msg1");
+
+ FakeInput rawInput = new FakeInput("H3", "H4", "H5");
+ Issue raw = rawInput.createIssueOnLine(1, RULE_UNUSED_LOCAL_VARIABLE, "msg2");
+
+ Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput);
+ assertThat(tracking.baseFor(raw)).isNull();
+ assertThat(tracking.getUnmatchedBases()).containsOnly(base);
+ }
+
+ @Test
+ public void fail_if_raw_line_does_not_exist() throws Exception {
+ FakeInput baseInput = new FakeInput();
+ FakeInput rawInput = new FakeInput("H1").addIssue(new Issue(200, "H200", RULE_SYSTEM_PRINT, "msg"));
+
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("Issue line is not valid");
+ tracker.track(rawInput, baseInput);
+ }
+
+ /**
+ * SONAR-3072
+ */
+ @Test
+ public void recognize_blocks_1() throws Exception {
+ FakeInput baseInput = FakeInput.createForSourceLines(
+ "package example1;",
+ "",
+ "public class Toto {",
+ "",
+ " public void doSomething() {",
+ " // doSomething",
+ " }",
+ "",
+ " public void doSomethingElse() {",
+ " // doSomethingElse",
+ " }",
+ "}"
+ );
+ Issue base1 = baseInput.createIssueOnLine(7, RULE_SYSTEM_PRINT, "Indentation");
+ Issue base2 = baseInput.createIssueOnLine(11, RULE_SYSTEM_PRINT, "Indentation");
+
+ FakeInput rawInput = FakeInput.createForSourceLines(
+ "package example1;",
+ "",
+ "public class Toto {",
+ "",
+ " public Toto(){}",
+ "",
+ " public void doSomethingNew() {",
+ " // doSomethingNew",
+ " }",
+ "",
+ " public void doSomethingElseNew() {",
+ " // doSomethingElseNew",
+ " }",
+ "",
+ " public void doSomething() {",
+ " // doSomething",
+ " }",
+ "",
+ " public void doSomethingElse() {",
+ " // doSomethingElse",
+ " }",
+ "}"
+ );
+ Issue raw1 = rawInput.createIssueOnLine(9, RULE_SYSTEM_PRINT, "Indentation");
+ Issue raw2 = rawInput.createIssueOnLine(13, RULE_SYSTEM_PRINT, "Indentation");
+ Issue raw3 = rawInput.createIssueOnLine(17, RULE_SYSTEM_PRINT, "Indentation");
+ Issue raw4 = rawInput.createIssueOnLine(21, RULE_SYSTEM_PRINT, "Indentation");
+
+ Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput);
+ assertThat(tracking.baseFor(raw1)).isNull();
+ assertThat(tracking.baseFor(raw2)).isNull();
+ assertThat(tracking.baseFor(raw3)).isSameAs(base1);
+ assertThat(tracking.baseFor(raw4)).isSameAs(base2);
+ assertThat(tracking.getUnmatchedBases()).isEmpty();
+ }
+
+ /**
+ * SONAR-3072
+ */
+ @Test
+ public void recognize_blocks_2() throws Exception {
+ FakeInput baseInput = FakeInput.createForSourceLines(
+ "package example2;",
+ "",
+ "public class Toto {",
+ " void method1() {",
+ " System.out.println(\"toto\");",
+ " }",
+ "}"
+ );
+ Issue base1 = baseInput.createIssueOnLine(5, RULE_SYSTEM_PRINT, "SystemPrintln");
+
+ FakeInput rawInput = FakeInput.createForSourceLines(
+ "package example2;",
+ "",
+ "public class Toto {",
+ "",
+ " void method2() {",
+ " System.out.println(\"toto\");",
+ " }",
+ "",
+ " void method1() {",
+ " System.out.println(\"toto\");",
+ " }",
+ "",
+ " void method3() {",
+ " System.out.println(\"toto\");",
+ " }",
+ "}"
+ );
+ Issue raw1 = rawInput.createIssueOnLine(6, RULE_SYSTEM_PRINT, "SystemPrintln");
+ Issue raw2 = rawInput.createIssueOnLine(10, RULE_SYSTEM_PRINT, "SystemPrintln");
+ Issue raw3 = rawInput.createIssueOnLine(14, RULE_SYSTEM_PRINT, "SystemPrintln");
+
+ Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput);
+ assertThat(tracking.baseFor(raw1)).isNull();
+ assertThat(tracking.baseFor(raw2)).isSameAs(base1);
+ assertThat(tracking.baseFor(raw3)).isNull();
+ }
+
+ @Test
+ public void recognize_blocks_3() throws Exception {
+ FakeInput baseInput = FakeInput.createForSourceLines(
+ "package sample;",
+ "",
+ "public class Sample {",
+ "\t",
+ "\tpublic Sample(int i) {",
+ "\t\tint j = i+1;", // UnusedLocalVariable
+ "\t}",
+ "",
+ "\tpublic boolean avoidUtilityClass() {", // NotDesignedForExtension
+ "\t\treturn true;",
+ "\t}",
+ "",
+ "\tprivate String myMethod() {", // UnusedPrivateMethod
+ "\t\treturn \"hello\";",
+ "\t}",
+ "}"
+ );
+ Issue base1 = baseInput.createIssueOnLine(6, RULE_UNUSED_LOCAL_VARIABLE, "Avoid unused local variables such as 'j'.");
+ Issue base2 = baseInput.createIssueOnLine(13, RULE_UNUSED_PRIVATE_METHOD, "Avoid unused private methods such as 'myMethod()'.");
+ Issue base3 = baseInput.createIssueOnLine(9, RULE_NOT_DESIGNED_FOR_EXTENSION,
+ "Method 'avoidUtilityClass' is not designed for extension - needs to be abstract, final or empty.");
+
+ FakeInput rawInput = FakeInput.createForSourceLines(
+ "package sample;",
+ "",
+ "public class Sample {",
+ "",
+ "\tpublic Sample(int i) {",
+ "\t\tint j = i+1;", // UnusedLocalVariable is still there
+ "\t}",
+ "\t",
+ "\tpublic boolean avoidUtilityClass() {", // NotDesignedForExtension is still there
+ "\t\treturn true;",
+ "\t}",
+ "\t",
+ "\tprivate String myMethod() {", // issue UnusedPrivateMethod is fixed because it's called at line 18
+ "\t\treturn \"hello\";",
+ "\t}",
+ "",
+ " public void newIssue() {",
+ " String msg = myMethod();", // new issue UnusedLocalVariable
+ " }",
+ "}"
+ );
+
+ Issue newRaw = rawInput.createIssueOnLine(18, RULE_UNUSED_LOCAL_VARIABLE, "Avoid unused local variables such as 'msg'.");
+ Issue rawSameAsBase1 = rawInput.createIssueOnLine(6, RULE_UNUSED_LOCAL_VARIABLE, "Avoid unused local variables such as 'j'.");
+ Issue rawSameAsBase3 = rawInput.createIssueOnLine(9, RULE_NOT_DESIGNED_FOR_EXTENSION,
+ "Method 'avoidUtilityClass' is not designed for extension - needs to be abstract, final or empty.");
+
+ Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput);
+
+ assertThat(tracking.baseFor(newRaw)).isNull();
+ assertThat(tracking.baseFor(rawSameAsBase1)).isSameAs(base1);
+ assertThat(tracking.baseFor(rawSameAsBase3)).isSameAs(base3);
+ assertThat(tracking.getUnmatchedBases()).containsOnly(base2);
+ }
+
+ private static class Issue implements Trackable {
+ private final RuleKey ruleKey;
+ private final Integer line;
+ private final String message, lineHash;
+
+ Issue(@Nullable Integer line, String lineHash, RuleKey ruleKey, String message) {
+ this.line = line;
+ this.lineHash = lineHash;
+ this.ruleKey = ruleKey;
+ this.message = message;
+ }
+
+ @Override
+ public Integer getLine() {
+ return line;
+ }
+
+ @Override
+ public String getMessage() {
+ return message;
+ }
+
+ @Override
+ public String getLineHash() {
+ return lineHash;
+ }
+
+ @Override
+ public RuleKey getRuleKey() {
+ return ruleKey;
+ }
+ }
+
+ private static class FakeInput implements Input<Issue> {
+ private final List<Issue> issues = new ArrayList<>();
+ private final List<String> lineHashes;
+
+ public FakeInput(String... lineHashes) {
+ this.lineHashes = asList(lineHashes);
+ }
+
+ static FakeInput createForSourceLines(String... lines) {
+ String[] hashes = new String[lines.length];
+ for (int i = 0; i < lines.length; i++) {
+ hashes[i] = DigestUtils.md5Hex(lines[i].replaceAll("[\t ]", ""));
+ }
+ return new FakeInput(hashes);
+ }
+
+ public Issue createIssueOnLine(int line, RuleKey ruleKey, String message) {
+ Issue issue = new Issue(line, lineHashes.get(line - 1), ruleKey, message);
+ issues.add(issue);
+ return issue;
+ }
+
+ /**
+ * No line (line 0)
+ */
+ public Issue createIssue(RuleKey ruleKey, String message) {
+ Issue issue = new Issue(null, "", ruleKey, message);
+ issues.add(issue);
+ return issue;
+ }
+
+ public FakeInput addIssue(Issue issue) {
+ this.issues.add(issue);
+ return this;
+ }
+
+ @Override
+ public LineHashSequence getLineHashSequence() {
+ return new LineHashSequence(lineHashes);
+ }
+
+ @Override
+ public BlockHashSequence getBlockHashSequence() {
+ return new BlockHashSequence(getLineHashSequence(), 2);
+ }
+
+ @Override
+ public Collection<Issue> getIssues() {
+ return issues;
+ }
+ }
+}
diff --git a/sonar-core/src/test/java/org/sonar/core/issue/workflow/IsEndOfLifeTest.java b/sonar-core/src/test/java/org/sonar/core/issue/workflow/IsBeingClosedTest.java
index 47ca6b3d526..04e6c29304e 100644
--- a/sonar-core/src/test/java/org/sonar/core/issue/workflow/IsEndOfLifeTest.java
+++ b/sonar-core/src/test/java/org/sonar/core/issue/workflow/IsBeingClosedTest.java
@@ -20,24 +20,19 @@
package org.sonar.core.issue.workflow;
import org.junit.Test;
-import org.sonar.api.issue.internal.DefaultIssue;
+import org.sonar.core.issue.DefaultIssue;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.core.issue.workflow.IsBeingClosed.INSTANCE;
+
+public class IsBeingClosedTest {
-public class IsEndOfLifeTest {
- DefaultIssue issue = new DefaultIssue();
@Test
public void should_be_end_of_life() {
- IsEndOfLife condition = new IsEndOfLife(true);
- assertThat(condition.matches(issue.setEndOfLife(true))).isTrue();
- assertThat(condition.matches(issue.setEndOfLife(false))).isFalse();
+ DefaultIssue issue = new DefaultIssue();
+ assertThat(INSTANCE.matches(issue.setBeingClosed(true))).isTrue();
+ assertThat(INSTANCE.matches(issue.setBeingClosed(false))).isFalse();
}
- @Test
- public void should_not_be_end_of_life() {
- IsEndOfLife condition = new IsEndOfLife(false);
- assertThat(condition.matches(issue.setEndOfLife(true))).isFalse();
- assertThat(condition.matches(issue.setEndOfLife(false))).isTrue();
- }
}
diff --git a/sonar-core/src/test/java/org/sonar/core/issue/workflow/IsManualTest.java b/sonar-core/src/test/java/org/sonar/core/issue/workflow/IsManualTest.java
index 16a82274f04..78a581387c7 100644
--- a/sonar-core/src/test/java/org/sonar/core/issue/workflow/IsManualTest.java
+++ b/sonar-core/src/test/java/org/sonar/core/issue/workflow/IsManualTest.java
@@ -20,24 +20,19 @@
package org.sonar.core.issue.workflow;
import org.junit.Test;
-import org.sonar.api.issue.internal.DefaultIssue;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.core.issue.DefaultIssue;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.core.issue.workflow.IsManual.INSTANCE;
public class IsManualTest {
- DefaultIssue issue = new DefaultIssue();
@Test
public void should_match() {
- IsManual condition = new IsManual(true);
- assertThat(condition.matches(issue.setReporter("you"))).isTrue();
- assertThat(condition.matches(issue.setReporter(null))).isFalse();
+ DefaultIssue issue = new DefaultIssue();
+ assertThat(INSTANCE.matches(issue.setRuleKey(RuleKey.of(RuleKey.MANUAL_REPOSITORY_KEY, "R1")))).isTrue();
+ assertThat(INSTANCE.matches(issue.setRuleKey(RuleKey.of("java", "R1")))).isFalse();
}
- @Test
- public void should_match_dead() {
- IsManual condition = new IsManual(false);
- assertThat(condition.matches(issue.setReporter("you"))).isFalse();
- assertThat(condition.matches(issue.setReporter(null))).isTrue();
- }
}
diff --git a/sonar-core/src/test/java/org/sonar/core/issue/workflow/IssueWorkflowTest.java b/sonar-core/src/test/java/org/sonar/core/issue/workflow/IssueWorkflowTest.java
index 8cc01a72189..ad5b9bef6e8 100644
--- a/sonar-core/src/test/java/org/sonar/core/issue/workflow/IssueWorkflowTest.java
+++ b/sonar-core/src/test/java/org/sonar/core/issue/workflow/IssueWorkflowTest.java
@@ -25,8 +25,8 @@ import org.apache.commons.lang.time.DateUtils;
import org.junit.Test;
import org.sonar.api.issue.DefaultTransitions;
import org.sonar.api.issue.Issue;
-import org.sonar.api.issue.internal.DefaultIssue;
-import org.sonar.api.issue.internal.IssueChangeContext;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.core.issue.IssueChangeContext;
import org.sonar.api.rule.RuleKey;
import org.sonar.core.issue.IssueUpdater;
@@ -106,7 +106,7 @@ public class IssueWorkflowTest {
public void list_no_out_transition_from_status_closed() {
workflow.start();
- DefaultIssue issue = new DefaultIssue().setStatus(Issue.STATUS_CLOSED);
+ DefaultIssue issue = new DefaultIssue().setStatus(Issue.STATUS_CLOSED).setRuleKey(RuleKey.of("java", "R1 "));
List<Transition> transitions = workflow.outTransitions(issue);
assertThat(transitions).isEmpty();
}
@@ -149,7 +149,7 @@ public class IssueWorkflowTest {
.setResolution(Issue.RESOLUTION_FIXED)
.setStatus(Issue.STATUS_RESOLVED)
.setNew(false)
- .setEndOfLife(true);
+ .setBeingClosed(true);
Date now = new Date();
workflow.doAutomaticTransition(issue, IssueChangeContext.createScan(now));
assertThat(issue.resolution()).isEqualTo(Issue.RESOLUTION_FIXED);
@@ -167,7 +167,7 @@ public class IssueWorkflowTest {
.setResolution(null)
.setStatus(Issue.STATUS_OPEN)
.setNew(false)
- .setEndOfLife(true);
+ .setBeingClosed(true);
Date now = new Date();
workflow.doAutomaticTransition(issue, IssueChangeContext.createScan(now));
assertThat(issue.resolution()).isEqualTo(Issue.RESOLUTION_FIXED);
@@ -185,7 +185,7 @@ public class IssueWorkflowTest {
.setResolution(null)
.setStatus(Issue.STATUS_REOPENED)
.setNew(false)
- .setEndOfLife(true);
+ .setBeingClosed(true);
Date now = new Date();
workflow.doAutomaticTransition(issue, IssueChangeContext.createScan(now));
assertThat(issue.resolution()).isEqualTo(Issue.RESOLUTION_FIXED);
@@ -203,7 +203,7 @@ public class IssueWorkflowTest {
.setResolution(null)
.setStatus(Issue.STATUS_CONFIRMED)
.setNew(false)
- .setEndOfLife(true);
+ .setBeingClosed(true);
Date now = new Date();
workflow.doAutomaticTransition(issue, IssueChangeContext.createScan(now));
assertThat(issue.resolution()).isEqualTo(Issue.RESOLUTION_FIXED);
@@ -222,7 +222,7 @@ public class IssueWorkflowTest {
.setResolution(Issue.RESOLUTION_FIXED)
.setStatus("xxx")
.setNew(false)
- .setEndOfLife(true);
+ .setBeingClosed(true);
try {
workflow.doAutomaticTransition(issue, IssueChangeContext.createScan(new Date()));
fail();
@@ -346,7 +346,7 @@ public class IssueWorkflowTest {
.setStatus(Issue.STATUS_OPEN)
.setRuleKey(RuleKey.of("manual", "Performance"))
.setReporter("simon")
- .setEndOfLife(true)
+ .setBeingClosed(true)
.setOnDisabledRule(true);
workflow.start();
@@ -364,7 +364,7 @@ public class IssueWorkflowTest {
.setStatus(Issue.STATUS_OPEN)
.setRuleKey(RuleKey.of("manual", "Performance"))
.setReporter("simon")
- .setEndOfLife(true)
+ .setBeingClosed(true)
.setOnDisabledRule(false);
workflow.start();
diff --git a/sonar-core/src/test/java/org/sonar/core/issue/workflow/SetEndOfLifeTest.java b/sonar-core/src/test/java/org/sonar/core/issue/workflow/SetClosedTest.java
index 014a8c2d087..84e46786292 100644
--- a/sonar-core/src/test/java/org/sonar/core/issue/workflow/SetEndOfLifeTest.java
+++ b/sonar-core/src/test/java/org/sonar/core/issue/workflow/SetClosedTest.java
@@ -21,39 +21,39 @@ package org.sonar.core.issue.workflow;
import org.junit.Test;
import org.sonar.api.issue.Issue;
-import org.sonar.api.issue.internal.DefaultIssue;
+import org.sonar.core.issue.DefaultIssue;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.*;
+import static org.sonar.core.issue.workflow.SetClosed.INSTANCE;
-public class SetEndOfLifeTest {
+public class SetClosedTest {
Function.Context context = mock(Function.Context.class);
- SetEndOfLife function = new SetEndOfLife();
@Test
public void should_resolve_as_fixed() {
- Issue issue = new DefaultIssue().setEndOfLife(true).setOnDisabledRule(false);
+ Issue issue = new DefaultIssue().setBeingClosed(true).setOnDisabledRule(false);
when(context.issue()).thenReturn(issue);
- function.execute(context);
+ INSTANCE.execute(context);
verify(context, times(1)).setResolution(Issue.RESOLUTION_FIXED);
}
@Test
public void should_resolve_as_removed_when_rule_is_disabled() {
- Issue issue = new DefaultIssue().setEndOfLife(true).setOnDisabledRule(true);
+ Issue issue = new DefaultIssue().setBeingClosed(true).setOnDisabledRule(true);
when(context.issue()).thenReturn(issue);
- function.execute(context);
+ INSTANCE.execute(context);
verify(context, times(1)).setResolution(Issue.RESOLUTION_REMOVED);
}
@Test
public void should_fail_if_issue_is_not_resolved() {
- Issue issue = new DefaultIssue().setEndOfLife(false);
+ Issue issue = new DefaultIssue().setBeingClosed(false);
when(context.issue()).thenReturn(issue);
try {
- function.execute(context);
+ INSTANCE.execute(context);
fail();
} catch (IllegalStateException e) {
assertThat(e.getMessage()).contains("Issue is still alive");
@@ -63,9 +63,9 @@ public class SetEndOfLifeTest {
@Test
public void line_number_must_be_unset() {
- Issue issue = new DefaultIssue().setEndOfLife(true).setLine(10);
+ Issue issue = new DefaultIssue().setBeingClosed(true).setLine(10);
when(context.issue()).thenReturn(issue);
- function.execute(context);
+ INSTANCE.execute(context);
verify(context).setLine(null);
}
}
diff --git a/sonar-core/src/test/java/org/sonar/core/issue/workflow/TransitionTest.java b/sonar-core/src/test/java/org/sonar/core/issue/workflow/TransitionTest.java
index 4c680c54ec1..eab956c6858 100644
--- a/sonar-core/src/test/java/org/sonar/core/issue/workflow/TransitionTest.java
+++ b/sonar-core/src/test/java/org/sonar/core/issue/workflow/TransitionTest.java
@@ -21,7 +21,7 @@ package org.sonar.core.issue.workflow;
import org.junit.Test;
import org.sonar.api.issue.condition.Condition;
-import org.sonar.api.issue.internal.DefaultIssue;
+import org.sonar.core.issue.DefaultIssue;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
diff --git a/sonar-core/src/test/java/org/sonar/core/issue/workflow/SetAssigneeTest.java b/sonar-core/src/test/java/org/sonar/core/issue/workflow/UnsetAssigneeTest.java
index ebadb4bccd9..3458baa77dd 100644
--- a/sonar-core/src/test/java/org/sonar/core/issue/workflow/SetAssigneeTest.java
+++ b/sonar-core/src/test/java/org/sonar/core/issue/workflow/UnsetAssigneeTest.java
@@ -21,25 +21,18 @@
package org.sonar.core.issue.workflow;
import org.junit.Test;
-import org.sonar.api.user.User;
-import org.sonar.core.user.DefaultUser;
-import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.sonar.core.issue.workflow.UnsetAssignee.INSTANCE;
-public class SetAssigneeTest {
- @Test
- public void assign() {
- User user = new DefaultUser().setLogin("eric").setName("eric");
- SetAssignee function = new SetAssignee(user);
- Function.Context context = mock(Function.Context.class);
- function.execute(context);
- verify(context, times(1)).setAssignee(user);
- }
+public class UnsetAssigneeTest {
@Test
public void unassign() {
Function.Context context = mock(Function.Context.class);
- SetAssignee.UNASSIGN.execute(context);
+ INSTANCE.execute(context);
verify(context, times(1)).setAssignee(null);
}
}
diff --git a/sonar-core/src/test/resources/org/sonar/core/issue/db/IssueDaoTest/should_select_by_key.xml b/sonar-core/src/test/resources/org/sonar/core/issue/db/IssueDaoTest/should_select_by_key.xml
deleted file mode 100644
index 080269cef46..00000000000
--- a/sonar-core/src/test/resources/org/sonar/core/issue/db/IssueDaoTest/should_select_by_key.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<dataset>
-
- <issues
- id="100"
- kee="ABCDE"
- component_uuid="CDEF"
- project_uuid="ABCD"
- rule_id="500"
- severity="BLOCKER"
- manual_severity="[false]"
- message="[null]"
- line="200"
- effort_to_fix="4.2"
- status="OPEN"
- resolution="FIXED"
- checksum="XXX"
- reporter="arthur"
- assignee="perceval"
- author_login="karadoc"
- issue_attributes="JIRA=FOO-1234"
- issue_creation_date="1366063200000"
- issue_update_date="1366063200000"
- issue_close_date="1366063200000"
- created_at="1400000000000"
- updated_at="1400000000000"
- />
-
-</dataset>