summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSimon Brandhof <simon.brandhof@gmail.com>2013-04-22 17:57:20 +0200
committerSimon Brandhof <simon.brandhof@gmail.com>2013-04-22 17:57:41 +0200
commite5e0c1076f177f79ab3024d8800e1c01ae7c40a2 (patch)
treecb434fc24d939d945e0f0e12fb6134441515be49
parentd78b5f1ef8a5516d18ae8ba0b158eb57a5c8777d (diff)
downloadsonarqube-e5e0c1076f177f79ab3024d8800e1c01ae7c40a2.tar.gz
sonarqube-e5e0c1076f177f79ab3024d8800e1c01ae7c40a2.zip
SONAR-3755 first draft of issue state machine
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/issue/ScanIssueChanges.java9
-rw-r--r--sonar-batch/src/test/java/org/sonar/batch/issue/ScanIssueChangesTest.java27
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java2
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/IssueDto.java2
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/UpdateIssueFields.java (renamed from sonar-core/src/main/java/org/sonar/core/issue/ApplyIssueChange.java)5
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/workflow/Condition.java28
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/workflow/Function.java26
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/workflow/IsNotManualIssue.java30
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/workflow/IssueWorkflow.java130
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/workflow/SetResolution.java36
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/workflow/State.java82
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/workflow/StateMachine.java77
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/workflow/Transition.java126
-rw-r--r--sonar-core/src/test/java/org/sonar/core/issue/IssueDtoTest.java2
-rw-r--r--sonar-core/src/test/java/org/sonar/core/issue/UpdateIssueFieldsTest.java (renamed from sonar-core/src/test/java/org/sonar/core/issue/ApplyIssueChangeTest.java)8
-rw-r--r--sonar-core/src/test/java/org/sonar/core/issue/workflow/TransitionTest.java113
-rw-r--r--sonar-plugin-api/src/main/java/org/sonar/api/issue/DefaultTransitions.java30
-rw-r--r--sonar-plugin-api/src/main/java/org/sonar/api/issue/Issue.java5
-rw-r--r--sonar-plugin-api/src/main/java/org/sonar/api/issue/IssueChange.java12
-rw-r--r--sonar-plugin-api/src/test/java/org/sonar/api/issue/IssueChangeTest.java6
-rw-r--r--sonar-server/src/main/java/org/sonar/server/issue/DefaultJRubyIssues.java4
-rw-r--r--sonar-server/src/main/java/org/sonar/server/issue/ServerIssueChanges.java4
22 files changed, 716 insertions, 48 deletions
diff --git a/sonar-batch/src/main/java/org/sonar/batch/issue/ScanIssueChanges.java b/sonar-batch/src/main/java/org/sonar/batch/issue/ScanIssueChanges.java
index 727bf2f7dc0..0d1ce0991bb 100644
--- a/sonar-batch/src/main/java/org/sonar/batch/issue/ScanIssueChanges.java
+++ b/sonar-batch/src/main/java/org/sonar/batch/issue/ScanIssueChanges.java
@@ -22,17 +22,19 @@ package org.sonar.batch.issue;
import org.sonar.api.issue.Issue;
import org.sonar.api.issue.IssueChange;
import org.sonar.api.issue.IssueChanges;
-import org.sonar.core.issue.ApplyIssueChange;
import org.sonar.core.issue.DefaultIssue;
+import org.sonar.core.issue.workflow.IssueWorkflow;
import java.util.Date;
public class ScanIssueChanges implements IssueChanges {
private final IssueCache cache;
+ private final IssueWorkflow workflow;
- public ScanIssueChanges(IssueCache cache) {
+ public ScanIssueChanges(IssueCache cache, IssueWorkflow workflow) {
this.cache = cache;
+ this.workflow = workflow;
}
@Override
@@ -41,8 +43,7 @@ public class ScanIssueChanges implements IssueChanges {
return issue;
}
DefaultIssue reloaded = reload(issue);
- ApplyIssueChange.apply(reloaded, change);
- // TODO set the date of loading of issues
+ workflow.apply(reloaded, change);
reloaded.setUpdatedAt(new Date());
cache.addOrUpdate(reloaded);
// TODO keep history of changes
diff --git a/sonar-batch/src/test/java/org/sonar/batch/issue/ScanIssueChangesTest.java b/sonar-batch/src/test/java/org/sonar/batch/issue/ScanIssueChangesTest.java
index 1f6b22c8f4e..c833c9c1e8a 100644
--- a/sonar-batch/src/test/java/org/sonar/batch/issue/ScanIssueChangesTest.java
+++ b/sonar-batch/src/test/java/org/sonar/batch/issue/ScanIssueChangesTest.java
@@ -24,6 +24,7 @@ import org.sonar.api.issue.Issue;
import org.sonar.api.issue.IssueChange;
import org.sonar.api.rule.Severity;
import org.sonar.core.issue.DefaultIssue;
+import org.sonar.core.issue.workflow.IssueWorkflow;
import static org.fest.assertions.Assertions.assertThat;
import static org.fest.assertions.Fail.fail;
@@ -32,7 +33,8 @@ import static org.mockito.Mockito.*;
public class ScanIssueChangesTest {
IssueCache cache = mock(IssueCache.class);
- ScanIssueChanges changes = new ScanIssueChanges(cache);
+ IssueWorkflow workflow = mock(IssueWorkflow.class);
+ ScanIssueChanges changes = new ScanIssueChanges(cache, workflow);
@Test
public void should_ignore_empty_change() throws Exception {
@@ -58,24 +60,13 @@ public class ScanIssueChangesTest {
@Test
public void should_change_fields() throws Exception {
- Issue issue = new DefaultIssue().setComponentKey("org/struts/Action.java").setKey("ABCDE");
+ DefaultIssue issue = new DefaultIssue().setComponentKey("org/struts/Action.java").setKey("ABCDE");
when(cache.componentIssue("org/struts/Action.java", "ABCDE")).thenReturn(issue);
- Issue changed = changes.apply(issue, IssueChange.create()
- .setLine(200)
- .setResolution(Issue.RESOLUTION_FALSE_POSITIVE)
- .setAttribute("JIRA", "FOO-123")
- .setManualSeverity(true)
- .setSeverity(Severity.CRITICAL)
- .setAssignee("arthur")
- .setCost(4.2)
- );
+
+ IssueChange change = IssueChange.create().setTransition("resolve");
+ changes.apply(issue, change);
+
verify(cache).addOrUpdate(issue);
- assertThat(changed.line()).isEqualTo(200);
- assertThat(changed.resolution()).isEqualTo(Issue.RESOLUTION_FALSE_POSITIVE);
- assertThat(changed.attribute("JIRA")).isEqualTo("FOO-123");
- assertThat(changed.severity()).isEqualTo(Severity.CRITICAL);
- assertThat(changed.assignee()).isEqualTo("arthur");
- assertThat(changed.cost()).isEqualTo(4.2);
- assertThat(changed.updatedAt()).isNotNull();
+ verify(workflow).apply(issue, change);
}
}
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 84509ff59fd..4b623eade82 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
@@ -214,7 +214,7 @@ public class DefaultIssue implements Issue, Serializable {
return this;
}
- public boolean isManual() {
+ public boolean manual() {
return manual;
}
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/IssueDto.java b/sonar-core/src/main/java/org/sonar/core/issue/IssueDto.java
index ccd22e5e5ad..5d9f0a16ece 100644
--- a/sonar-core/src/main/java/org/sonar/core/issue/IssueDto.java
+++ b/sonar-core/src/main/java/org/sonar/core/issue/IssueDto.java
@@ -317,7 +317,7 @@ public final class IssueDto {
.setStatus(issue.status())
.setSeverity(issue.severity())
.setChecksum(issue.getChecksum())
- .setManualIssue(issue.isManual())
+ .setManualIssue(issue.manual())
.setManualSeverity(issue.isManualSeverity())
.setUserLogin(issue.userLogin())
.setAssignee(issue.assignee())
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/ApplyIssueChange.java b/sonar-core/src/main/java/org/sonar/core/issue/UpdateIssueFields.java
index 6a493c6cf59..ba6a26d7d12 100644
--- a/sonar-core/src/main/java/org/sonar/core/issue/ApplyIssueChange.java
+++ b/sonar-core/src/main/java/org/sonar/core/issue/UpdateIssueFields.java
@@ -23,7 +23,7 @@ import org.sonar.api.issue.IssueChange;
import java.util.Map;
-public class ApplyIssueChange {
+public class UpdateIssueFields {
public static void apply(DefaultIssue issue, IssueChange change) {
if (change.description() != null) {
@@ -41,9 +41,6 @@ public class ApplyIssueChange {
if (change.isAssigneeChanged()) {
issue.setAssignee(change.assignee());
}
- if (change.resolution() != null) {
- issue.setResolution(change.resolution());
- }
if (change.isLineChanged()) {
issue.setLine(change.line());
}
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/workflow/Condition.java b/sonar-core/src/main/java/org/sonar/core/issue/workflow/Condition.java
new file mode 100644
index 00000000000..dcbcefa85c7
--- /dev/null
+++ b/sonar-core/src/main/java/org/sonar/core/issue/workflow/Condition.java
@@ -0,0 +1,28 @@
+/*
+ * 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.issue.Issue;
+
+interface Condition {
+
+ boolean matches(Issue issue);
+
+}
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
new file mode 100644
index 00000000000..a45a595acbd
--- /dev/null
+++ b/sonar-core/src/main/java/org/sonar/core/issue/workflow/Function.java
@@ -0,0 +1,26 @@
+/*
+ * 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.core.issue.DefaultIssue;
+
+interface Function {
+ void execute(DefaultIssue issue);
+}
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/workflow/IsNotManualIssue.java b/sonar-core/src/main/java/org/sonar/core/issue/workflow/IsNotManualIssue.java
new file mode 100644
index 00000000000..ad035352dc6
--- /dev/null
+++ b/sonar-core/src/main/java/org/sonar/core/issue/workflow/IsNotManualIssue.java
@@ -0,0 +1,30 @@
+/*
+ * 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.issue.Issue;
+
+class IsNotManualIssue implements Condition {
+
+ @Override
+ public boolean matches(Issue issue) {
+ return !issue.manual();
+ }
+}
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
new file mode 100644
index 00000000000..a263651c563
--- /dev/null
+++ b/sonar-core/src/main/java/org/sonar/core/issue/workflow/IssueWorkflow.java
@@ -0,0 +1,130 @@
+/*
+ * 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.picocontainer.Startable;
+import org.sonar.api.BatchComponent;
+import org.sonar.api.ServerComponent;
+import org.sonar.api.issue.DefaultTransitions;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.issue.IssueChange;
+import org.sonar.core.issue.DefaultIssue;
+
+import java.util.List;
+import java.util.Map;
+
+public class IssueWorkflow implements BatchComponent, ServerComponent, Startable {
+
+ private StateMachine machine;
+
+ @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))
+ // TODO set closed at
+ .build())
+ .transition(Transition.builder(DefaultTransitions.CLOSE)
+ .from(Issue.STATUS_RESOLVED).to(Issue.STATUS_CLOSED)
+ // TODO set closed at
+ .build())
+ .transition(Transition.builder(DefaultTransitions.CLOSE)
+ .from(Issue.STATUS_REOPENED).to(Issue.STATUS_CLOSED)
+ // TODO set 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))
+ .build())
+ .transition(Transition.builder(DefaultTransitions.RESOLVE)
+ .from(Issue.STATUS_REOPENED).to(Issue.STATUS_RESOLVED)
+ .functions(new SetResolution(Issue.RESOLUTION_FIXED))
+ .build())
+ .transition(Transition.builder(DefaultTransitions.REOPEN)
+ .from(Issue.STATUS_RESOLVED).to(Issue.STATUS_REOPENED)
+ .functions(new SetResolution(Issue.RESOLUTION_OPEN))
+ .build())
+ .transition(Transition.builder(DefaultTransitions.REOPEN)
+ .from(Issue.STATUS_CLOSED).to(Issue.STATUS_REOPENED)
+ .functions(new SetResolution(Issue.RESOLUTION_OPEN))
+ .build())
+ .transition(Transition.builder(DefaultTransitions.FALSE_POSITIVE)
+ .from(Issue.STATUS_OPEN).to(Issue.STATUS_RESOLVED)
+ .conditions(new IsNotManualIssue())
+ .functions(new SetResolution(Issue.RESOLUTION_FALSE_POSITIVE))
+ .build())
+ .transition(Transition.builder(DefaultTransitions.FALSE_POSITIVE)
+ .from(Issue.STATUS_REOPENED).to(Issue.STATUS_RESOLVED)
+ .conditions(new IsNotManualIssue())
+ .functions(new SetResolution(Issue.RESOLUTION_FALSE_POSITIVE))
+ .build())
+ .build();
+ }
+
+ @Override
+ public void stop() {
+ }
+
+ public List<Transition> availableTransitions(Issue issue) {
+ return machine.state(issue.status()).outTransitions(issue);
+ }
+
+ public boolean apply(DefaultIssue issue, IssueChange change) {
+ if (change.hasChanges()) {
+ if (change.description() != null) {
+ issue.setDescription(change.description());
+ }
+ if (change.manualSeverity() != null) {
+ change.setManualSeverity(change.manualSeverity());
+ }
+ if (change.severity() != null) {
+ issue.setSeverity(change.severity());
+ }
+ if (change.title() != null) {
+ issue.setTitle(change.title());
+ }
+ if (change.isAssigneeChanged()) {
+ issue.setAssignee(change.assignee());
+ }
+ if (change.isLineChanged()) {
+ issue.setLine(change.line());
+ }
+ if (change.isCostChanged()) {
+ issue.setCost(change.cost());
+ }
+ for (Map.Entry<String, String> entry : change.attributes().entrySet()) {
+ issue.setAttribute(entry.getKey(), entry.getValue());
+ }
+ if (change.transition() != null) {
+ move(issue, change.transition());
+ }
+ return true;
+ }
+ return false;
+ }
+
+ public void move(DefaultIssue issue, String transition) {
+ State state = machine.state(issue.status());
+ state.move(issue, transition);
+ }
+}
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
new file mode 100644
index 00000000000..2ed3dc0fa27
--- /dev/null
+++ b/sonar-core/src/main/java/org/sonar/core/issue/workflow/SetResolution.java
@@ -0,0 +1,36 @@
+/*
+ * 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.core.issue.DefaultIssue;
+
+class SetResolution implements Function {
+ private final String resolution;
+
+ SetResolution(String resolution) {
+ this.resolution = resolution;
+ }
+
+ @Override
+ public void execute(DefaultIssue issue) {
+ issue.setResolution(resolution);
+ }
+
+}
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/workflow/State.java b/sonar-core/src/main/java/org/sonar/core/issue/workflow/State.java
new file mode 100644
index 00000000000..8f636df6f68
--- /dev/null
+++ b/sonar-core/src/main/java/org/sonar/core/issue/workflow/State.java
@@ -0,0 +1,82 @@
+/*
+ * 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 com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.issue.Issue;
+import org.sonar.core.issue.DefaultIssue;
+
+import java.util.List;
+import java.util.Set;
+
+public class State {
+ private final String key;
+ private final Transition[] outTransitions;
+
+ public State(String key, Transition[] outTransitions) {
+ Preconditions.checkArgument(!Strings.isNullOrEmpty(key), "State key must be set");
+ Preconditions.checkArgument(StringUtils.isAllUpperCase(key), "State key must be upper-case");
+ checkDuplications(outTransitions, key);
+
+ this.key = key;
+ this.outTransitions = outTransitions;
+ }
+
+ private static void checkDuplications(Transition[] transitions, String stateKey) {
+ Set<String> keys = Sets.newHashSet();
+ for (Transition transition : transitions) {
+ if (keys.contains(transition.key())) {
+ throw new IllegalArgumentException("Transition " + transition.key() + " is declared several times from the originating state " + stateKey);
+ }
+ keys.add(transition.key());
+ }
+ }
+
+ public List<Transition> outTransitions(Issue issue) {
+ List<Transition> result = Lists.newArrayList();
+ for (Transition transition : outTransitions) {
+ if (transition.supports(issue)) {
+ result.add(transition);
+ }
+ }
+ return result;
+ }
+
+ public void move(DefaultIssue issue, String transitionKey) {
+ Transition transition = transition(transitionKey);
+ if (!transition.supports(issue)) {
+ throw new IllegalStateException("TODO");
+ }
+ transition.execute(issue);
+ }
+
+ private Transition transition(String transitionKey) {
+ for (Transition transition : outTransitions) {
+ if (transitionKey.equals(transition.key())) {
+ return transition;
+ }
+ }
+ throw new IllegalStateException("Transition from state " + key + " does not exist: " + transitionKey);
+ }
+}
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/workflow/StateMachine.java b/sonar-core/src/main/java/org/sonar/core/issue/workflow/StateMachine.java
new file mode 100644
index 00000000000..463e462330e
--- /dev/null
+++ b/sonar-core/src/main/java/org/sonar/core/issue/workflow/StateMachine.java
@@ -0,0 +1,77 @@
+/*
+ * 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 com.google.common.base.Preconditions;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+import javax.annotation.CheckForNull;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class StateMachine {
+
+ private Map<String, State> states = Maps.newHashMap();
+
+ private StateMachine(Builder builder) {
+ for (String stateKey : builder.states) {
+ List<Transition> outTransitions = builder.outTransitions.get(stateKey);
+ State state = new State(stateKey, outTransitions.toArray(new Transition[outTransitions.size()]));
+ states.put(stateKey, state);
+ }
+ }
+
+ @CheckForNull
+ public State state(String stateKey) {
+ return states.get(stateKey);
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+ private final Set<String> states = Sets.newTreeSet();
+ // transitions per originating state
+ private final ListMultimap<String, Transition> outTransitions = ArrayListMultimap.create();
+
+ public Builder states(String... keys) {
+ states.addAll(Arrays.asList(keys));
+ return this;
+ }
+
+ public Builder transition(Transition transition) {
+ Preconditions.checkArgument(states.contains(transition.from()), "Originating state does not exist: " + transition.from());
+ Preconditions.checkArgument(states.contains(transition.to()), "Destination state does not exist: " + transition.to());
+ outTransitions.put(transition.from(), transition);
+ return this;
+ }
+
+ public StateMachine build() {
+ Preconditions.checkArgument(!states.isEmpty(), "At least one state is required");
+ return new StateMachine(this);
+ }
+ }
+}
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
new file mode 100644
index 00000000000..02decc94881
--- /dev/null
+++ b/sonar-core/src/main/java/org/sonar/core/issue/workflow/Transition.java
@@ -0,0 +1,126 @@
+/*
+ * 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 com.google.common.base.Preconditions;
+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;
+
+class Transition {
+ private final String key;
+ private final String from, to;
+ private final Condition[] conditions;
+ private final Function[] functions;
+
+ private Transition(TransitionBuilder builder) {
+ key = builder.key;
+ from = builder.from;
+ to = builder.to;
+ conditions = builder.conditions.toArray(new Condition[builder.conditions.size()]);
+ functions = builder.functions.toArray(new Function[builder.functions.size()]);
+ }
+
+ String key() {
+ return key;
+ }
+
+ String from() {
+ return from;
+ }
+
+ String to() {
+ return to;
+ }
+
+ Condition[] conditions() {
+ return conditions;
+ }
+
+ Function[] functions() {
+ return functions;
+ }
+
+ public boolean supports(Issue issue) {
+ for (Condition condition : conditions) {
+ if (!condition.matches(issue)) {
+ return false;
+ }
+ }
+ 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);
+ }
+
+ public static class TransitionBuilder {
+ private final String key;
+ private String from, to;
+ private List<Condition> conditions = Lists.newArrayList();
+ private List<Function> functions = Lists.newArrayList();
+
+ private TransitionBuilder(String key) {
+ this.key = key;
+ }
+
+ public TransitionBuilder from(String from) {
+ this.from = from;
+ return this;
+ }
+
+ public TransitionBuilder to(String to) {
+ this.to = to;
+ return this;
+ }
+
+ public TransitionBuilder conditions(Condition... c) {
+ this.conditions.addAll(Arrays.asList(c));
+ return this;
+ }
+
+ public TransitionBuilder functions(Function... f) {
+ this.functions.addAll(Arrays.asList(f));
+ return this;
+ }
+
+ public Transition build() {
+ Preconditions.checkArgument(!Strings.isNullOrEmpty(key), "Transition key must be set");
+ Preconditions.checkArgument(StringUtils.isAllLowerCase(key), "Transition key must be lower-case");
+ Preconditions.checkArgument(!Strings.isNullOrEmpty(from), "Originating status must be set");
+ Preconditions.checkArgument(!Strings.isNullOrEmpty(to), "Destination status must be set");
+ return new Transition(this);
+ }
+ }
+}
diff --git a/sonar-core/src/test/java/org/sonar/core/issue/IssueDtoTest.java b/sonar-core/src/test/java/org/sonar/core/issue/IssueDtoTest.java
index d8e6a2dc036..d7f5fcfce9c 100644
--- a/sonar-core/src/test/java/org/sonar/core/issue/IssueDtoTest.java
+++ b/sonar-core/src/test/java/org/sonar/core/issue/IssueDtoTest.java
@@ -87,7 +87,7 @@ public class IssueDtoTest {
assertThat(issue.title()).isEqualTo("title");
assertThat(issue.description()).isEqualTo("message");
assertThat(issue.isManualSeverity()).isTrue();
- assertThat(issue.isManual()).isTrue();
+ assertThat(issue.manual()).isTrue();
assertThat(issue.userLogin()).isEqualTo("arthur");
assertThat(issue.assignee()).isEqualTo("perceval");
assertThat(issue.attribute("key")).isEqualTo("value");
diff --git a/sonar-core/src/test/java/org/sonar/core/issue/ApplyIssueChangeTest.java b/sonar-core/src/test/java/org/sonar/core/issue/UpdateIssueFieldsTest.java
index b538cf1c269..4e17798bdb3 100644
--- a/sonar-core/src/test/java/org/sonar/core/issue/ApplyIssueChangeTest.java
+++ b/sonar-core/src/test/java/org/sonar/core/issue/UpdateIssueFieldsTest.java
@@ -26,14 +26,13 @@ import org.sonar.api.rule.Severity;
import static org.fest.assertions.Assertions.assertThat;
-public class ApplyIssueChangeTest {
+public class UpdateIssueFieldsTest {
@Test
public void should_change_fields() throws Exception {
DefaultIssue issue = new DefaultIssue().setComponentKey("org/struts/Action.java").setKey("ABCDE");
- ApplyIssueChange.apply(issue, IssueChange.create()
+ UpdateIssueFields.apply(issue, IssueChange.create()
.setLine(200)
- .setResolution(Issue.RESOLUTION_FALSE_POSITIVE)
.setAttribute("JIRA", "FOO-123")
.setManualSeverity(true)
.setSeverity(Severity.CRITICAL)
@@ -43,7 +42,6 @@ public class ApplyIssueChangeTest {
.setCost(4.2)
);
assertThat(issue.line()).isEqualTo(200);
- assertThat(issue.resolution()).isEqualTo(Issue.RESOLUTION_FALSE_POSITIVE);
assertThat(issue.title()).isEqualTo("new title");
assertThat(issue.description()).isEqualTo("new desc");
assertThat(issue.attribute("JIRA")).isEqualTo("FOO-123");
@@ -67,7 +65,7 @@ public class ApplyIssueChangeTest {
.setSeverity("BLOCKER")
.setStatus("CLOSED")
.setResolution("FIXED");
- ApplyIssueChange.apply(issue, IssueChange.create());
+ UpdateIssueFields.apply(issue, IssueChange.create());
assertThat(issue.componentKey()).isEqualTo("org/struts/Action.java");
assertThat(issue.key()).isEqualTo("ABCDE");
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
new file mode 100644
index 00000000000..c3ab3e52bbc
--- /dev/null
+++ b/sonar-core/src/test/java/org/sonar/core/issue/workflow/TransitionTest.java
@@ -0,0 +1,113 @@
+/*
+ * 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 static org.fest.assertions.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+public class TransitionTest {
+
+ Condition condition1 = mock(Condition.class);
+ Condition condition2 = mock(Condition.class);
+ Function function1 = mock(Function.class);
+ Function function2 = mock(Function.class);
+
+ @Test
+ public void test_builder() throws Exception {
+ Transition transition = Transition.builder("close")
+ .from("OPEN").to("CLOSED")
+ .conditions(condition1, condition2)
+ .functions(function1, function2)
+ .build();
+ assertThat(transition.key()).isEqualTo("close");
+ assertThat(transition.from()).isEqualTo("OPEN");
+ assertThat(transition.to()).isEqualTo("CLOSED");
+ assertThat(transition.conditions()).containsOnly(condition1, condition2);
+ assertThat(transition.functions()).containsOnly(function1, function2);
+ }
+
+ @Test
+ public void test_simplest_transition() throws Exception {
+ Transition transition = Transition.builder("close")
+ .from("OPEN").to("CLOSED")
+ .build();
+ assertThat(transition.key()).isEqualTo("close");
+ assertThat(transition.from()).isEqualTo("OPEN");
+ assertThat(transition.to()).isEqualTo("CLOSED");
+ assertThat(transition.conditions()).isEmpty();
+ assertThat(transition.functions()).isEmpty();
+ }
+
+ @Test
+ public void key_should_be_set() throws Exception {
+ try {
+ Transition.builder("").from("OPEN").to("CLOSED").build();
+ } catch (Exception e) {
+ assertThat(e).hasMessage("Transition key must be set");
+ }
+ }
+
+ @Test
+ public void key_should_be_lower_case() throws Exception {
+ try {
+ Transition.builder("CLOSE").from("OPEN").to("CLOSED").build();
+ } catch (Exception e) {
+ assertThat(e).hasMessage("Transition key must be lower-case");
+ }
+ }
+
+ @Test
+ public void originating_status_should_be_set() throws Exception {
+ try {
+ Transition.builder("close").from("").to("CLOSED").build();
+ } catch (Exception e) {
+ assertThat(e).hasMessage("Originating status must be set");
+ }
+ }
+
+ @Test
+ public void destination_status_should_be_set() throws Exception {
+ try {
+ Transition.builder("close").from("OPEN").to("").build();
+ } catch (Exception e) {
+ assertThat(e).hasMessage("Destination status must be set");
+ }
+ }
+
+ @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);
+ }
+}
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/issue/DefaultTransitions.java b/sonar-plugin-api/src/main/java/org/sonar/api/issue/DefaultTransitions.java
new file mode 100644
index 00000000000..8ba23b9aa83
--- /dev/null
+++ b/sonar-plugin-api/src/main/java/org/sonar/api/issue/DefaultTransitions.java
@@ -0,0 +1,30 @@
+/*
+ * 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.api.issue;
+
+/**
+ * @since 3.6
+ */
+public interface DefaultTransitions {
+ String REOPEN = "reopen";
+ String RESOLVE = "resolve";
+ String FALSE_POSITIVE = "falsepositive";
+ String CLOSE = "close";
+}
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/issue/Issue.java b/sonar-plugin-api/src/main/java/org/sonar/api/issue/Issue.java
index f4143eb8fc2..d842bec2d4e 100644
--- a/sonar-plugin-api/src/main/java/org/sonar/api/issue/Issue.java
+++ b/sonar-plugin-api/src/main/java/org/sonar/api/issue/Issue.java
@@ -35,8 +35,9 @@ public interface Issue {
String STATUS_RESOLVED = "RESOLVED";
String STATUS_CLOSED = "CLOSED";
- String RESOLUTION_FALSE_POSITIVE = "FALSE-POSITIVE";
+ String RESOLUTION_OPEN = "OPEN";
String RESOLUTION_FIXED = "FIXED";
+ String RESOLUTION_FALSE_POSITIVE = "FALSE-POSITIVE";
/**
* Unique generated key
@@ -65,6 +66,8 @@ public interface Issue {
String assignee();
+ boolean manual();
+
Date createdAt();
Date updatedAt();
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/issue/IssueChange.java b/sonar-plugin-api/src/main/java/org/sonar/api/issue/IssueChange.java
index 1d1bc4fe062..4db95704a2b 100644
--- a/sonar-plugin-api/src/main/java/org/sonar/api/issue/IssueChange.java
+++ b/sonar-plugin-api/src/main/java/org/sonar/api/issue/IssueChange.java
@@ -38,7 +38,7 @@ public class IssueChange {
private Integer line = null;
private boolean costChanged = false;
private Double cost = null;
- private String resolution = null;
+ private String transition = null;
private boolean assigneeChanged = false;
private String assignee = null;
private String title = null;
@@ -53,7 +53,7 @@ public class IssueChange {
public boolean hasChanges() {
return severity != null || comment != null || manualSeverity != null || description != null ||
- lineChanged || costChanged || resolution != null || assigneeChanged || attributes != null;
+ lineChanged || costChanged || transition != null || assigneeChanged || attributes != null;
}
public IssueChange setSeverity(String s) {
@@ -98,8 +98,8 @@ public class IssueChange {
return this;
}
- public IssueChange setResolution(String resolution) {
- this.resolution = resolution;
+ public IssueChange setTransition(String transition) {
+ this.transition = transition;
return this;
}
@@ -157,8 +157,8 @@ public class IssueChange {
return costChanged;
}
- public String resolution() {
- return resolution;
+ public String transition() {
+ return transition;
}
public String assignee() {
diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/issue/IssueChangeTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/issue/IssueChangeTest.java
index 5760587b1dd..d39f7c89fb4 100644
--- a/sonar-plugin-api/src/test/java/org/sonar/api/issue/IssueChangeTest.java
+++ b/sonar-plugin-api/src/test/java/org/sonar/api/issue/IssueChangeTest.java
@@ -38,7 +38,7 @@ public class IssueChangeTest {
assertThat(change.line()).isNull();
assertThat(change.comment()).isNull();
assertThat(change.description()).isNull();
- assertThat(change.resolution()).isNull();
+ assertThat(change.transition()).isNull();
assertThat(change.manualSeverity()).isNull();
assertThat(change.attributes()).isEmpty();
}
@@ -118,8 +118,8 @@ public class IssueChangeTest {
@Test
public void should_change_resolution() {
IssueChange change = IssueChange.create();
- change.setResolution(Issue.RESOLUTION_FALSE_POSITIVE);
- assertThat(change.resolution()).isEqualTo(Issue.RESOLUTION_FALSE_POSITIVE);
+ change.setTransition("resolve");
+ assertThat(change.transition()).isEqualTo("resolve");
assertThat(change.hasChanges()).isTrue();
}
diff --git a/sonar-server/src/main/java/org/sonar/server/issue/DefaultJRubyIssues.java b/sonar-server/src/main/java/org/sonar/server/issue/DefaultJRubyIssues.java
index 09e723e5ef8..3e672dd5387 100644
--- a/sonar-server/src/main/java/org/sonar/server/issue/DefaultJRubyIssues.java
+++ b/sonar-server/src/main/java/org/sonar/server/issue/DefaultJRubyIssues.java
@@ -102,8 +102,8 @@ public class DefaultJRubyIssues implements JRubyIssues {
if (props.containsKey("newAssignee")) {
change.setAssignee((String) props.get("newAssignee"));
}
- if (props.containsKey("newResolution")) {
- change.setResolution((String) props.get("newResolution"));
+ if (props.containsKey("transition")) {
+ change.setTransition((String) props.get("transition"));
}
if (props.containsKey("newTitle")) {
change.setTitle((String) props.get("newTitle"));
diff --git a/sonar-server/src/main/java/org/sonar/server/issue/ServerIssueChanges.java b/sonar-server/src/main/java/org/sonar/server/issue/ServerIssueChanges.java
index f2a51ecafbf..a997eb93a3d 100644
--- a/sonar-server/src/main/java/org/sonar/server/issue/ServerIssueChanges.java
+++ b/sonar-server/src/main/java/org/sonar/server/issue/ServerIssueChanges.java
@@ -22,7 +22,7 @@ package org.sonar.server.issue;
import org.sonar.api.issue.Issue;
import org.sonar.api.issue.IssueChange;
import org.sonar.api.web.UserRole;
-import org.sonar.core.issue.ApplyIssueChange;
+import org.sonar.core.issue.UpdateIssueFields;
import org.sonar.core.issue.DefaultIssue;
import org.sonar.core.issue.IssueDao;
import org.sonar.core.issue.IssueDto;
@@ -59,7 +59,7 @@ public class ServerIssueChanges {
}
DefaultIssue issue = dto.toDefaultIssue();
if (change.hasChanges()) {
- ApplyIssueChange.apply(issue, change);
+ UpdateIssueFields.apply(issue, change);
issueDao.update(Arrays.asList(IssueDto.toDto(issue, dto.getResourceId(), dto.getRuleId())));
}
return issue;