summaryrefslogtreecommitdiffstats
path: root/sonar-core/src
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 /sonar-core/src
parentd78b5f1ef8a5516d18ae8ba0b158eb57a5c8777d (diff)
downloadsonarqube-e5e0c1076f177f79ab3024d8800e1c01ae7c40a2.tar.gz
sonarqube-e5e0c1076f177f79ab3024d8800e1c01ae7c40a2.zip
SONAR-3755 first draft of issue state machine
Diffstat (limited to 'sonar-core/src')
-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
14 files changed, 655 insertions, 12 deletions
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java b/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java
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);
+ }
+}