diff options
34 files changed, 671 insertions, 404 deletions
diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/DefaultIssueHandlerContext.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/DefaultIssueHandlerContext.java deleted file mode 100644 index c9810f76603..00000000000 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/DefaultIssueHandlerContext.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * 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.plugins.core.issue; - -import org.sonar.api.issue.Issue; -import org.sonar.api.issue.IssueHandler; -import org.sonar.core.issue.DefaultIssue; - -import javax.annotation.Nullable; - -class DefaultIssueHandlerContext implements IssueHandler.IssueContext { - - private final DefaultIssue issue; - - DefaultIssueHandlerContext(DefaultIssue issue) { - this.issue = issue; - } - - @Override - public Issue issue() { - return issue; - } - - @Override - public boolean isNew() { - return issue.isNew(); - } - - @Override - public boolean isAlive() { - return issue.isAlive(); - } - - @Override - public IssueHandler.IssueContext setLine(@Nullable Integer line) { - issue.setLine(line); - return this; - } - - @Override - public IssueHandler.IssueContext setDescription(String description) { - issue.setDescription(description); - return this; - } - - @Override - public IssueHandler.IssueContext setSeverity(String severity) { - issue.setSeverity(severity); - return this; - } - - @Override - public IssueHandler.IssueContext setAuthorLogin(@Nullable String login) { - issue.setAuthorLogin(login); - return this; - } - - @Override - public IssueHandler.IssueContext setAttribute(String key, @Nullable String value) { - issue.setAttribute(key, value); - return this; - } - - @Override - public IssueHandler.IssueContext assignTo(@Nullable String login) { - issue.setAssignee(login); - return this; - } -} diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/IssueHandlers.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/IssueHandlers.java index 3e496edcb69..96be94d7966 100644 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/IssueHandlers.java +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/IssueHandlers.java @@ -20,25 +20,100 @@ package org.sonar.plugins.core.issue; import org.sonar.api.BatchExtension; +import org.sonar.api.issue.Issue; import org.sonar.api.issue.IssueHandler; import org.sonar.core.issue.DefaultIssue; +import org.sonar.core.issue.IssueUpdater; + +import javax.annotation.Nullable; public class IssueHandlers implements BatchExtension { private final IssueHandler[] handlers; + private final DefaultContext context; - public IssueHandlers(IssueHandler[] handlers) { + public IssueHandlers(IssueUpdater updater, IssueHandler[] handlers) { this.handlers = handlers; + this.context = new DefaultContext(updater); } - public IssueHandlers() { - this(new IssueHandler[0]); + public IssueHandlers(IssueUpdater updater) { + this(updater, new IssueHandler[0]); } public void execute(DefaultIssue issue) { - DefaultIssueHandlerContext context = new DefaultIssueHandlerContext(issue); + context.reset(issue); for (IssueHandler handler : handlers) { handler.onIssue(context); } } + static class DefaultContext implements IssueHandler.Context { + private final IssueUpdater updater; + private DefaultIssue issue; + + + private DefaultContext(IssueUpdater updater) { + this.updater = updater; + } + + private void reset(DefaultIssue i) { + this.issue = i; + } + + @Override + public Issue issue() { + return issue; + } + + @Override + public boolean isNew() { + return issue.isNew(); + } + + @Override + public boolean isAlive() { + return issue.isAlive(); + } + + @Override + public IssueHandler.Context setLine(@Nullable Integer line) { + updater.setLine(issue, line); + return this; + } + + @Override + public IssueHandler.Context setDescription(@Nullable String description) { + updater.setDescription(issue, description); + return this; + } + + @Override + public IssueHandler.Context setSeverity(String severity) { + updater.setSeverity(issue, severity); + return this; + } + + @Override + public IssueHandler.Context setAuthorLogin(@Nullable String login) { + updater.setAuthorLogin(issue, login); + return this; + } + + @Override + public IssueHandler.Context setAttribute(String key, @Nullable String value) { + throw new UnsupportedOperationException("TODO"); + } + + @Override + public IssueHandler.Context assign(@Nullable String assignee) { + updater.assign(issue, assignee); + return this; + } + + @Override + public IssueHandler.Context comment(String message) { + return null; + } + } + } diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/IssueTracking.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/IssueTracking.java index 2c1f78e570e..65485967058 100644 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/IssueTracking.java +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/IssueTracking.java @@ -349,6 +349,8 @@ public class IssueTracking implements BatchExtension { if (pastIssue.isManualSeverity()) { newIssue.setManualSeverity(true); newIssue.setSeverity(pastIssue.getSeverity()); + } else if (!Objects.equal(pastIssue.getSeverity(), newIssue.severity())) { + newIssue.setDiff("severity", pastIssue.getSeverity(), newIssue.severity()); } newIssue.setResolution(pastIssue.getResolution()); newIssue.setStatus(pastIssue.getStatus()); @@ -358,15 +360,13 @@ public class IssueTracking implements BatchExtension { newIssue.setAlive(true); newIssue.setAuthorLogin(pastIssue.getAuthorLogin()); if (pastIssue.getAttributes() != null) { + //TODO do not loose new attributes newIssue.setAttributes(KeyValueFormat.parse(pastIssue.getAttributes())); } lastIssuesByRule.remove(getRuleId(newIssue), pastIssue); issueMap.put(newIssue, pastIssue); unmappedLastIssues.remove(pastIssue); - } else { - newIssue.setNew(true); - newIssue.setCreatedAt(project.getAnalysisDate()); } } 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 7ef5af13ac7..f6c8b833d69 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 @@ -31,6 +31,7 @@ import org.sonar.api.resources.ResourceUtils; import org.sonar.api.resources.Scopes; import org.sonar.batch.issue.ScanIssues; import org.sonar.core.issue.DefaultIssue; +import org.sonar.core.issue.IssueChangeContext; import org.sonar.core.issue.db.IssueDto; import org.sonar.core.issue.workflow.IssueWorkflow; @@ -83,8 +84,9 @@ public class IssueTrackingDecorator implements Decorator { addDead(issues); } + IssueChangeContext changeContext = IssueChangeContext.createScan(context.getProject().getAnalysisDate()); for (DefaultIssue issue : issues) { - workflow.doAutomaticTransition(issue); + workflow.doAutomaticTransition(issue, changeContext); handlers.execute(issue); scanIssues.addOrUpdate(issue); } diff --git a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/IssueHandlersTest.java b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/IssueHandlersTest.java index dbc5e7bec7e..684f6431770 100644 --- a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/IssueHandlersTest.java +++ b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/IssueHandlersTest.java @@ -23,33 +23,38 @@ import org.junit.Test; import org.mockito.ArgumentMatcher; import org.sonar.api.issue.IssueHandler; import org.sonar.core.issue.DefaultIssue; +import org.sonar.core.issue.IssueChangeContext; +import org.sonar.core.issue.IssueUpdater; import static org.mockito.Matchers.argThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; public class IssueHandlersTest { @Test public void should_execute_handlers() throws Exception { IssueHandler h1 = mock(IssueHandler.class); IssueHandler h2 = mock(IssueHandler.class); + IssueUpdater updater = mock(IssueUpdater.class); - IssueHandlers handlers = new IssueHandlers(new IssueHandler[]{h1, h2}); + IssueHandlers handlers = new IssueHandlers(updater, new IssueHandler[]{h1, h2}); final DefaultIssue issue = new DefaultIssue(); handlers.execute(issue); - verify(h1).onIssue(argThat(new ArgumentMatcher<IssueHandler.IssueContext>() { + verify(h1).onIssue(argThat(new ArgumentMatcher<IssueHandler.Context>() { @Override public boolean matches(Object o) { - return ((IssueHandler.IssueContext) o).issue() == issue; + return ((IssueHandler.Context) o).issue() == issue; } })); } @Test public void test_no_handlers() { - IssueHandlers handlers = new IssueHandlers(); + IssueUpdater updater = mock(IssueUpdater.class); + IssueHandlers handlers = new IssueHandlers(updater); handlers.execute(new DefaultIssue()); - // do not fail + verifyZeroInteractions(updater); } } diff --git a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/IssueTrackingDecoratorTest.java b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/IssueTrackingDecoratorTest.java index 72d74f21186..3eae474dd84 100644 --- a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/IssueTrackingDecoratorTest.java +++ b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/IssueTrackingDecoratorTest.java @@ -23,12 +23,14 @@ import com.google.common.collect.Sets; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentMatcher; +import org.mockito.Mockito; import org.sonar.api.batch.DecoratorContext; import org.sonar.api.resources.File; import org.sonar.api.resources.Project; import org.sonar.api.resources.Resource; import org.sonar.batch.issue.ScanIssues; import org.sonar.core.issue.DefaultIssue; +import org.sonar.core.issue.IssueChangeContext; import org.sonar.core.issue.db.IssueDto; import org.sonar.core.issue.workflow.IssueWorkflow; import org.sonar.core.persistence.AbstractDaoTestCase; @@ -88,7 +90,7 @@ public class IssueTrackingDecoratorTest extends AbstractDaoTestCase { List<IssueDto> dbIssues = Collections.emptyList(); when(initialOpenIssues.selectAndRemove(123)).thenReturn(dbIssues); - decorator.decorate(file, mock(DecoratorContext.class)); + decorator.decorate(file, mock(DecoratorContext.class, Mockito.RETURNS_MOCKS)); // Apply filters, track, apply transitions, notify extensions then update cache verify(filters).accept(issue); @@ -99,8 +101,8 @@ public class IssueTrackingDecoratorTest extends AbstractDaoTestCase { return issues.size() == 1 && issues.get(0) == issue; } })); - verify(workflow).doAutomaticTransition(issue); - verify(handlers).execute(issue); + verify(workflow).doAutomaticTransition(eq(issue), any(IssueChangeContext.class)); + verify(handlers).execute(eq(issue)); verify(scanIssues).addOrUpdate(issue); } @@ -117,9 +119,9 @@ public class IssueTrackingDecoratorTest extends AbstractDaoTestCase { List<IssueDto> unmatchedIssues = Arrays.asList(unmatchedIssue); when(tracking.track(eq(file), anyCollection(), anyCollection())).thenReturn(Sets.newHashSet(unmatchedIssues)); - decorator.decorate(file, mock(DecoratorContext.class)); + decorator.decorate(file, mock(DecoratorContext.class, Mockito.RETURNS_MOCKS)); - verify(workflow, times(2)).doAutomaticTransition(any(DefaultIssue.class)); + verify(workflow, times(2)).doAutomaticTransition(any(DefaultIssue.class), any(IssueChangeContext.class)); verify(handlers, times(2)).execute(any(DefaultIssue.class)); verify(scanIssues, times(2)).addOrUpdate(any(DefaultIssue.class)); @@ -141,10 +143,10 @@ public class IssueTrackingDecoratorTest extends AbstractDaoTestCase { IssueDto deadIssue = new IssueDto().setKey("ABCDE").setResolution("OPEN").setStatus("OPEN").setRuleKey_unit_test_only("squid", "AvoidCycle"); when(initialOpenIssues.getAllIssues()).thenReturn(Arrays.asList(deadIssue)); - decorator.decorate(project, mock(DecoratorContext.class)); + decorator.decorate(project, mock(DecoratorContext.class, Mockito.RETURNS_MOCKS)); // the dead issue must be closed -> apply automatic transition, notify handlers and add to cache - verify(workflow, times(2)).doAutomaticTransition(any(DefaultIssue.class)); + verify(workflow, times(2)).doAutomaticTransition(any(DefaultIssue.class), any(IssueChangeContext.class)); verify(handlers, times(2)).execute(any(DefaultIssue.class)); verify(scanIssues, times(2)).addOrUpdate(any(DefaultIssue.class)); diff --git a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/IssueTrackingTest.java b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/IssueTrackingTest.java index ad4a625b2f3..82eeebdbd5a 100644 --- a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/IssueTrackingTest.java +++ b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/IssueTrackingTest.java @@ -207,17 +207,6 @@ public class IssueTrackingTest { } @Test - public void should_set_date_of_new_issues() { - DefaultIssue newIssue = newDefaultIssue("message", 1, RuleKey.of("squid", "AvoidCycle"), "checksum"); - assertThat(newIssue.createdAt()).isNull(); - - Map<DefaultIssue, IssueDto> mapping = tracking.mapIssues(newArrayList(newIssue), Lists.<IssueDto>newArrayList()); - assertThat(mapping.size()).isEqualTo(0); - assertThat(newIssue.createdAt()).isEqualTo(analysisDate); - assertThat(newIssue.isNew()).isTrue(); - } - - @Test public void should_set_severity_if_severity_has_been_changed_by_user() { DefaultIssue newIssue = newDefaultIssue("message", 1, RuleKey.of("squid", "AvoidCycle"), "checksum").setSeverity("MAJOR"); IssueDto referenceIssue = newReferenceIssue("message", 1, 1, "checksum").setSeverity("MINOR").setManualSeverity(true); diff --git a/sonar-batch/src/main/java/org/sonar/batch/issue/DeprecatedViolations.java b/sonar-batch/src/main/java/org/sonar/batch/issue/DeprecatedViolations.java index 28d69f309c4..c7e421bb335 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/issue/DeprecatedViolations.java +++ b/sonar-batch/src/main/java/org/sonar/batch/issue/DeprecatedViolations.java @@ -20,14 +20,13 @@ package org.sonar.batch.issue; import org.sonar.api.BatchComponent; -import org.sonar.api.issue.Issue; import org.sonar.api.resources.Resource; import org.sonar.api.rule.RuleKey; import org.sonar.api.rules.Violation; import org.sonar.core.issue.DefaultIssue; +import org.sonar.core.issue.DefaultIssueBuilder; import java.util.Collection; -import java.util.UUID; public class DeprecatedViolations implements BatchComponent { @@ -47,21 +46,12 @@ public class DeprecatedViolations implements BatchComponent { } DefaultIssue toIssue(Violation violation) { - DefaultIssue issue = new DefaultIssue() - .setComponentKey(violation.getResource().getEffectiveKey()) - .setKey(UUID.randomUUID().toString()) - .setRuleKey(RuleKey.of(violation.getRule().getRepositoryKey(), violation.getRule().getKey())) - .setCost(violation.getCost()) - .setLine(violation.getLineId()) - .setDescription(violation.getMessage()) - .setResolution(Issue.RESOLUTION_OPEN) - .setStatus(Issue.STATUS_OPEN) - .setManualSeverity(false) - .setManual(false) - .setSeverity(violation.getSeverity() != null ? violation.getSeverity().name() : null); - - // FIXME - //issue.setPerson(violation.getPersonId()); - return issue; + return (DefaultIssue) new DefaultIssueBuilder(violation.getResource().getEffectiveKey()) + .ruleKey(RuleKey.of(violation.getRule().getRepositoryKey(), violation.getRule().getKey())) + .cost(violation.getCost()) + .line(violation.getLineId()) + .description(violation.getMessage()) + .severity(violation.getSeverity() != null ? violation.getSeverity().name() : null) + .build(); } } diff --git a/sonar-batch/src/main/java/org/sonar/batch/issue/ScanIssues.java b/sonar-batch/src/main/java/org/sonar/batch/issue/ScanIssues.java index b1dcdaf2993..4719c96353d 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/issue/ScanIssues.java +++ b/sonar-batch/src/main/java/org/sonar/batch/issue/ScanIssues.java @@ -28,7 +28,6 @@ import org.sonar.api.rules.ActiveRule; import org.sonar.core.issue.DefaultIssue; import java.util.Collection; -import java.util.UUID; /** * Central component to manage issues @@ -65,10 +64,8 @@ public class ScanIssues { // rule does not exist or is not enabled -> ignore the issue return false; } - String key = UUID.randomUUID().toString(); - Preconditions.checkState(!Strings.isNullOrEmpty(key), "Fail to generate issue key"); - issue.setKey(key); issue.setCreatedAt(project.getAnalysisDate()); + issue.setUpdatedAt(project.getAnalysisDate()); if (issue.severity() == null) { issue.setSeverity(activeRule.getSeverity().name()); } diff --git a/sonar-batch/src/main/java/org/sonar/batch/scan/ProjectScanContainer.java b/sonar-batch/src/main/java/org/sonar/batch/scan/ProjectScanContainer.java index e73daf25d5b..6fd278c0e05 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/scan/ProjectScanContainer.java +++ b/sonar-batch/src/main/java/org/sonar/batch/scan/ProjectScanContainer.java @@ -30,24 +30,8 @@ import org.sonar.batch.DefaultFileLinesContextFactory; import org.sonar.batch.DefaultResourceCreationLock; import org.sonar.batch.ProjectConfigurator; import org.sonar.batch.ProjectTree; -import org.sonar.batch.bootstrap.BatchSettings; -import org.sonar.batch.bootstrap.ExtensionInstaller; -import org.sonar.batch.bootstrap.ExtensionMatcher; -import org.sonar.batch.bootstrap.ExtensionUtils; -import org.sonar.batch.bootstrap.MetricProvider; -import org.sonar.batch.index.Caches; -import org.sonar.batch.index.ComponentDataCache; -import org.sonar.batch.index.ComponentDataPersister; -import org.sonar.batch.index.DefaultIndex; -import org.sonar.batch.index.DefaultPersistenceManager; -import org.sonar.batch.index.DefaultResourcePersister; -import org.sonar.batch.index.DependencyPersister; -import org.sonar.batch.index.EventPersister; -import org.sonar.batch.index.LinkPersister; -import org.sonar.batch.index.MeasurePersister; -import org.sonar.batch.index.MemoryOptimizer; -import org.sonar.batch.index.SnapshotCache; -import org.sonar.batch.index.SourcePersister; +import org.sonar.batch.bootstrap.*; +import org.sonar.batch.index.*; import org.sonar.batch.issue.DeprecatedViolations; import org.sonar.batch.issue.IssueCache; import org.sonar.batch.issue.IssuePersister; @@ -58,6 +42,8 @@ import org.sonar.batch.scan.maven.MavenPluginExecutor; import org.sonar.batch.scan.source.HighlightableBuilder; import org.sonar.batch.scan.source.SymbolPerspectiveBuilder; import org.sonar.core.component.ScanGraph; +import org.sonar.core.issue.IssueUpdater; +import org.sonar.core.issue.workflow.FunctionExecutor; import org.sonar.core.issue.workflow.IssueWorkflow; import org.sonar.core.notification.DefaultNotificationManager; import org.sonar.core.test.TestPlanBuilder; @@ -83,44 +69,46 @@ public class ProjectScanContainer extends ComponentContainer { private void addBatchComponents() { add( - DefaultResourceCreationLock.class, - DefaultPersistenceManager.class, - DependencyPersister.class, - EventPersister.class, - LinkPersister.class, - MeasurePersister.class, - MemoryOptimizer.class, - DefaultResourcePersister.class, - SourcePersister.class, - DefaultNotificationManager.class, - MetricProvider.class, - ProjectConfigurator.class, - DefaultIndex.class, - DefaultFileLinesContextFactory.class, - ProjectLock.class, - LastSnapshots.class, - Caches.class, - SnapshotCache.class, - ComponentDataCache.class, - ComponentDataPersister.class, + DefaultResourceCreationLock.class, + DefaultPersistenceManager.class, + DependencyPersister.class, + EventPersister.class, + LinkPersister.class, + MeasurePersister.class, + MemoryOptimizer.class, + DefaultResourcePersister.class, + SourcePersister.class, + DefaultNotificationManager.class, + MetricProvider.class, + ProjectConfigurator.class, + DefaultIndex.class, + DefaultFileLinesContextFactory.class, + ProjectLock.class, + LastSnapshots.class, + Caches.class, + SnapshotCache.class, + ComponentDataCache.class, + ComponentDataPersister.class, - // issues - IssueWorkflow.class, - DeprecatedViolations.class, - IssueCache.class, - IssuePersister.class, + // issues + IssueUpdater.class, + FunctionExecutor.class, + IssueWorkflow.class, + DeprecatedViolations.class, + IssueCache.class, + IssuePersister.class, - // tests - TestPlanPerspectiveLoader.class, - TestablePerspectiveLoader.class, - TestPlanBuilder.class, - TestableBuilder.class, - ScanGraph.create(), - GraphPersister.class, + // tests + TestPlanPerspectiveLoader.class, + TestablePerspectiveLoader.class, + TestPlanBuilder.class, + TestableBuilder.class, + ScanGraph.create(), + GraphPersister.class, - // lang - HighlightableBuilder.class, - SymbolPerspectiveBuilder.class); + // lang + HighlightableBuilder.class, + SymbolPerspectiveBuilder.class); } private void fixMavenExecutor() { diff --git a/sonar-batch/src/test/java/org/sonar/batch/issue/ScanIssuesTest.java b/sonar-batch/src/test/java/org/sonar/batch/issue/ScanIssuesTest.java index 963f0618ffc..695d2292f97 100644 --- a/sonar-batch/src/test/java/org/sonar/batch/issue/ScanIssuesTest.java +++ b/sonar-batch/src/test/java/org/sonar/batch/issue/ScanIssuesTest.java @@ -84,13 +84,15 @@ public class ScanIssuesTest { Date analysisDate = new Date(); when(project.getAnalysisDate()).thenReturn(analysisDate); - DefaultIssue issue = new DefaultIssue().setRuleKey(RuleKey.of("squid", "AvoidCycle")).setSeverity(Severity.CRITICAL); + DefaultIssue issue = new DefaultIssue() + .setKey("ABCDE") + .setRuleKey(RuleKey.of("squid", "AvoidCycle")) + .setSeverity(Severity.CRITICAL); boolean added = scanIssues.initAndAddIssue(issue); assertThat(added).isTrue(); ArgumentCaptor<DefaultIssue> argument = ArgumentCaptor.forClass(DefaultIssue.class); verify(cache).put(argument.capture()); - assertThat(argument.getValue().key()).isNotNull(); assertThat(argument.getValue().severity()).isEqualTo(Severity.CRITICAL); assertThat(argument.getValue().createdAt()).isEqualTo(analysisDate); } @@ -111,7 +113,6 @@ public class ScanIssuesTest { ArgumentCaptor<DefaultIssue> argument = ArgumentCaptor.forClass(DefaultIssue.class); verify(cache).put(argument.capture()); - assertThat(argument.getValue().key()).isNotNull(); assertThat(argument.getValue().severity()).isEqualTo(Severity.INFO); assertThat(argument.getValue().createdAt()).isEqualTo(analysisDate); } 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 index bb6fe07a4c7..23d3dfd82b3 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java @@ -19,10 +19,10 @@ */ 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.ImmutableMap; -import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.builder.ToStringBuilder; @@ -36,9 +36,8 @@ import java.io.Serializable; import java.util.Collections; import java.util.Date; import java.util.Map; -import java.util.Set; -public class DefaultIssue implements Issue, Serializable { +public class DefaultIssue implements Issue { private String key; private String componentKey; @@ -60,8 +59,8 @@ public class DefaultIssue implements Issue, Serializable { private boolean isNew = true; private boolean isAlive = true; private Map<String, String> attributes = null; - - private String authorLogin; + private String authorLogin = null; + private IssueChange change = null; public String key() { return key; @@ -100,7 +99,7 @@ public class DefaultIssue implements Issue, Serializable { return this; } - public boolean isManualSeverity() { + public boolean manualSeverity() { return manualSeverity; } @@ -198,6 +197,7 @@ public class DefaultIssue implements Issue, Serializable { return closedAt; } + // TODO rename setClosedDate public DefaultIssue setClosedAt(@Nullable Date d) { this.closedAt = d; return this; @@ -273,6 +273,20 @@ public class DefaultIssue implements Issue, Serializable { return this; } + public DefaultIssue setDiff(String field, @Nullable Serializable oldValue, @Nullable Serializable newValue) { + if (!Objects.equal(oldValue, newValue)) { + if (change == null) { + change = new IssueChange(); + } + change.setDiff(field, oldValue, newValue); + } + return this; + } + + public IssueChange change() { + return change; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -297,6 +311,4 @@ public class DefaultIssue implements Issue, Serializable { public String toString() { return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); } - - } 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 eb80c617351..3ed6d326c56 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 @@ -19,13 +19,18 @@ */ 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.Maps; import org.sonar.api.issue.Issuable; import org.sonar.api.issue.Issue; import org.sonar.api.rule.RuleKey; +import org.sonar.api.rule.Severity; +import java.util.Date; import java.util.Map; +import java.util.UUID; public class DefaultIssueBuilder implements Issuable.IssueBuilder { @@ -88,18 +93,25 @@ public class DefaultIssueBuilder implements Issuable.IssueBuilder { } @Override - public Issue build() { + public DefaultIssue build() { Preconditions.checkNotNull(componentKey, "Component key must be set"); Preconditions.checkNotNull(ruleKey, "Rule key must be set"); DefaultIssue issue = new DefaultIssue(); + String key = UUID.randomUUID().toString(); + Preconditions.checkState(!Strings.isNullOrEmpty(key), "Fail to generate issue key"); + issue.setKey(key); + Date now = new Date(); + issue.setCreatedAt(now); + issue.setUpdatedAt(now); issue.setComponentKey(componentKey); issue.setRuleKey(ruleKey); issue.setDescription(description); - issue.setSeverity(severity); + issue.setSeverity(Objects.firstNonNull(severity, Severity.MAJOR)); issue.setCost(cost); issue.setLine(line); issue.setManual(manual); + issue.setManualSeverity(manual); issue.setAttributes(attributes); issue.setNew(true); issue.setAlive(true); diff --git a/sonar-core/src/main/java/org/sonar/core/issue/IssueChange.java b/sonar-core/src/main/java/org/sonar/core/issue/IssueChange.java new file mode 100644 index 00000000000..d64c6b98411 --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/issue/IssueChange.java @@ -0,0 +1,67 @@ +/* + * 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; + +import com.google.common.collect.Maps; + +import javax.annotation.Nullable; +import java.io.Serializable; +import java.util.Map; + +public class IssueChange implements Serializable { + + public static class Diff<T extends Serializable> implements Serializable { + private T before, after; + + public Diff(@Nullable T before, @Nullable T after) { + this.before = before; + this.after = after; + } + + public T before() { + return before; + } + + public T after() { + return after; + } + + void setAfter(T t) { + this.after = t; + } + } + + private Map<String, Diff> diffs = Maps.newLinkedHashMap(); + + public Map<String, Diff> diffs() { + return diffs; + } + + @SuppressWarnings("unchecked") + public void 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.setAfter(newValue); + } + } +} diff --git a/sonar-core/src/main/java/org/sonar/core/issue/workflow/SetClosedAt.java b/sonar-core/src/main/java/org/sonar/core/issue/IssueChangeContext.java index ca2f47c268d..a8422d6edd9 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/workflow/SetClosedAt.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/IssueChangeContext.java @@ -17,20 +17,43 @@ * 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; - -import org.sonar.core.issue.DefaultIssue; +package org.sonar.core.issue; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import java.io.Serializable; import java.util.Date; -class SetClosedAt implements Function { - static final SetClosedAt CLOSED_AT = new SetClosedAt(); +public class IssueChangeContext implements Serializable { + + private String login; + private Date date; + private boolean automatic; + + private IssueChangeContext(@Nullable String login, Date date, boolean automatic) { + this.login = login; + this.date = date; + this.automatic = automatic; + } + + @CheckForNull + public String login() { + return login; + } + + public Date date() { + return date; + } + + public boolean automatic() { + return automatic; + } - private SetClosedAt() { + public static IssueChangeContext createScan(Date date) { + return new IssueChangeContext(null, date, true); } - @Override - public void execute(DefaultIssue issue) { - issue.setClosedAt(new Date()); + public static IssueChangeContext createUser(Date date, 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 new file mode 100644 index 00000000000..013e4b24f7f --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/issue/IssueUpdater.java @@ -0,0 +1,110 @@ +/* + * 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; + +import com.google.common.base.Objects; +import org.apache.commons.lang.StringUtils; +import org.sonar.api.BatchComponent; +import org.sonar.api.ServerComponent; + +import javax.annotation.Nullable; +import java.util.Date; + +public class IssueUpdater implements BatchComponent, ServerComponent { + + public boolean setSeverity(DefaultIssue issue, String severity) { + if (!Objects.equal(severity, issue.severity())) { + issue.setDiff("severity", issue.severity(), severity); + issue.setSeverity(severity); + return true; + } + return false; + } + + public DefaultIssue setManualSeverity(DefaultIssue issue, String severity) { + if (!issue.manualSeverity() || !Objects.equal(severity, issue.severity())) { + issue.setDiff("severity", issue.severity(), severity); + issue.setSeverity(severity); + issue.setManualSeverity(true); + } + return issue; + } + + public boolean assign(DefaultIssue issue, @Nullable String assignee) { + String sanitizedAssignee = StringUtils.defaultIfBlank(assignee, null); + if (!Objects.equal(sanitizedAssignee, issue.assignee())) { + issue.setDiff("assignee", issue.assignee(), sanitizedAssignee); + issue.setAssignee(sanitizedAssignee); + return true; + } + return false; + } + + public DefaultIssue setLine(DefaultIssue issue, @Nullable Integer line) { + if (!Objects.equal(line, issue.line())) { + issue.setLine(line); + } + return issue; + } + + public DefaultIssue setResolution(DefaultIssue issue, String resolution) { + if (!Objects.equal(resolution, issue.resolution())) { + issue.setDiff("resolution", issue.resolution(), resolution); + issue.setResolution(resolution); + } + return issue; + } + + public DefaultIssue setStatus(DefaultIssue issue, String status) { + if (!Objects.equal(status, issue.status())) { + issue.setDiff("status", issue.status(), status); + issue.setStatus(status); + } + return issue; + } + + public DefaultIssue setAuthorLogin(DefaultIssue issue, @Nullable String authorLogin) { + if (!Objects.equal(authorLogin, issue.authorLogin())) { + issue.setAuthorLogin(authorLogin); + } + return issue; + } + + public DefaultIssue setDescription(DefaultIssue issue, @Nullable String description) { + if (!Objects.equal(description, issue.description())) { + if (issue.manual()) { + issue.setDiff("description", issue.description(), description); + } + issue.setDescription(description); + } + return issue; + } + + public DefaultIssue setClosedDate(DefaultIssue issue, @Nullable Date date) { + if (!Objects.equal(date, issue.closedAt())) { + issue.setDiff("closedDate", issue.closedAt(), date); + issue.setClosedAt(date); + } + return issue; + } + + // TODO setAttribute + // TODO comment +} 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 4c5385c390b..40ea1e57411 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 @@ -308,7 +308,7 @@ public final class IssueDto { .setSeverity(issue.severity()) .setChecksum(issue.getChecksum()) .setManualIssue(issue.manual()) - .setManualSeverity(issue.isManualSeverity()) + .setManualSeverity(issue.manualSeverity()) .setUserLogin(issue.userLogin()) .setAssignee(issue.assignee()) .setCreatedAt(issue.createdAt()) diff --git a/sonar-core/src/main/java/org/sonar/core/issue/workflow/Function.java b/sonar-core/src/main/java/org/sonar/core/issue/workflow/Function.java index a45a595acbd..82b79dc1ec9 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/workflow/Function.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/workflow/Function.java @@ -19,8 +19,14 @@ */ package org.sonar.core.issue.workflow; -import org.sonar.core.issue.DefaultIssue; +import org.sonar.api.issue.Issue; interface Function { - void execute(DefaultIssue issue); + interface Context { + Issue issue(); + Context setResolution(String s); + Context setClosedDate(boolean b); + } + + void execute(Context context); } 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 new file mode 100644 index 00000000000..a54badf74cd --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/issue/workflow/FunctionExecutor.java @@ -0,0 +1,75 @@ +/* + * 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.workflow; + +import org.sonar.api.BatchComponent; +import org.sonar.api.ServerComponent; +import org.sonar.api.issue.Issue; +import org.sonar.core.issue.DefaultIssue; +import org.sonar.core.issue.IssueChangeContext; +import org.sonar.core.issue.IssueUpdater; + +public class FunctionExecutor implements BatchComponent, ServerComponent { + + private final IssueUpdater updater; + + public FunctionExecutor(IssueUpdater updater) { + this.updater = updater; + } + + public void execute(Function[] functions, DefaultIssue issue, IssueChangeContext changeContext) { + if (functions.length > 0) { + FunctionContext functionContext = new FunctionContext(updater, issue, changeContext); + for (Function function : functions) { + function.execute(functionContext); + } + } + } + + static class FunctionContext implements Function.Context { + + private final IssueUpdater updater; + private final DefaultIssue issue; + private final IssueChangeContext changeContext; + + FunctionContext(IssueUpdater updater, DefaultIssue issue, IssueChangeContext changeContext) { + this.updater = updater; + this.issue = issue; + this.changeContext = changeContext; + } + + @Override + public Issue issue() { + return issue; + } + + @Override + public Function.Context setResolution(String s) { + updater.setResolution(issue, s); + return this; + } + + @Override + public Function.Context setClosedDate(boolean b) { + updater.setClosedDate(issue, b ? changeContext.date() : null); + return null; + } + } +} 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 3d630bc4f4c..f536d2c534c 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 @@ -25,30 +25,23 @@ import org.sonar.api.ServerComponent; import org.sonar.api.issue.DefaultTransitions; import org.sonar.api.issue.Issue; import org.sonar.core.issue.DefaultIssue; +import org.sonar.core.issue.IssueChangeContext; import java.util.List; public class IssueWorkflow implements BatchComponent, ServerComponent, Startable { private StateMachine machine; + private final FunctionExecutor functionExecutor; + + public IssueWorkflow(FunctionExecutor functionExecutor) { + this.functionExecutor = functionExecutor; + } @Override public void start() { machine = StateMachine.builder() .states(Issue.STATUS_OPEN, Issue.STATUS_REOPENED, Issue.STATUS_RESOLVED, Issue.STATUS_CLOSED) - .transition(Transition.builder(DefaultTransitions.CLOSE) - .from(Issue.STATUS_OPEN).to(Issue.STATUS_CLOSED) - .functions(new SetResolution(Issue.RESOLUTION_FIXED), SetClosedAt.CLOSED_AT) - .build()) - .transition(Transition.builder(DefaultTransitions.CLOSE) - .from(Issue.STATUS_RESOLVED).to(Issue.STATUS_CLOSED) - .functions(SetClosedAt.CLOSED_AT) - .build()) - .transition(Transition.builder(DefaultTransitions.CLOSE) - .from(Issue.STATUS_REOPENED).to(Issue.STATUS_CLOSED) - .functions(SetClosedAt.CLOSED_AT) - .functions(new SetResolution(Issue.RESOLUTION_FIXED)) - .build()) .transition(Transition.builder(DefaultTransitions.RESOLVE) .from(Issue.STATUS_OPEN).to(Issue.STATUS_RESOLVED) .functions(new SetResolution(Issue.RESOLUTION_FIXED)) @@ -63,7 +56,7 @@ public class IssueWorkflow implements BatchComponent, ServerComponent, Startable .build()) .transition(Transition.builder(DefaultTransitions.REOPEN) .from(Issue.STATUS_CLOSED).to(Issue.STATUS_REOPENED) - .functions(new SetResolution(Issue.RESOLUTION_OPEN), new UnsetClosedAt()) + .functions(new SetResolution(Issue.RESOLUTION_OPEN), new SetClosedDate(false)) .build()) .transition(Transition.builder(DefaultTransitions.FALSE_POSITIVE) .from(Issue.STATUS_OPEN).to(Issue.STATUS_RESOLVED) @@ -82,13 +75,13 @@ public class IssueWorkflow implements BatchComponent, ServerComponent, Startable .transition(Transition.builder("automaticclose") .from(Issue.STATUS_OPEN).to(Issue.STATUS_CLOSED) .conditions(new IsAlive(false), new IsManual(false)) - .functions(new SetResolution(Issue.RESOLUTION_FIXED), SetClosedAt.CLOSED_AT) + .functions(new SetResolution(Issue.RESOLUTION_FIXED), new SetClosedDate(true)) .automatic() .build()) .transition(Transition.builder("automaticclose") .from(Issue.STATUS_REOPENED).to(Issue.STATUS_CLOSED) .conditions(new IsAlive(false)) - .functions(new SetResolution(Issue.RESOLUTION_FIXED), SetClosedAt.CLOSED_AT) + .functions(new SetResolution(Issue.RESOLUTION_FIXED), new SetClosedDate(true)) .automatic() .build()) // Close the issues marked as resolved and that do not exist anymore. @@ -96,7 +89,7 @@ public class IssueWorkflow implements BatchComponent, ServerComponent, Startable .transition(Transition.builder("automaticclose") .from(Issue.STATUS_RESOLVED).to(Issue.STATUS_CLOSED) .conditions(new IsAlive(false)) - .functions(SetClosedAt.CLOSED_AT) + .functions(new SetClosedDate(true)) .automatic() .build()) .transition(Transition.builder("automaticreopen") @@ -112,24 +105,26 @@ public class IssueWorkflow implements BatchComponent, ServerComponent, Startable public void stop() { } - public boolean doManualTransition(DefaultIssue issue, String transitionKey) { + public boolean doTransition(DefaultIssue issue, String transitionKey, IssueChangeContext issueChangeContext) { Transition transition = machine.state(issue.status()).transition(transitionKey); if (transition != null && !transition.automatic()) { - transition.execute(issue); + functionExecutor.execute(transition.functions(), issue, issueChangeContext); + issue.setStatus(transition.to()); return true; } return false; } - public List<Transition> outManualTransitions(Issue issue) { + public List<Transition> outTransitions(Issue issue) { return machine.state(issue.status()).outManualTransitions(issue); } - public void doAutomaticTransition(DefaultIssue issue) { + public void doAutomaticTransition(DefaultIssue issue, IssueChangeContext issueChangeContext) { Transition transition = machine.state(issue.status()).outAutomaticTransition(issue); if (transition != null) { - transition.execute(issue); + functionExecutor.execute(transition.functions(), issue, issueChangeContext); + issue.setStatus(transition.to()); } } diff --git a/sonar-core/src/main/java/org/sonar/core/issue/workflow/UnsetClosedAt.java b/sonar-core/src/main/java/org/sonar/core/issue/workflow/SetClosedDate.java index 87540a123c4..203cc473d74 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/workflow/UnsetClosedAt.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/workflow/SetClosedDate.java @@ -19,13 +19,15 @@ */ package org.sonar.core.issue.workflow; -import org.sonar.core.issue.DefaultIssue; +public class SetClosedDate implements Function { + private final boolean set; -class UnsetClosedAt implements Function { + public SetClosedDate(boolean set) { + this.set = set; + } @Override - public void execute(DefaultIssue issue) { - issue.setClosedAt(null); + public void execute(Context context) { + context.setClosedDate(set); } - } diff --git a/sonar-core/src/main/java/org/sonar/core/issue/workflow/SetResolution.java b/sonar-core/src/main/java/org/sonar/core/issue/workflow/SetResolution.java index c18bbc5493f..e5147d4bf99 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/workflow/SetResolution.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/workflow/SetResolution.java @@ -21,19 +21,17 @@ package org.sonar.core.issue.workflow; import com.google.common.base.Preconditions; import com.google.common.base.Strings; -import org.sonar.core.issue.DefaultIssue; -class SetResolution implements Function { +public class SetResolution implements Function { private final String resolution; - SetResolution(String resolution) { + public SetResolution(String resolution) { Preconditions.checkArgument(!Strings.isNullOrEmpty(resolution), "Resolution must be set"); this.resolution = resolution; } @Override - public void execute(DefaultIssue issue) { - issue.setResolution(resolution); + public void execute(Context context) { + context.setResolution(resolution); } - } diff --git a/sonar-core/src/main/java/org/sonar/core/issue/workflow/Transition.java b/sonar-core/src/main/java/org/sonar/core/issue/workflow/Transition.java index 632b944f711..ce4b4aecdb2 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/workflow/Transition.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/workflow/Transition.java @@ -24,10 +24,8 @@ import com.google.common.base.Strings; import com.google.common.collect.Lists; import org.apache.commons.lang.StringUtils; import org.sonar.api.issue.Issue; -import org.sonar.core.issue.DefaultIssue; import java.util.Arrays; -import java.util.Date; import java.util.List; public class Transition { @@ -79,13 +77,6 @@ public class Transition { return true; } - public void execute(DefaultIssue issue) { - for (Function function : functions) { - function.execute(issue); - } - issue.setStatus(to); - issue.setUpdatedAt(new Date()); - } public static TransitionBuilder builder(String key) { return new TransitionBuilder(key); 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 4a0834f8fad..e625470bedc 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 @@ -42,7 +42,7 @@ public class DefaultIssueBuilderTest { .build(); assertThat(issue).isNotNull(); - assertThat(issue.key()).isNull(); + assertThat(issue.key()).isNotNull(); assertThat(issue.cost()).isEqualTo(10000.0); assertThat(issue.componentKey()).isEqualTo(componentKey); assertThat(issue.description()).isEqualTo("the desc"); @@ -50,7 +50,7 @@ public class DefaultIssueBuilderTest { assertThat(issue.ruleKey().repository()).isEqualTo("squid"); assertThat(issue.ruleKey().rule()).isEqualTo("NullDereference"); assertThat(issue.severity()).isEqualTo(Severity.CRITICAL); - assertThat(issue.updatedAt()).isNull(); + assertThat(issue.updatedAt()).isNotNull(); assertThat(issue.closedAt()).isNull(); assertThat(issue.assignee()).isNull(); assertThat(issue.isNew()).isTrue(); 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 8600e27ba99..89aeb4fbd5a 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 @@ -25,7 +25,6 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.sonar.api.issue.Issue; import org.sonar.core.issue.DefaultIssue; -import org.sonar.core.issue.db.IssueDto; import java.util.Date; @@ -86,7 +85,7 @@ public class IssueDtoTest { assertThat(issue.line()).isEqualTo(6); assertThat(issue.severity()).isEqualTo("BLOCKER"); assertThat(issue.description()).isEqualTo("message"); - assertThat(issue.isManualSeverity()).isTrue(); + assertThat(issue.manualSeverity()).isTrue(); assertThat(issue.manual()).isTrue(); assertThat(issue.userLogin()).isEqualTo("arthur"); assertThat(issue.assignee()).isEqualTo("perceval"); 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 77abe017209..19d07e6c213 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 @@ -24,15 +24,19 @@ import com.google.common.collect.Collections2; import org.junit.Test; import org.sonar.api.issue.Issue; import org.sonar.core.issue.DefaultIssue; +import org.sonar.core.issue.IssueChangeContext; +import org.sonar.core.issue.IssueUpdater; import javax.annotation.Nullable; import java.util.Collection; +import java.util.Date; import java.util.List; import static org.fest.assertions.Assertions.assertThat; +import static org.mockito.Mockito.mock; public class IssueWorkflowTest { - IssueWorkflow workflow = new IssueWorkflow(); + IssueWorkflow workflow = new IssueWorkflow(new FunctionExecutor(new IssueUpdater())); @Test public void should_init_state_machine() throws Exception { @@ -51,9 +55,9 @@ public class IssueWorkflowTest { workflow.start(); DefaultIssue issue = new DefaultIssue().setStatus(Issue.STATUS_OPEN); - List<Transition> transitions = workflow.outManualTransitions(issue); - assertThat(transitions).hasSize(3); - assertThat(keys(transitions)).containsOnly("close", "falsepositive", "resolve"); + List<Transition> transitions = workflow.outTransitions(issue); + assertThat(transitions).hasSize(2); + assertThat(keys(transitions)).containsOnly("falsepositive", "resolve"); } @Test @@ -65,7 +69,7 @@ public class IssueWorkflowTest { .setStatus(Issue.STATUS_RESOLVED) .setNew(false) .setAlive(false); - workflow.doAutomaticTransition(issue); + workflow.doAutomaticTransition(issue, IssueChangeContext.createScan(new Date())); assertThat(issue.resolution()).isEqualTo(Issue.RESOLUTION_FIXED); assertThat(issue.status()).isEqualTo(Issue.STATUS_CLOSED); assertThat(issue.closedAt()).isNotNull(); diff --git a/sonar-core/src/test/java/org/sonar/core/issue/workflow/SetResolutionTest.java b/sonar-core/src/test/java/org/sonar/core/issue/workflow/SetResolutionTest.java deleted file mode 100644 index 2ab06515ada..00000000000 --- a/sonar-core/src/test/java/org/sonar/core/issue/workflow/SetResolutionTest.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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.workflow; - -import org.junit.Test; -import org.sonar.api.issue.Issue; -import org.sonar.core.issue.DefaultIssue; - -import static org.fest.assertions.Assertions.assertThat; -import static org.fest.assertions.Fail.fail; - -public class SetResolutionTest { - @Test - public void should_set_resolution() throws Exception { - DefaultIssue issue = new DefaultIssue(); - SetResolution function = new SetResolution(Issue.RESOLUTION_FIXED); - function.execute(issue); - assertThat(issue.resolution()).isEqualTo(Issue.RESOLUTION_FIXED); - } - - @Test - public void resolution_should_not_be_empty() throws Exception { - try { - new SetResolution(""); - fail(); - } catch (IllegalArgumentException e) { - assertThat(e).hasMessage("Resolution must be set"); - } - } -} 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 4f068afe6cc..caf383720f4 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 @@ -25,7 +25,6 @@ import org.sonar.core.issue.DefaultIssue; import static org.fest.assertions.Assertions.assertThat; import static org.fest.assertions.Fail.fail; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class TransitionTest { @@ -102,22 +101,6 @@ public class TransitionTest { } @Test - public void should_execute_functions() throws Exception { - DefaultIssue issue = new DefaultIssue(); - Transition transition = Transition.builder("close") - .from("OPEN").to("CLOSED") - .conditions(condition1, condition2) - .functions(function1, function2) - .build(); - transition.execute(issue); - - assertThat(issue.status()).isEqualTo("CLOSED"); - assertThat(issue.updatedAt()).isNotNull(); - verify(function1).execute(issue); - verify(function2).execute(issue); - } - - @Test public void should_verify_conditions() throws Exception { DefaultIssue issue = new DefaultIssue(); Transition transition = Transition.builder("close") diff --git a/sonar-core/src/test/java/org/sonar/core/issue/workflow/UnsetClosedAtTest.java b/sonar-core/src/test/java/org/sonar/core/issue/workflow/UnsetClosedAtTest.java deleted file mode 100644 index e74f78b9a1d..00000000000 --- a/sonar-core/src/test/java/org/sonar/core/issue/workflow/UnsetClosedAtTest.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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.workflow; - -import org.junit.Test; -import org.sonar.core.issue.DefaultIssue; - -import java.util.Date; - -import static org.fest.assertions.Assertions.assertThat; - -public class UnsetClosedAtTest { - @Test - public void should_remove_date() throws Exception { - UnsetClosedAt function = new UnsetClosedAt(); - DefaultIssue issue = new DefaultIssue().setCreatedAt(new Date()).setCreatedAt(new Date()); - function.execute(issue); - assertThat(issue.closedAt()).isNull(); - assertThat(issue.createdAt()).isNotNull(); - } -} diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/issue/IssueHandler.java b/sonar-plugin-api/src/main/java/org/sonar/api/issue/IssueHandler.java index 6ba7c0edb7c..8b7a6aadcc4 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/issue/IssueHandler.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/issue/IssueHandler.java @@ -28,31 +28,29 @@ import javax.annotation.Nullable; */ public interface IssueHandler extends BatchExtension { - interface IssueContext { + interface Context { Issue issue(); boolean isNew(); boolean isAlive(); - IssueContext setLine(@Nullable Integer line); + Context setLine(@Nullable Integer line); - IssueContext setDescription(String description); + Context setDescription(@Nullable String description); - // set manual severity ? - IssueContext setSeverity(String severity); + Context setSeverity(String severity); - // TODO rename to setScmLogin ? - IssueContext setAuthorLogin(@Nullable String login); + Context setAuthorLogin(@Nullable String login); - IssueContext setAttribute(String key, @Nullable String value); + Context setAttribute(String key, @Nullable String value); - IssueContext assignTo(@Nullable String login); + Context assign(@Nullable String login); - //TODO IssueContext comment(String comment); + Context comment(String message); } - void onIssue(IssueContext context); + void onIssue(Context context); } diff --git a/sonar-server/src/main/java/org/sonar/server/issue/JRubyInternalIssues.java b/sonar-server/src/main/java/org/sonar/server/issue/JRubyInternalIssues.java index fab07960dcd..a529f1206fd 100644 --- a/sonar-server/src/main/java/org/sonar/server/issue/JRubyInternalIssues.java +++ b/sonar-server/src/main/java/org/sonar/server/issue/JRubyInternalIssues.java @@ -21,10 +21,14 @@ package org.sonar.server.issue; import org.sonar.api.ServerComponent; import org.sonar.api.issue.Issue; +import org.sonar.api.rule.RuleKey; +import org.sonar.core.issue.DefaultIssue; +import org.sonar.core.issue.DefaultIssueBuilder; import org.sonar.core.issue.workflow.Transition; import org.sonar.server.platform.UserSession; import java.util.List; +import java.util.Map; /** * All the issue features that are not published to public API @@ -38,10 +42,34 @@ public class JRubyInternalIssues implements ServerComponent { } public List<Transition> listTransitions(String issueKey) { - return actions.listTransitions(issueKey, UserSession.get().userId()); + return actions.listTransitions(issueKey, UserSession.get()); } public Issue doTransition(String issueKey, String transitionKey) { return actions.doTransition(issueKey, transitionKey, UserSession.get()); } + + public Issue assign(String issueKey, String transitionKey) { + return actions.assign(issueKey, transitionKey, UserSession.get()); + } + + public Issue setSeverity(String issueKey, String severity) { + return actions.setSeverity(issueKey, severity, UserSession.get()); + } + + public Issue create(Map<String, String> parameters) { + String componentKey = parameters.get("component"); + // TODO verify authorization + // TODO check existence of component + DefaultIssueBuilder builder = new DefaultIssueBuilder(componentKey); + String line = parameters.get("line"); + builder.line(line != null ? Integer.parseInt(line) : null); + builder.description(parameters.get("description")); + builder.severity(parameters.get("severity")); + // TODO verify existence of rule + builder.ruleKey(RuleKey.parse(parameters.get("rule"))); + builder.manual(true); + Issue issue = builder.build(); + return actions.create((DefaultIssue) issue, UserSession.get()); + } } diff --git a/sonar-server/src/main/java/org/sonar/server/issue/ServerIssueActions.java b/sonar-server/src/main/java/org/sonar/server/issue/ServerIssueActions.java index cb902736e47..e56f966fc32 100644 --- a/sonar-server/src/main/java/org/sonar/server/issue/ServerIssueActions.java +++ b/sonar-server/src/main/java/org/sonar/server/issue/ServerIssueActions.java @@ -21,19 +21,25 @@ package org.sonar.server.issue; import org.sonar.api.ServerComponent; import org.sonar.api.issue.Issue; -import org.sonar.api.issue.IssueFinder; +import org.sonar.api.rules.Rule; +import org.sonar.api.rules.RuleFinder; 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 org.sonar.core.issue.db.IssueDao; import org.sonar.core.issue.db.IssueDto; import org.sonar.core.issue.workflow.IssueWorkflow; import org.sonar.core.issue.workflow.Transition; +import org.sonar.core.resource.ResourceDao; +import org.sonar.core.resource.ResourceDto; +import org.sonar.core.resource.ResourceQuery; import org.sonar.core.user.AuthorizationDao; import org.sonar.server.platform.UserSession; import javax.annotation.Nullable; import java.util.Arrays; -import java.util.Collections; +import java.util.Date; import java.util.List; /** @@ -42,27 +48,73 @@ import java.util.List; public class ServerIssueActions implements ServerComponent { private final IssueWorkflow workflow; - private final IssueFinder finder; private final IssueDao issueDao; private final AuthorizationDao authorizationDao; + private final ResourceDao resourceDao; + private final RuleFinder ruleFinder; + private final IssueUpdater issueUpdater; - public ServerIssueActions(IssueWorkflow workflow, IssueFinder finder, IssueDao issueDao, AuthorizationDao authorizationDao) { + public ServerIssueActions(IssueWorkflow workflow, IssueDao issueDao, + AuthorizationDao authorizationDao, ResourceDao resourceDao, RuleFinder ruleFinder, IssueUpdater issueUpdater) { this.workflow = workflow; - this.finder = finder; this.issueDao = issueDao; this.authorizationDao = authorizationDao; + this.resourceDao = resourceDao; + this.ruleFinder = ruleFinder; + this.issueUpdater = issueUpdater; } - public List<Transition> listTransitions(String issueKey, @Nullable Integer userId) { - Issue issue = finder.findByKey(issueKey /*, userId */); - // TODO check authorization - if (issue == null) { - return Collections.emptyList(); - } - return workflow.outManualTransitions(issue); + public List<Transition> listTransitions(String issueKey, UserSession userSession) { + IssueDto dto = loadDto(issueKey, userSession); + DefaultIssue issue = dto.toDefaultIssue(); + return workflow.outTransitions(issue); } public Issue doTransition(String issueKey, String transition, UserSession userSession) { + IssueDto dto = loadDto(issueKey, userSession); + DefaultIssue issue = dto.toDefaultIssue(); + IssueChangeContext context = IssueChangeContext.createUser(new Date(), userSession.login()); + if (workflow.doTransition(issue, transition, context)) { + issueDao.update(Arrays.asList(IssueDto.toDto(issue, dto.getResourceId(), dto.getRuleId()))); + } + return issue; + } + + public Issue assign(String issueKey, @Nullable String assigneeLogin, UserSession userSession) { + IssueDto dto = loadDto(issueKey, userSession); + DefaultIssue issue = dto.toDefaultIssue(); + + // TODO check that assignee exists + if (issueUpdater.assign(issue, assigneeLogin)) { + issueDao.update(Arrays.asList(IssueDto.toDto(issue, dto.getResourceId(), dto.getRuleId()))); + } + return issue; + } + + public Issue setSeverity(String issueKey, String severity, UserSession userSession) { + IssueDto dto = loadDto(issueKey, userSession); + DefaultIssue issue = dto.toDefaultIssue(); + + if (issueUpdater.setSeverity(issue, severity)) { + issueDao.update(Arrays.asList(IssueDto.toDto(issue, dto.getResourceId(), dto.getRuleId()))); + } + return issue; + } + + public Issue create(DefaultIssue issue, UserSession userSession) { + issue.setManual(true); + issue.setUserLogin(userSession.login()); + + // TODO check that rule and component exist + Rule rule = ruleFinder.findByKey(issue.ruleKey()); + ResourceDto resourceDto = resourceDao.getResource(ResourceQuery.create().setKey(issue.componentKey())); + IssueDto dto = IssueDto.toDto(issue, resourceDto.getId().intValue(), rule.getId()); + issueDao.insert(dto); + + return issue; + } + + private IssueDto loadDto(String issueKey, UserSession userSession) { if (!userSession.isLoggedIn()) { // must be logged throw new IllegalStateException("User is not logged in"); @@ -75,10 +127,6 @@ public class ServerIssueActions implements ServerComponent { if (!authorizationDao.isAuthorizedComponentId(dto.getResourceId(), userSession.userId(), requiredRole)) { throw new IllegalStateException("User does not have the role " + requiredRole + " required to change the issue: " + issueKey); } - DefaultIssue issue = dto.toDefaultIssue(); - if (workflow.doManualTransition(issue, transition)) { - issueDao.update(Arrays.asList(IssueDto.toDto(issue, dto.getResourceId(), dto.getRuleId()))); - } - return issue; + return dto; } } diff --git a/sonar-server/src/main/java/org/sonar/server/platform/Platform.java b/sonar-server/src/main/java/org/sonar/server/platform/Platform.java index c02d1a7e1cd..896ddb28287 100644 --- a/sonar-server/src/main/java/org/sonar/server/platform/Platform.java +++ b/sonar-server/src/main/java/org/sonar/server/platform/Platform.java @@ -40,6 +40,8 @@ import org.sonar.core.config.Logback; import org.sonar.core.i18n.GwtI18n; import org.sonar.core.i18n.I18nManager; import org.sonar.core.i18n.RuleI18nManager; +import org.sonar.core.issue.IssueUpdater; +import org.sonar.core.issue.workflow.FunctionExecutor; import org.sonar.core.issue.workflow.IssueWorkflow; import org.sonar.core.measure.MeasureFilterEngine; import org.sonar.core.measure.MeasureFilterExecutor; @@ -240,6 +242,8 @@ public final class Platform { servicesContainer.addSingleton(Periods.class); // issues + servicesContainer.addSingleton(IssueUpdater.class); + servicesContainer.addSingleton(FunctionExecutor.class); servicesContainer.addSingleton(IssueWorkflow.class); servicesContainer.addSingleton(ServerIssueActions.class); servicesContainer.addSingleton(ServerIssueFinder.class); diff --git a/sonar-server/src/main/webapp/WEB-INF/app/controllers/api/issues_controller.rb b/sonar-server/src/main/webapp/WEB-INF/app/controllers/api/issues_controller.rb index 4886e1e8df7..f473065fc85 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/controllers/api/issues_controller.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/controllers/api/issues_controller.rb @@ -54,20 +54,54 @@ class Api::IssuesController < Api::ApiController issue = Internal.issues.doTransition(params[:issue], params[:transition]) if issue render :json => jsonp({ - :issue => issue_to_json(issue) - }) + :issue => issue_to_json(issue) + }) else render :status => 400 end end - # POST /api/issues/create?severity=xxx>&<resolution=xxx>&component=<component key> + # POST /api/issues/add_comment?issue=<key>&text=<text> + # Note that the text can also be set in the post body + def add_comment + verify_post_request + require_parameters :issue, :text + + text = Api::Utils.read_post_request_param(:text) + Internal.issues.addComment(params[:issue], text) + # TODO add more response data ? + render :json => jsonp({}) + end + + # POST /api/issues/assign?issue=<key>&assignee=<optional assignee> + # A nil assignee will remove the assignee. + def assign + verify_post_request + require_parameters :issue + + Internal.issues.assign(params[:issue], params[:assignee]) + # TODO return the assignee + render :json => jsonp({}) + end + + # POST /api/issues/create + # + # Mandatory parameters + # 'component' is the component key + # 'rule' includes the repository key and the rule key, for example 'squid:AvoidCycle' + # + # Optional parameters + # 'severity' is in BLOCKER, CRITICAL, ... INFO. Default value is MAJOR. + # 'line' starts at 1 + # 'description' is the plain-text description + # def create verify_post_request access_denied unless logged_in? + require_parameters :component, :rule - # TODO - render :json => jsonp({}) + issue = Internal.issues.create(params) + render :json => jsonp({:issue => issue_to_json(issue)}) end private |