diff options
author | Simon Brandhof <simon.brandhof@gmail.com> | 2013-04-22 17:57:20 +0200 |
---|---|---|
committer | Simon Brandhof <simon.brandhof@gmail.com> | 2013-04-22 17:57:41 +0200 |
commit | e5e0c1076f177f79ab3024d8800e1c01ae7c40a2 (patch) | |
tree | cb434fc24d939d945e0f0e12fb6134441515be49 | |
parent | d78b5f1ef8a5516d18ae8ba0b158eb57a5c8777d (diff) | |
download | sonarqube-e5e0c1076f177f79ab3024d8800e1c01ae7c40a2.tar.gz sonarqube-e5e0c1076f177f79ab3024d8800e1c01ae7c40a2.zip |
SONAR-3755 first draft of issue state machine
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; |