diff options
author | Simon Brandhof <simon.brandhof@gmail.com> | 2013-06-04 19:04:44 +0200 |
---|---|---|
committer | Simon Brandhof <simon.brandhof@gmail.com> | 2013-06-04 19:05:21 +0200 |
commit | b3070fe1c1fc8c15cf8ba5e0433fec598572bdc4 (patch) | |
tree | c8fc47d90d1d8d034c07cad214fa2e70d55ba67d | |
parent | 01809e26ca0408b39a6153a57c5509e0df98350e (diff) | |
download | sonarqube-b3070fe1c1fc8c15cf8ba5e0433fec598572bdc4.tar.gz sonarqube-b3070fe1c1fc8c15cf8ba5e0433fec598572bdc4.zip |
SONAR-4309 Support concurrent modifications on issues made by batch and simultaneously by user
13 files changed, 464 insertions, 19 deletions
diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/InitialOpenIssuesSensor.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/InitialOpenIssuesSensor.java index 9796c39293b..cff5ec73080 100644 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/InitialOpenIssuesSensor.java +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/InitialOpenIssuesSensor.java @@ -19,6 +19,7 @@ */ package org.sonar.plugins.core.issue; +import org.apache.commons.lang.time.DateUtils; import org.apache.ibatis.session.ResultContext; import org.apache.ibatis.session.ResultHandler; import org.sonar.api.batch.Sensor; @@ -27,6 +28,9 @@ import org.sonar.api.resources.Project; import org.sonar.core.issue.db.IssueDao; import org.sonar.core.issue.db.IssueDto; +import java.util.Calendar; +import java.util.Date; + /** * Load all the issues referenced during the previous scan. */ @@ -47,10 +51,15 @@ public class InitialOpenIssuesSensor implements Sensor { @Override public void analyse(Project project, SensorContext context) { + // Adding one second is a hack for resolving conflicts with concurrent user + // changes during issue persistence + final Date now = DateUtils.addSeconds(DateUtils.truncate(new Date(), Calendar.MILLISECOND), 1); + issueDao.selectNonClosedIssuesByModule(project.getId(), new ResultHandler() { @Override public void handleResult(ResultContext rc) { IssueDto dto = (IssueDto) rc.getResultObject(); + dto.setSelectedAt(now); initialOpenIssuesStack.addIssue(dto); } }); diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/IssueTrackingDecorator.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/IssueTrackingDecorator.java index 1728af0ffd3..539dd16224f 100644 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/IssueTrackingDecorator.java +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/IssueTrackingDecorator.java @@ -133,6 +133,7 @@ public class IssueTrackingDecorator implements Decorator { issue.setNew(false); issue.setEndOfLife(false); issue.setOnDisabledRule(false); + issue.setSelectedAt(ref.getSelectedAt()); // fields to update with old values issue.setActionPlanKey(ref.getActionPlanKey()); 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 412d4deb26f..b0d87a27c9b 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 @@ -29,7 +29,6 @@ import org.sonar.api.utils.KeyValueFormat; import javax.annotation.CheckForNull; import javax.annotation.Nullable; - import java.io.Serializable; import java.util.Date; @@ -66,6 +65,11 @@ public final class IssueDto implements Serializable { private Date createdAt; private Date updatedAt; + /** + * Temporary date used only during scan + */ + private Date selectedAt; + // joins private String ruleKey; private String ruleRepo; @@ -305,6 +309,16 @@ public final class IssueDto implements Serializable { return rootComponentKey; } + @CheckForNull + public Date getSelectedAt() { + return selectedAt; + } + + public IssueDto setSelectedAt(Date d) { + this.selectedAt = d; + return this; + } + /** * Only for unit tests */ @@ -357,7 +371,7 @@ public final class IssueDto implements Serializable { .setIssueCreationDate(issue.creationDate()) .setIssueCloseDate(issue.closeDate()) .setIssueUpdateDate(issue.updateDate()) - + .setSelectedAt(issue.selectedAt()) .setCreatedAt(now) .setUpdatedAt(now); } @@ -382,6 +396,7 @@ public final class IssueDto implements Serializable { .setIssueCreationDate(issue.creationDate()) .setIssueCloseDate(issue.closeDate()) .setIssueUpdateDate(issue.updateDate()) + .setSelectedAt(issue.selectedAt()) .setUpdatedAt(now); } @@ -407,6 +422,7 @@ public final class IssueDto implements Serializable { issue.setCreationDate(issueCreationDate); issue.setCloseDate(issueCloseDate); issue.setUpdateDate(issueUpdateDate); + issue.setSelectedAt(selectedAt); return issue; } } 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 d0f81df029d..42c9b091340 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 @@ -40,4 +40,5 @@ 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 b8287a83655..bf4050312c0 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 @@ -19,7 +19,6 @@ */ package org.sonar.core.issue.db; -import com.google.common.collect.Lists; import org.apache.ibatis.session.SqlSession; import org.sonar.api.issue.Issue; import org.sonar.api.issue.IssueComment; @@ -32,12 +31,12 @@ import org.sonar.core.persistence.MyBatis; import java.util.Arrays; import java.util.Date; -import java.util.List; public abstract class IssueStorage { private final MyBatis mybatis; private final RuleFinder ruleFinder; + private final UpdateConflictResolver conflictResolver = new UpdateConflictResolver(); protected IssueStorage(MyBatis mybatis, RuleFinder ruleFinder) { this.mybatis = mybatis; @@ -49,37 +48,50 @@ public abstract class IssueStorage { } public void save(Iterable<DefaultIssue> issues) { - SqlSession session = mybatis.openBatchSession(); + SqlSession session = mybatis.openSession(); IssueMapper issueMapper = session.getMapper(IssueMapper.class); IssueChangeMapper issueChangeMapper = session.getMapper(IssueChangeMapper.class); Date now = new Date(); try { - List<DefaultIssue> conflicts = Lists.newArrayList(); for (DefaultIssue issue : issues) { if (issue.isNew()) { - long componentId = componentId(issue); - long projectId = projectId(issue); - int ruleId = ruleId(issue); - IssueDto dto = IssueDto.toDtoForInsert(issue, componentId, projectId, ruleId, now); - issueMapper.insert(dto); - + insert(issueMapper, now, issue); } else if (issue.isChanged()) { - IssueDto dto = IssueDto.toDtoForUpdate(issue, now); - int count = issueMapper.update(dto); - if (count < 1) { - conflicts.add(issue); - } + update(issueMapper, now, issue); } insertChanges(issueChangeMapper, issue); } session.commit(); - // TODO log and fix conflicts } finally { MyBatis.closeQuietly(session); } } + private void insert(IssueMapper issueMapper, Date now, DefaultIssue issue) { + long componentId = componentId(issue); + long projectId = projectId(issue); + int ruleId = ruleId(issue); + IssueDto dto = IssueDto.toDtoForInsert(issue, componentId, projectId, ruleId, now); + issueMapper.insert(dto); + } + + private void update(IssueMapper issueMapper, Date now, DefaultIssue issue) { + IssueDto dto = IssueDto.toDtoForUpdate(issue, now); + if (Issue.STATUS_CLOSED.equals(issue.status()) || issue.selectedAt() == null) { + // Issue is closed by scan or changed by end-user + issueMapper.update(dto); + + } else { + int count = issueMapper.updateIfBeforeSelectedDate(dto); + if (count == 0) { + // End-user and scan changed the issue at the same time. + // See https://jira.codehaus.org/browse/SONAR-4309 + conflictResolver.resolve(issue, issueMapper); + } + } + } + private void insertChanges(IssueChangeMapper mapper, DefaultIssue issue) { for (IssueComment comment : issue.comments()) { DefaultIssueComment c = (DefaultIssueComment) comment; 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 new file mode 100644 index 00000000000..5ebe79bf908 --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/issue/db/UpdateConflictResolver.java @@ -0,0 +1,85 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 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.db; + +import com.google.common.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.api.issue.internal.DefaultIssue; + +import java.util.Date; + +/** + * See https://jira.codehaus.org/browse/SONAR-4309 + * + * @since 3.6 + */ +class UpdateConflictResolver { + + private final Logger LOG = LoggerFactory.getLogger(IssueStorage.class); + + public void resolve(DefaultIssue issue, IssueMapper mapper) { + LOG.debug("Resolve conflict on issue " + issue.key()); + + IssueDto dbIssue = mapper.selectByKey(issue.key()); + if (dbIssue != null) { + mergeFields(dbIssue, issue); + mapper.update(IssueDto.toDtoForUpdate(issue, new Date())); + } + } + + @VisibleForTesting + void mergeFields(IssueDto dbIssue, DefaultIssue issue) { + resolveAssignee(dbIssue, issue); + resolvePlan(dbIssue, issue); + resolveSeverity(dbIssue, issue); + resolveEffortToFix(dbIssue, issue); + resolveResolution(dbIssue, issue); + resolveStatus(dbIssue, issue); + } + + private void resolveStatus(IssueDto dbIssue, DefaultIssue issue) { + issue.setStatus(dbIssue.getStatus()); + } + + private void resolveResolution(IssueDto dbIssue, DefaultIssue issue) { + issue.setResolution(dbIssue.getResolution()); + } + + private void resolveEffortToFix(IssueDto dbIssue, DefaultIssue issue) { + issue.setEffortToFix(dbIssue.getEffortToFix()); + } + + private void resolveSeverity(IssueDto dbIssue, DefaultIssue issue) { + if (dbIssue.isManualSeverity()) { + issue.setManualSeverity(true); + issue.setSeverity(dbIssue.getSeverity()); + } + // else keep severity as declared in quality profile + } + + private void resolvePlan(IssueDto dbIssue, DefaultIssue issue) { + issue.setActionPlanKey(dbIssue.getActionPlanKey()); + } + + private void resolveAssignee(IssueDto dbIssue, DefaultIssue issue) { + issue.setAssignee(dbIssue.getAssignee()); + } +} 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 855800149a5..e52ab7b8648 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 @@ -111,6 +111,31 @@ where kee = #{kee} </update> + <!-- + IMPORTANT - invariant columns can't be updated. See IssueDto#toDtoForUpdate() + --> + <update id="updateIfBeforeSelectedDate" parameterType="Issue"> + update issues set + action_plan_key=#{actionPlanKey}, + severity=#{severity}, + manual_severity=#{manualSeverity}, + message=#{message}, + line=#{line}, + effort_to_fix=#{effortToFix}, + status=#{status}, + resolution=#{resolution}, + checksum=#{checksum}, + reporter=#{reporter}, + assignee=#{assignee}, + author_login=#{authorLogin}, + issue_attributes=#{issueAttributes}, + issue_creation_date=#{issueCreationDate}, + issue_update_date=#{issueUpdateDate}, + issue_close_date=#{issueCloseDate}, + updated_at=#{updatedAt} + where kee = #{kee} and updated_at <= #{selectedAt} + </update> + <select id="selectByKey" parameterType="String" resultType="Issue"> select <include refid="issueColumns"/> diff --git a/sonar-core/src/test/java/org/sonar/core/issue/db/IssueMapperTest.java b/sonar-core/src/test/java/org/sonar/core/issue/db/IssueMapperTest.java index 8311558009f..7476d5b98fc 100644 --- a/sonar-core/src/test/java/org/sonar/core/issue/db/IssueMapperTest.java +++ b/sonar-core/src/test/java/org/sonar/core/issue/db/IssueMapperTest.java @@ -25,6 +25,10 @@ import org.junit.Test; import org.sonar.api.utils.DateUtils; import org.sonar.core.persistence.AbstractDaoTestCase; +import java.util.Date; + +import static org.fest.assertions.Assertions.assertThat; + public class IssueMapperTest extends AbstractDaoTestCase { SqlSession session; @@ -101,4 +105,78 @@ public class IssueMapperTest extends AbstractDaoTestCase { checkTables("testUpdate", new String[]{"id"}, "issues"); } + + @Test + public void updateBeforeSelectedDate_without_conflict() throws Exception { + setupData("testUpdate"); + + IssueDto dto = new IssueDto(); + dto.setComponentId(123l); + dto.setRootComponentId(100l); + dto.setRuleId(200); + dto.setKee("ABCDE"); + dto.setLine(500); + dto.setEffortToFix(3.14); + dto.setResolution("FIXED"); + dto.setStatus("RESOLVED"); + dto.setSeverity("BLOCKER"); + dto.setReporter("emmerik"); + dto.setAuthorLogin("morgan"); + dto.setAssignee("karadoc"); + dto.setActionPlanKey("current_sprint"); + dto.setIssueAttributes("JIRA=FOO-1234"); + dto.setChecksum("123456789"); + dto.setMessage("the message"); + dto.setIssueCreationDate(DateUtils.parseDate("2013-05-18")); + dto.setIssueUpdateDate(DateUtils.parseDate("2013-05-19")); + dto.setIssueCloseDate(DateUtils.parseDate("2013-05-20")); + dto.setCreatedAt(DateUtils.parseDate("2013-05-21")); + dto.setUpdatedAt(DateUtils.parseDate("2013-05-22")); + + // selected after last update -> ok + dto.setSelectedAt(DateUtils.parseDate("2015-01-01")); + + int count = mapper.updateIfBeforeSelectedDate(dto); + assertThat(count).isEqualTo(1); + session.commit(); + + checkTables("testUpdate", new String[]{"id"}, "issues"); + } + + @Test + public void updateBeforeSelectedDate_with_conflict() throws Exception { + setupData("updateBeforeSelectedDate_with_conflict"); + + IssueDto dto = new IssueDto(); + dto.setComponentId(123l); + dto.setRootComponentId(100l); + dto.setRuleId(200); + dto.setKee("ABCDE"); + dto.setLine(500); + dto.setEffortToFix(3.14); + dto.setResolution("FIXED"); + dto.setStatus("RESOLVED"); + dto.setSeverity("BLOCKER"); + dto.setReporter("emmerik"); + dto.setAuthorLogin("morgan"); + dto.setAssignee("karadoc"); + dto.setActionPlanKey("current_sprint"); + dto.setIssueAttributes("JIRA=FOO-1234"); + dto.setChecksum("123456789"); + dto.setMessage("the message"); + dto.setIssueCreationDate(DateUtils.parseDate("2013-05-18")); + dto.setIssueUpdateDate(DateUtils.parseDate("2013-05-19")); + dto.setIssueCloseDate(DateUtils.parseDate("2013-05-20")); + dto.setCreatedAt(DateUtils.parseDate("2013-05-21")); + dto.setUpdatedAt(DateUtils.parseDate("2013-05-22")); + + // selected before last update -> ko + dto.setSelectedAt(DateUtils.parseDate("2009-01-01")); + + int count = mapper.updateIfBeforeSelectedDate(dto); + assertThat(count).isEqualTo(0); + session.commit(); + + checkTables("updateBeforeSelectedDate_with_conflict", new String[]{"id"}, "issues"); + } } 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 new file mode 100644 index 00000000000..09458daae24 --- /dev/null +++ b/sonar-core/src/test/java/org/sonar/core/issue/db/UpdateConflictResolverTest.java @@ -0,0 +1,149 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 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.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.api.rule.RuleKey; +import org.sonar.api.rule.Severity; +import org.sonar.api.utils.DateUtils; + +import static org.fest.assertions.Assertions.assertThat; +import static org.mockito.Mockito.*; + +public class UpdateConflictResolverTest { + + @Test + public void should_reload_issue_and_resolve_conflict() throws Exception { + DefaultIssue issue = new DefaultIssue() + .setKey("ABCDE") + .setRuleKey(RuleKey.of("squid", "AvoidCycles")) + .setComponentKey("struts:org.apache.struts.Action") + .setNew(false) + .setStatus(Issue.STATUS_OPEN); + + // Issue as seen and changed by end-user + IssueMapper mapper = mock(IssueMapper.class); + when(mapper.selectByKey("ABCDE")).thenReturn( + new IssueDto() + .setKee("ABCDE") + .setRuleId(10) + .setRuleKey_unit_test_only("squid", "AvoidCycles") + .setComponentId(100L) + .setComponentKey_unit_test_only("struts:org.apache.struts.Action") + .setLine(10) + .setStatus(Issue.STATUS_OPEN) + + // field changed by user + .setAssignee("arthur") + ); + + new UpdateConflictResolver().resolve(issue, mapper); + + ArgumentCaptor<IssueDto> argument = ArgumentCaptor.forClass(IssueDto.class); + verify(mapper).update(argument.capture()); + IssueDto updatedIssue = argument.getValue(); + assertThat(updatedIssue.getKee()).isEqualTo("ABCDE"); + assertThat(updatedIssue.getAssignee()).isEqualTo("arthur"); + } + + @Test + public void should_keep_changes_made_by_user() throws Exception { + DefaultIssue issue = new DefaultIssue() + .setKey("ABCDE") + .setRuleKey(RuleKey.of("squid", "AvoidCycles")) + .setComponentKey("struts:org.apache.struts.Action") + .setNew(false); + + // Before starting scan + issue.setAssignee(null); + issue.setActionPlanKey("PLAN-1"); + issue.setCreationDate(DateUtils.parseDate("2012-01-01")); + issue.setUpdateDate(DateUtils.parseDate("2012-02-02")); + + // Changed by scan + issue.setLine(200); + issue.setSeverity(Severity.BLOCKER); + issue.setManualSeverity(false); + issue.setAuthorLogin("simon"); + issue.setChecksum("CHECKSUM-ABCDE"); + issue.setResolution(null); + issue.setStatus(Issue.STATUS_REOPENED); + + // Issue as seen and changed by end-user + IssueDto dbIssue = new IssueDto() + .setKee("ABCDE") + .setRuleId(10) + .setRuleKey_unit_test_only("squid", "AvoidCycles") + .setComponentId(100L) + .setComponentKey_unit_test_only("struts:org.apache.struts.Action") + .setLine(10) + .setResolution(Issue.RESOLUTION_FALSE_POSITIVE) + .setStatus(Issue.STATUS_RESOLVED) + .setAssignee("arthur") + .setActionPlanKey("PLAN-2") + .setSeverity(Severity.MAJOR) + .setManualSeverity(false); + + new UpdateConflictResolver().mergeFields(dbIssue, issue); + + assertThat(issue.key()).isEqualTo("ABCDE"); + assertThat(issue.componentKey()).isEqualTo("struts:org.apache.struts.Action"); + + // Scan wins on : + assertThat(issue.line()).isEqualTo(200); + assertThat(issue.severity()).isEqualTo(Severity.BLOCKER); + assertThat(issue.manualSeverity()).isFalse(); + + // User wins on : + assertThat(issue.assignee()).isEqualTo("arthur"); + assertThat(issue.resolution()).isEqualTo(Issue.RESOLUTION_FALSE_POSITIVE); + assertThat(issue.status()).isEqualTo(Issue.STATUS_RESOLVED); + assertThat(issue.actionPlanKey()).isEqualTo("PLAN-2"); + } + + @Test + public void severity_changed_by_user_should_be_kept() throws Exception { + DefaultIssue issue = new DefaultIssue() + .setKey("ABCDE") + .setRuleKey(RuleKey.of("squid", "AvoidCycles")) + .setComponentKey("struts:org.apache.struts.Action") + .setNew(false) + .setStatus(Issue.STATUS_OPEN); + + // Changed by scan + issue.setSeverity(Severity.BLOCKER); + issue.setManualSeverity(false); + + // Issue as seen and changed by end-user + IssueDto dbIssue = new IssueDto() + .setKee("ABCDE") + .setStatus(Issue.STATUS_OPEN) + .setSeverity(Severity.INFO) + .setManualSeverity(true); + + new UpdateConflictResolver().mergeFields(dbIssue, issue); + + assertThat(issue.severity()).isEqualTo(Severity.INFO); + assertThat(issue.manualSeverity()).isTrue(); + } +} diff --git a/sonar-core/src/test/resources/org/sonar/core/issue/db/IssueMapperTest/testUpdate.xml b/sonar-core/src/test/resources/org/sonar/core/issue/db/IssueMapperTest/testUpdate.xml index c1473018a53..a4df3d603dd 100644 --- a/sonar-core/src/test/resources/org/sonar/core/issue/db/IssueMapperTest/testUpdate.xml +++ b/sonar-core/src/test/resources/org/sonar/core/issue/db/IssueMapperTest/testUpdate.xml @@ -21,7 +21,7 @@ issue_update_date="[null]" issue_close_date="[null]" created_at="[null]" - updated_at="[null]" + updated_at="2009-01-01" action_plan_key="[null]" /> </dataset>
\ No newline at end of file diff --git a/sonar-core/src/test/resources/org/sonar/core/issue/db/IssueMapperTest/updateBeforeSelectedDate_with_conflict-result.xml b/sonar-core/src/test/resources/org/sonar/core/issue/db/IssueMapperTest/updateBeforeSelectedDate_with_conflict-result.xml new file mode 100644 index 00000000000..3bd12af6cf8 --- /dev/null +++ b/sonar-core/src/test/resources/org/sonar/core/issue/db/IssueMapperTest/updateBeforeSelectedDate_with_conflict-result.xml @@ -0,0 +1,28 @@ +<dataset> + <!-- not updated --> + <issues + id="100" + kee="ABCDE" + component_id="123" + root_component_id="100" + rule_id="200" + severity="INFO" + manual_severity="[false]" + message="old" + line="[null]" + effort_to_fix="[null]" + status="OPEN" + resolution="[null]" + checksum="[null]" + reporter="[null]" + author_login="[null]" + assignee="[null]" + issue_attributes="[null]" + issue_creation_date="[null]" + issue_update_date="[null]" + issue_close_date="[null]" + created_at="[null]" + updated_at="2013-06-01" + action_plan_key="[null]" + /> +</dataset>
\ No newline at end of file diff --git a/sonar-core/src/test/resources/org/sonar/core/issue/db/IssueMapperTest/updateBeforeSelectedDate_with_conflict.xml b/sonar-core/src/test/resources/org/sonar/core/issue/db/IssueMapperTest/updateBeforeSelectedDate_with_conflict.xml new file mode 100644 index 00000000000..d598d02536e --- /dev/null +++ b/sonar-core/src/test/resources/org/sonar/core/issue/db/IssueMapperTest/updateBeforeSelectedDate_with_conflict.xml @@ -0,0 +1,27 @@ +<dataset> + <issues + id="100" + kee="ABCDE" + component_id="123" + root_component_id="100" + rule_id="200" + severity="INFO" + manual_severity="[false]" + message="old" + line="[null]" + effort_to_fix="[null]" + status="OPEN" + resolution="[null]" + checksum="[null]" + reporter="[null]" + author_login="[null]" + assignee="[null]" + issue_attributes="[null]" + issue_creation_date="[null]" + issue_update_date="[null]" + issue_close_date="[null]" + created_at="[null]" + updated_at="2013-06-01" + action_plan_key="[null]" + /> +</dataset>
\ No newline at end of file diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/issue/internal/DefaultIssue.java b/sonar-plugin-api/src/main/java/org/sonar/api/issue/internal/DefaultIssue.java index e594b754145..8bd1e6b322b 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/issue/internal/DefaultIssue.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/issue/internal/DefaultIssue.java @@ -89,6 +89,9 @@ public class DefaultIssue implements Issue { // true if some fields have been changed since the previous scan private boolean isChanged = false; + // Date when issue was loaded from db (only when isNew=false) + private Date selectedAt; + public String key() { return key; } @@ -380,6 +383,15 @@ public class DefaultIssue implements Issue { return Objects.firstNonNull(comments, Collections.<IssueComment>emptyList()); } + @CheckForNull + public Date selectedAt() { + return selectedAt; + } + + public void setSelectedAt(@Nullable Date d) { + this.selectedAt = d; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -404,4 +416,6 @@ public class DefaultIssue implements Issue { public String toString() { return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); } + + } |