aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJulien Lancelot <julien.lancelot@gmail.com>2013-06-03 11:16:23 +0200
committerJulien Lancelot <julien.lancelot@gmail.com>2013-06-03 11:16:23 +0200
commit507e8b9a0c214ca86722af265cb3fa81c3d0c09f (patch)
tree1e8e09d3197c8eda121a414ab57a4b9ba4118c40
parent6ce3ec7f1b3f0b3c1baf823037873bdd672af4e5 (diff)
downloadsonarqube-507e8b9a0c214ca86722af265cb3fa81c3d0c09f.tar.gz
sonarqube-507e8b9a0c214ca86722af265cb3fa81c3d0c09f.zip
SONAR-4315 Add Issue Action WS and add Action links to Issue detail
-rw-r--r--sonar-plugin-api/src/main/java/org/sonar/api/issue/action/Action.java4
-rw-r--r--sonar-plugin-api/src/main/java/org/sonar/api/issue/action/Actions.java36
-rw-r--r--sonar-plugin-api/src/main/java/org/sonar/api/workflow/function/CommentFunction.java35
-rw-r--r--sonar-plugin-api/src/main/java/org/sonar/api/workflow/function/package-info.java23
-rw-r--r--sonar-plugin-api/src/test/java/org/sonar/api/workflow/function/CommentFunctionTest.java43
-rw-r--r--sonar-server/src/main/java/org/sonar/server/issue/ActionService.java48
-rw-r--r--sonar-server/src/main/java/org/sonar/server/issue/DefaultActions.java58
-rw-r--r--sonar-server/src/main/java/org/sonar/server/issue/ExtendWorkflow.java46
-rw-r--r--sonar-server/src/main/java/org/sonar/server/issue/InternalRubyIssueService.java5
-rw-r--r--sonar-server/src/main/java/org/sonar/server/platform/Platform.java13
-rw-r--r--sonar-server/src/main/webapp/WEB-INF/app/controllers/issue_controller.rb3
-rw-r--r--sonar-server/src/main/webapp/WEB-INF/app/views/issue/_issue.html.erb22
-rw-r--r--sonar-server/src/main/webapp/javascripts/issue.js6
-rw-r--r--sonar-server/src/test/java/org/sonar/server/issue/ActionServiceTest.java181
-rw-r--r--sonar-server/src/test/java/org/sonar/server/issue/DefaultActionsTest.java61
-rw-r--r--sonar-ws-client/src/main/java/org/sonar/wsclient/issue/DefaultIssueClient.java23
-rw-r--r--sonar-ws-client/src/main/java/org/sonar/wsclient/issue/IssueClient.java4
-rw-r--r--sonar-ws-client/src/main/java/org/sonar/wsclient/issue/IssueJsonParser.java10
-rw-r--r--sonar-ws-client/src/test/java/org/sonar/wsclient/issue/DefaultIssueClientTest.java30
19 files changed, 303 insertions, 348 deletions
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/issue/action/Action.java b/sonar-plugin-api/src/main/java/org/sonar/api/issue/action/Action.java
index 113b1ea7fe3..6da2f1ecbc4 100644
--- a/sonar-plugin-api/src/main/java/org/sonar/api/issue/action/Action.java
+++ b/sonar-plugin-api/src/main/java/org/sonar/api/issue/action/Action.java
@@ -22,14 +22,14 @@ package org.sonar.api.issue.action;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
-import org.sonar.api.ServerComponent;
+import org.sonar.api.ServerExtension;
import org.sonar.api.issue.Issue;
import org.sonar.api.issue.condition.Condition;
import java.util.Arrays;
import java.util.List;
-public class Action implements ServerComponent {
+public class Action implements ServerExtension {
private final String key;
private final List<Condition> conditions;
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/issue/action/Actions.java b/sonar-plugin-api/src/main/java/org/sonar/api/issue/action/Actions.java
deleted file mode 100644
index d883ab8a5c7..00000000000
--- a/sonar-plugin-api/src/main/java/org/sonar/api/issue/action/Actions.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * SonarQube, open source software quality management tool.
- * Copyright (C) 2008-2013 SonarSource
- * mailto:contact AT sonarsource DOT com
- *
- * SonarQube is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * SonarQube is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.api.issue.action;
-
-import org.sonar.api.ServerComponent;
-
-import java.util.Set;
-
-/**
- * @since 3.6
- */
-// TODO remove it
-public interface Actions extends ServerComponent {
-
- Actions addAction(Action action);
-
- Set<Action> getActions();
-
-}
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/workflow/function/CommentFunction.java b/sonar-plugin-api/src/main/java/org/sonar/api/workflow/function/CommentFunction.java
deleted file mode 100644
index 697e9b5f2a0..00000000000
--- a/sonar-plugin-api/src/main/java/org/sonar/api/workflow/function/CommentFunction.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * SonarQube, open source software quality management tool.
- * Copyright (C) 2008-2013 SonarSource
- * mailto:contact AT sonarsource DOT com
- *
- * SonarQube is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * SonarQube is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.api.workflow.function;
-
-import com.google.common.annotations.Beta;
-import org.sonar.api.issue.action.Function;
-
-/**
- * @since 3.1
- */
-@Beta
-public final class CommentFunction implements Function {
-
- @Override
- public void execute(Context context) {
- context.addComment("New comment!");
- }
-}
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/workflow/function/package-info.java b/sonar-plugin-api/src/main/java/org/sonar/api/workflow/function/package-info.java
deleted file mode 100644
index 3285b8fdd91..00000000000
--- a/sonar-plugin-api/src/main/java/org/sonar/api/workflow/function/package-info.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * SonarQube, open source software quality management tool.
- * Copyright (C) 2008-2013 SonarSource
- * mailto:contact AT sonarsource DOT com
- *
- * SonarQube is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * SonarQube is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-@ParametersAreNonnullByDefault
-package org.sonar.api.workflow.function;
-
-import javax.annotation.ParametersAreNonnullByDefault; \ No newline at end of file
diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/workflow/function/CommentFunctionTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/workflow/function/CommentFunctionTest.java
deleted file mode 100644
index 1d0b9ba3da6..00000000000
--- a/sonar-plugin-api/src/test/java/org/sonar/api/workflow/function/CommentFunctionTest.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * SonarQube, open source software quality management tool.
- * Copyright (C) 2008-2013 SonarSource
- * mailto:contact AT sonarsource DOT com
- *
- * SonarQube is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * SonarQube is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.api.workflow.function;
-
-import org.junit.Test;
-
-public class CommentFunctionTest {
-
- @Test
- // TODO
- public void setTextAndUserId() {
-// CommentFunction function = new CommentFunction();
-// Map<String, String> parameters = Maps.newHashMap();
-// parameters.put("text", "foo");
-// DefaultReview review = new DefaultReview();
-// DefaultWorkflowContext context = new DefaultWorkflowContext();
-// context.setUserId(1234L);
-//
-// function.doExecute(review, new DefaultReview(), context, parameters);
-//
-// List<Comment> newComments = review.getNewComments();
-// assertThat(newComments).hasSize(1);
-// assertThat(newComments.get(0).getMarkdownText()).isEqualTo("foo");
-// assertThat(newComments.get(0).getUserId()).isEqualTo(1234L);
- }
-}
diff --git a/sonar-server/src/main/java/org/sonar/server/issue/ActionService.java b/sonar-server/src/main/java/org/sonar/server/issue/ActionService.java
index 41b9a79a12e..301d0d2bf15 100644
--- a/sonar-server/src/main/java/org/sonar/server/issue/ActionService.java
+++ b/sonar-server/src/main/java/org/sonar/server/issue/ActionService.java
@@ -40,9 +40,9 @@ import org.sonar.server.user.UserSession;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
+import java.util.Collections;
import java.util.Date;
import java.util.List;
-import java.util.Map;
import static com.google.common.collect.Lists.newArrayList;
@@ -52,7 +52,6 @@ public class ActionService implements ServerComponent {
private final IssueStorage issueStorage;
private final IssueUpdater updater;
private final List<Action> actions;
-// private final DefaultActions actions;
public ActionService(DefaultIssueFinder finder, IssueStorage issueStorage, IssueUpdater updater, List<Action> actions) {
this.finder = finder;
@@ -61,10 +60,11 @@ public class ActionService implements ServerComponent {
this.actions = actions;
}
- public List<Action> listAvailableActions(String issueKey) {
- IssueQueryResult queryResult = loadIssue(issueKey);
- final DefaultIssue issue = (DefaultIssue) queryResult.first();
+ public ActionService(DefaultIssueFinder finder, IssueStorage issueStorage, IssueUpdater updater) {
+ this(finder, issueStorage, updater, Collections.<Action>emptyList());
+ }
+ public List<Action> listAvailableActions(final Issue issue) {
return newArrayList(Iterables.filter(actions, new Predicate<Action>() {
@Override
public boolean apply(Action action) {
@@ -73,32 +73,31 @@ public class ActionService implements ServerComponent {
}));
}
- @CheckForNull
- public Action getAction(final String actionKey) {
- return Iterables.find(actions, new Predicate<Action>() {
- @Override
- public boolean apply(Action action) {
- return action.key().equals(actionKey);
- }
- }, null);
+ public List<Action> listAvailableActions(String issueKey) {
+ IssueQueryResult queryResult = loadIssue(issueKey);
+ final DefaultIssue issue = (DefaultIssue) queryResult.first();
+ if (issue == null) {
+ throw new IllegalArgumentException("Issue is not found : " + issueKey);
+ }
+
+ return listAvailableActions(issue);
}
- public Issue execute(String issueKey, String actionKey, UserSession userSession, Map<String, String> parameters) {
public Issue execute(String issueKey, String actionKey, UserSession userSession) {
Preconditions.checkArgument(!Strings.isNullOrEmpty(actionKey), "Missing action");
IssueQueryResult queryResult = loadIssue(issueKey);
DefaultIssue issue = (DefaultIssue) queryResult.first();
if (issue == null) {
- throw new IllegalStateException("Issue is not found : " + issueKey);
+ throw new IllegalArgumentException("Issue is not found : " + issueKey);
}
Action action = getAction(actionKey);
if (action == null) {
- throw new IllegalStateException("Action is not found : " + actionKey);
+ throw new IllegalArgumentException("Action is not found : " + actionKey);
}
if (!action.supports(issue)) {
- throw new IllegalStateException("A condition is not respected.");
+ throw new IllegalStateException("A condition is not respected");
}
IssueChangeContext changeContext = IssueChangeContext.createUser(new Date(), userSession.login());
@@ -115,6 +114,16 @@ public class ActionService implements ServerComponent {
return finder.find(query);
}
+ @CheckForNull
+ private Action getAction(final String actionKey) {
+ return Iterables.find(actions, new Predicate<Action>() {
+ @Override
+ public boolean apply(Action action) {
+ return action.key().equals(actionKey);
+ }
+ }, null);
+ }
+
static class FunctionContext implements Function.Context {
private final DefaultIssue issue;
@@ -133,11 +142,6 @@ public class ActionService implements ServerComponent {
}
@Override
- public Map<String, String> parameters() {
- return parameters;
- }
-
- @Override
public Function.Context setAttribute(String key, @Nullable String value) {
updater.setAttribute(issue, key, value, changeContext);
return this;
diff --git a/sonar-server/src/main/java/org/sonar/server/issue/DefaultActions.java b/sonar-server/src/main/java/org/sonar/server/issue/DefaultActions.java
deleted file mode 100644
index 7e80303bb7a..00000000000
--- a/sonar-server/src/main/java/org/sonar/server/issue/DefaultActions.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * SonarQube, open source software quality management tool.
- * Copyright (C) 2008-2013 SonarSource
- * mailto:contact AT sonarsource DOT com
- *
- * SonarQube is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * SonarQube is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.server.issue;
-
-import com.google.common.base.Predicate;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Sets;
-import org.sonar.api.issue.action.Action;
-import org.sonar.api.issue.action.Actions;
-
-import javax.annotation.CheckForNull;
-
-import java.util.Set;
-
-/**
- * @since 3.6
- */
-public final class DefaultActions implements Actions {
-
- private Set<Action> actions = Sets.newLinkedHashSet();
-
- public DefaultActions addAction(Action action) {
- actions.add(action);
- return this;
- }
-
- public Set<Action> getActions() {
- return actions;
- }
-
- @CheckForNull
- public Action getAction(final String actionKey) {
- return Iterables.find(actions, new Predicate<Action>() {
- @Override
- public boolean apply(Action action) {
- return action.key().equals(actionKey);
- }
- }, null);
- }
-
-}
diff --git a/sonar-server/src/main/java/org/sonar/server/issue/ExtendWorkflow.java b/sonar-server/src/main/java/org/sonar/server/issue/ExtendWorkflow.java
deleted file mode 100644
index 2cdc076b4d6..00000000000
--- a/sonar-server/src/main/java/org/sonar/server/issue/ExtendWorkflow.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * SonarQube, open source software quality management tool.
- * Copyright (C) 2008-2013 SonarSource
- * mailto:contact AT sonarsource DOT com
- *
- * SonarQube is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * SonarQube is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-
-package org.sonar.server.issue;
-
-import org.sonar.api.ServerExtension;
-import org.sonar.api.issue.Issue;
-import org.sonar.api.issue.action.Action;
-import org.sonar.api.issue.action.Actions;
-import org.sonar.api.issue.condition.HasResolution;
-import org.sonar.api.workflow.function.CommentFunction;
-
-public final class ExtendWorkflow implements ServerExtension {
-
- private final Actions actions;
-
- public ExtendWorkflow(Actions actions) {
- this.actions = actions;
- }
-
- public void start() {
- actions.addAction(Action.builder("fake")
- .conditions(new HasResolution(Issue.RESOLUTION_FIXED))
- .functions(new CommentFunction())
- .build()
- );
- }
-}
-
diff --git a/sonar-server/src/main/java/org/sonar/server/issue/InternalRubyIssueService.java b/sonar-server/src/main/java/org/sonar/server/issue/InternalRubyIssueService.java
index 4944d000b08..adb53c4b0c6 100644
--- a/sonar-server/src/main/java/org/sonar/server/issue/InternalRubyIssueService.java
+++ b/sonar-server/src/main/java/org/sonar/server/issue/InternalRubyIssueService.java
@@ -45,6 +45,7 @@ import org.sonar.server.user.UserSession;
import org.sonar.server.util.RubyUtils;
import javax.annotation.Nullable;
+
import java.util.Collection;
import java.util.Date;
import java.util.List;
@@ -318,4 +319,8 @@ public class InternalRubyIssueService implements ServerComponent {
public List<Action> listActions(String issueKey){
return actionService.listAvailableActions(issueKey);
}
+
+ public List<Action> listActions(Issue issue) {
+ return actionService.listAvailableActions(issue);
+ }
} \ No newline at end of file
diff --git a/sonar-server/src/main/java/org/sonar/server/platform/Platform.java b/sonar-server/src/main/java/org/sonar/server/platform/Platform.java
index 57972aa6eb0..f17401cce00 100644
--- a/sonar-server/src/main/java/org/sonar/server/platform/Platform.java
+++ b/sonar-server/src/main/java/org/sonar/server/platform/Platform.java
@@ -22,9 +22,6 @@ package org.sonar.server.platform;
import org.apache.commons.configuration.BaseConfiguration;
import org.slf4j.LoggerFactory;
import org.sonar.api.config.EmailSettings;
-import org.sonar.api.issue.Issue;
-import org.sonar.api.issue.action.Action;
-import org.sonar.api.issue.condition.HasResolution;
import org.sonar.api.platform.ComponentContainer;
import org.sonar.api.platform.Server;
import org.sonar.api.profiles.AnnotationProfileParser;
@@ -37,7 +34,6 @@ import org.sonar.api.rules.XMLRuleParser;
import org.sonar.api.utils.HttpDownloader;
import org.sonar.api.utils.TimeProfiler;
import org.sonar.api.utils.UriReader;
-import org.sonar.api.workflow.function.CommentFunction;
import org.sonar.core.component.SnapshotPerspectives;
import org.sonar.core.config.Logback;
import org.sonar.core.i18n.GwtI18n;
@@ -211,7 +207,7 @@ public final class Platform {
*/
private void startServiceComponents() {
servicesContainer = coreContainer.createChild();
- servicesContainer.addSingleton(DefaultActions.class);
+
servicesContainer.addSingleton(HttpDownloader.class);
servicesContainer.addSingleton(UriReader.class);
servicesContainer.addSingleton(UpdateCenterClient.class);
@@ -275,13 +271,6 @@ public final class Platform {
servicesContainer.addSingleton(IssueNotifications.class);
servicesContainer.addSingleton(ActionService.class);
- // TODO only for test
- servicesContainer.addSingleton(Action.builder("fake")
- .conditions(new HasResolution(Issue.RESOLUTION_FIXED))
- .functions(new CommentFunction())
- .build()
- );
-
// rules
servicesContainer.addSingleton(RubyRuleService.class);
diff --git a/sonar-server/src/main/webapp/WEB-INF/app/controllers/issue_controller.rb b/sonar-server/src/main/webapp/WEB-INF/app/controllers/issue_controller.rb
index fe6e05e0397..9f1fb5c4f51 100644
--- a/sonar-server/src/main/webapp/WEB-INF/app/controllers/issue_controller.rb
+++ b/sonar-server/src/main/webapp/WEB-INF/app/controllers/issue_controller.rb
@@ -81,6 +81,9 @@ class IssueController < ApplicationController
Internal.issues.plan(issue_key, params[:plan])
elsif action_type=='unplan'
Internal.issues.plan(issue_key, nil)
+ else
+ # Execute action defined by plugin
+ Internal.issues.executeAction(issue_key, action_type)
end
@issue_results = Api.issues.find(issue_key)
diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/issue/_issue.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/issue/_issue.html.erb
index de7d6ba2812..7ea645906f3 100644
--- a/sonar-server/src/main/webapp/WEB-INF/app/views/issue/_issue.html.erb
+++ b/sonar-server/src/main/webapp/WEB-INF/app/views/issue/_issue.html.erb
@@ -92,7 +92,9 @@
<%
transitions = Internal.issues.listTransitions(issue)
- if transitions.size > 0 && transitions.first
+
+ # Display only the first transition
+ if !transitions.empty? && transitions.first
transition = transitions.first
%>
<%= image_tag 'sep12.png' -%>
@@ -101,7 +103,10 @@
<% end %>
<%
- if transitions.size > 1 || !issue.resolution
+ plugin_actions = Internal.issues.listActions(issue)
+ puts "### "
+ shouldDisplayDropDown = transitions.size > 1 || !issue.resolution || !plugin_actions.empty?
+ if shouldDisplayDropDown
transitions.remove(0)
%>
<div class="dropdown">
@@ -112,11 +117,22 @@
<a href="#" onclick="return issueForm('severity', this)" class="link-action spacer-right"><%= message("issue.set_severity") -%></a>
</li>
<% end %>
- <% transitions.each do |transition| %>
+
+ <%
+ # Display remaining transitions
+ transitions.each do |transition| %>
<li>
<a href="#" onclick="return doIssueTransition(this, '<%= transition.key -%>')" class="link-action spacer-right"><%= message("issue.transition.#{transition.key}") -%></a>
</li>
<% end %>
+
+ <%
+ # Display actions defined by plugins
+ plugin_actions.each do |action| %>
+ <li>
+ <a href="#" onclick="return doPluginIssueAction(this, '<%= action.key -%>')" class="link-action spacer-right"><%= message("issue.action.#{action.key}.formlink") -%></a>
+ </li>
+ <% end %>
</ul>
</div>
<% end %>
diff --git a/sonar-server/src/main/webapp/javascripts/issue.js b/sonar-server/src/main/webapp/javascripts/issue.js
index e034916d251..ca2e2818e70 100644
--- a/sonar-server/src/main/webapp/javascripts/issue.js
+++ b/sonar-server/src/main/webapp/javascripts/issue.js
@@ -93,6 +93,12 @@ function doIssueAction(elt, action, parameters) {
return false;
}
+// Used for actions defined by plugins
+function doPluginIssueAction(elt, action) {
+ var parameters = {};
+ return doIssueAction(elt, action, parameters)
+}
+
function assignIssueToMe(elt) {
var parameters = {'me': true};
return doIssueAction(elt, 'assign', parameters)
diff --git a/sonar-server/src/test/java/org/sonar/server/issue/ActionServiceTest.java b/sonar-server/src/test/java/org/sonar/server/issue/ActionServiceTest.java
index c5a796232fa..7ef368d9f95 100644
--- a/sonar-server/src/test/java/org/sonar/server/issue/ActionServiceTest.java
+++ b/sonar-server/src/test/java/org/sonar/server/issue/ActionServiceTest.java
@@ -20,23 +20,192 @@
package org.sonar.server.issue;
-import org.junit.Before;
+import org.junit.Test;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.issue.IssueQuery;
+import org.sonar.api.issue.IssueQueryResult;
+import org.sonar.api.issue.action.Action;
+import org.sonar.api.issue.action.Function;
+import org.sonar.api.issue.condition.Condition;
+import org.sonar.api.issue.internal.DefaultIssue;
+import org.sonar.api.issue.internal.IssueChangeContext;
+import org.sonar.core.issue.DefaultIssueQueryResult;
import org.sonar.core.issue.IssueUpdater;
import org.sonar.core.issue.db.IssueStorage;
+import org.sonar.server.user.UserSession;
-import static org.mockito.Mockito.mock;
+import java.util.Collections;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static org.fest.assertions.Assertions.assertThat;
+import static org.fest.assertions.Fail.fail;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.*;
public class ActionServiceTest {
private ActionService actionService;
-
private DefaultIssueFinder finder = mock(DefaultIssueFinder.class);
private IssueStorage issueStorage = mock(IssueStorage.class);
private IssueUpdater updater = mock(IssueUpdater.class);
- private DefaultActions actions = mock(DefaultActions.class);
- @Before
- public void before(){
+ @Test
+ public void should_execute_functions() {
+ Function function1 = mock(Function.class);
+ Function function2 = mock(Function.class);
+
+ Issue issue = new DefaultIssue().setKey("ABCD");
+ IssueQueryResult issueQueryResult = new DefaultIssueQueryResult(newArrayList(issue));
+ when(finder.find(any(IssueQuery.class))).thenReturn(issueQueryResult);
+
+ Action action = Action.builder("link-to-jira").conditions(new AlwaysMatch()).functions(function1, function2).build();
+
+ actionService = new ActionService(finder, issueStorage, updater, newArrayList(action));
+ assertThat(actionService.execute("ABCD", "link-to-jira", mock(UserSession.class))).isNotNull();
+
+ verify(function1).execute(any(Function.Context.class));
+ verify(function2).execute(any(Function.Context.class));
+ verifyNoMoreInteractions(function1, function2);
+ }
+
+ @Test
+ public void should_modify_issue_when_executing_a_function() {
+ Function function = new TweetFunction();
+
+ UserSession userSession = mock(UserSession.class);
+ when(userSession.login()).thenReturn("arthur");
+
+ DefaultIssue issue = new DefaultIssue().setKey("ABCD");
+ IssueQueryResult issueQueryResult = new DefaultIssueQueryResult(newArrayList((Issue) issue));
+ when(finder.find(any(IssueQuery.class))).thenReturn(issueQueryResult);
+
+ Action action = Action.builder("link-to-jira").conditions(new AlwaysMatch()).functions(function).build();
+
+ actionService = new ActionService(finder, issueStorage, updater, newArrayList(action));
+ assertThat(actionService.execute("ABCD", "link-to-jira", userSession)).isNotNull();
+
+ verify(updater).addComment(eq(issue), eq("New tweet on issue ABCD"), any(IssueChangeContext.class));
+ verify(updater).setAttribute(eq(issue), eq("tweet"), eq("tweet sent"), any(IssueChangeContext.class));
+ }
+
+ @Test
+ public void should_not_execute_function_if_issue_not_found() {
+ Function function = mock(Function.class);
+
+ IssueQueryResult issueQueryResult = new DefaultIssueQueryResult(Collections.<Issue>emptyList());
+ when(finder.find(any(IssueQuery.class))).thenReturn(issueQueryResult);
+
+ Action action = Action.builder("link-to-jira").conditions(new AlwaysMatch()).functions(function).build();
+
+ actionService = new ActionService(finder, issueStorage, updater, newArrayList(action));
+
+ try {
+ actionService.execute("ABCD", "link-to-jira", mock(UserSession.class));
+ } catch (Exception e){
+ assertThat(e).isInstanceOf(IllegalArgumentException.class).hasMessage("Issue is not found : ABCD");
+ }
+ verifyZeroInteractions(function);
+ }
+
+ @Test
+ public void should_not_execute_function_if_action_not_found() {
+ Function function = mock(Function.class);
+
+ Issue issue = new DefaultIssue().setKey("ABCD");
+ IssueQueryResult issueQueryResult = new DefaultIssueQueryResult(newArrayList(issue));
+ when(finder.find(any(IssueQuery.class))).thenReturn(issueQueryResult);
+
+ Action action = Action.builder("link-to-jira").conditions(new AlwaysMatch()).functions(function).build();
+
+ actionService = new ActionService(finder, issueStorage, updater, newArrayList(action));
+ try {
+ actionService.execute("ABCD", "tweet", mock(UserSession.class));
+ } catch (Exception e){
+ assertThat(e).isInstanceOf(IllegalArgumentException.class).hasMessage("Action is not found : tweet");
+ }
+ verifyZeroInteractions(function);
+ }
+
+ @Test
+ public void should_not_execute_function_if_action_is_not_supported() {
+ Function function = mock(Function.class);
+
+ Issue issue = new DefaultIssue().setKey("ABCD");
+ IssueQueryResult issueQueryResult = new DefaultIssueQueryResult(newArrayList(issue));
+ when(finder.find(any(IssueQuery.class))).thenReturn(issueQueryResult);
+
+ Action action = Action.builder("link-to-jira").conditions(new NeverMatch()).functions(function).build();
+
+ actionService = new ActionService(finder, issueStorage, updater, newArrayList(action));
+ try {
+ actionService.execute("ABCD", "link-to-jira", mock(UserSession.class));
+ } catch (Exception e){
+ assertThat(e).isInstanceOf(IllegalStateException.class).hasMessage("A condition is not respected");
+ }
+ verifyZeroInteractions(function);
+ }
+
+ @Test
+ public void should_list_available_supported_actions() {
+ Issue issue = new DefaultIssue().setKey("ABCD");
+ IssueQueryResult issueQueryResult = new DefaultIssueQueryResult(newArrayList(issue));
+ when(finder.find(any(IssueQuery.class))).thenReturn(issueQueryResult);
+
+ Action action1 = Action.builder("link-to-jira").conditions(new AlwaysMatch()).build();
+ Action action2 = Action.builder("tweet").conditions(new NeverMatch()).build();
+
+ actionService = new ActionService(finder, issueStorage, updater, newArrayList(action1, action2));
+ assertThat(actionService.listAvailableActions("ABCD")).containsOnly(action1);
+ }
+
+ @Test
+ public void should_list_available_actions_throw_exception_if_issue_not_found() {
+ IssueQueryResult issueQueryResult = new DefaultIssueQueryResult(Collections.<Issue>emptyList());
+ when(finder.find(any(IssueQuery.class))).thenReturn(issueQueryResult);
+
+ Action action1 = Action.builder("link-to-jira").conditions(new AlwaysMatch()).build();
+ Action action2 = Action.builder("tweet").conditions(new NeverMatch()).build();
+
+ actionService = new ActionService(finder, issueStorage, updater, newArrayList(action1, action2));
+
+ try {
+ actionService.listAvailableActions("ABCD");
+ fail();
+ } catch (Exception e){
+ assertThat(e).isInstanceOf(IllegalArgumentException.class).hasMessage("Issue is not found : ABCD");
+ }
+ }
+
+ @Test
+ public void should_return_no_action() {
+ Issue issue = new DefaultIssue().setKey("ABCD");
+ IssueQueryResult issueQueryResult = new DefaultIssueQueryResult(newArrayList(issue));
+ when(finder.find(any(IssueQuery.class))).thenReturn(issueQueryResult);
+
+ actionService = new ActionService(finder, issueStorage, updater);
+ assertThat(actionService.listAvailableActions("ABCD")).isEmpty();
+ }
+
+ public class AlwaysMatch implements Condition {
+ @Override
+ public boolean matches(Issue issue) {
+ return true;
+ }
+ }
+
+ public class NeverMatch implements Condition {
+ @Override
+ public boolean matches(Issue issue) {
+ return false;
+ }
+ }
+ public class TweetFunction implements Function {
+ @Override
+ public void execute(Context context) {
+ context.addComment("New tweet on issue "+ context.issue().key());
+ context.setAttribute("tweet", "tweet sent");
+ }
}
}
diff --git a/sonar-server/src/test/java/org/sonar/server/issue/DefaultActionsTest.java b/sonar-server/src/test/java/org/sonar/server/issue/DefaultActionsTest.java
deleted file mode 100644
index 08622ff2677..00000000000
--- a/sonar-server/src/test/java/org/sonar/server/issue/DefaultActionsTest.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * SonarQube, open source software quality management tool.
- * Copyright (C) 2008-2013 SonarSource
- * mailto:contact AT sonarsource DOT com
- *
- * SonarQube is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * SonarQube is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.server.issue;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.sonar.api.issue.action.Action;
-
-import static org.fest.assertions.Assertions.assertThat;
-
-public class DefaultActionsTest {
-
- @Rule
- public ExpectedException thrown = ExpectedException.none();
-
- private DefaultActions actions;
-
- @Before
- public void before(){
- actions = new DefaultActions();
- }
-
- @Test
- public void should_add_action() {
- Action action = Action.builder("link-to-jira").build();
-
- actions.addAction(action);
-
- assertThat(actions.getActions()).hasSize(1);
- }
-
- @Test
- public void should_get_action() {
- Action action = Action.builder("link-to-jira").build();
-
- actions.addAction(action);
-
- assertThat(actions.getAction("link-to-jira")).isEqualTo(action);
- assertThat(actions.getAction("not-found")).isNull();
- }
-
-}
diff --git a/sonar-ws-client/src/main/java/org/sonar/wsclient/issue/DefaultIssueClient.java b/sonar-ws-client/src/main/java/org/sonar/wsclient/issue/DefaultIssueClient.java
index 727a2e4c409..4dd1972e2ba 100644
--- a/sonar-ws-client/src/main/java/org/sonar/wsclient/issue/DefaultIssueClient.java
+++ b/sonar-ws-client/src/main/java/org/sonar/wsclient/issue/DefaultIssueClient.java
@@ -109,7 +109,7 @@ public class DefaultIssueClient implements IssueClient {
Map<String, Object> queryParams = EncodingUtils.toMap("issue", issueKey);
HttpRequest request = requestFactory.get("/api/issues/transitions", queryParams);
if (!request.ok()) {
- throw new IllegalStateException("Fail to return transition for issue. Bad HTTP response status: " + request.code());
+ throw new IllegalStateException("Fail to return transitions for issue. Bad HTTP response status: " + request.code());
}
String json = request.body("UTF-8");
return parser.parseTransitions(json);
@@ -125,6 +125,27 @@ public class DefaultIssueClient implements IssueClient {
return createIssueResult(request);
}
+ @Override
+ public List<String> actions(String issueKey) {
+ Map<String, Object> queryParams = EncodingUtils.toMap("issue", issueKey);
+ HttpRequest request = requestFactory.get("/api/issues/actions", queryParams);
+ if (!request.ok()) {
+ throw new IllegalStateException("Fail to return actions for issue. Bad HTTP response status: " + request.code());
+ }
+ String json = request.body("UTF-8");
+ return parser.parseActions(json);
+ }
+
+ @Override
+ public Issue doAction(String issueKey, String action) {
+ Map<String, Object> params = EncodingUtils.toMap("issue", issueKey, "actionKey", action);
+ HttpRequest request = requestFactory.post("/api/issues/do_action", params);
+ if (!request.ok()) {
+ throw new IllegalStateException("Fail to execute action on issue " + issueKey + ".Bad HTTP response status: " + request.code());
+ }
+ return createIssueResult(request);
+ }
+
private Issue createIssueResult(HttpRequest request){
String json = request.body("UTF-8");
Map jsonRoot = (Map) JSONValue.parse(json);
diff --git a/sonar-ws-client/src/main/java/org/sonar/wsclient/issue/IssueClient.java b/sonar-ws-client/src/main/java/org/sonar/wsclient/issue/IssueClient.java
index 2130d3c92ae..3527938710a 100644
--- a/sonar-ws-client/src/main/java/org/sonar/wsclient/issue/IssueClient.java
+++ b/sonar-ws-client/src/main/java/org/sonar/wsclient/issue/IssueClient.java
@@ -44,4 +44,8 @@ public interface IssueClient {
Issue doTransition(String issueKey, String transition);
+ List<String> actions(String issueKey);
+
+ Issue doAction(String issueKey, String action);
+
}
diff --git a/sonar-ws-client/src/main/java/org/sonar/wsclient/issue/IssueJsonParser.java b/sonar-ws-client/src/main/java/org/sonar/wsclient/issue/IssueJsonParser.java
index 635a624758b..906929ce308 100644
--- a/sonar-ws-client/src/main/java/org/sonar/wsclient/issue/IssueJsonParser.java
+++ b/sonar-ws-client/src/main/java/org/sonar/wsclient/issue/IssueJsonParser.java
@@ -112,4 +112,14 @@ class IssueJsonParser {
}
return transitions;
}
+
+ List<String> parseActions(String json) {
+ List<String> actions = new ArrayList<String>();
+ Map jRoot = (Map) JSONValue.parse(json);
+ List<String> jActions = (List) jRoot.get("actions");
+ for (String jAction : jActions) {
+ actions.add(jAction);
+ }
+ return actions;
+ }
}
diff --git a/sonar-ws-client/src/test/java/org/sonar/wsclient/issue/DefaultIssueClientTest.java b/sonar-ws-client/src/test/java/org/sonar/wsclient/issue/DefaultIssueClientTest.java
index 41118a61ff9..7ad6f6df3c8 100644
--- a/sonar-ws-client/src/test/java/org/sonar/wsclient/issue/DefaultIssueClientTest.java
+++ b/sonar-ws-client/src/test/java/org/sonar/wsclient/issue/DefaultIssueClientTest.java
@@ -179,4 +179,34 @@ public class DefaultIssueClientTest {
assertThat(comment.login()).isEqualTo("admin");
assertThat(comment.createdAt().getDate()).isEqualTo(18);
}
+
+ @Test
+ public void should_get_actions() {
+ HttpRequestFactory requestFactory = new HttpRequestFactory(httpServer.url());
+ httpServer.doReturnBody("{\n" +
+ " \"actions\": [\n" +
+ " \"link-to-jira\",\n" +
+ " \"tweet\"\n" +
+ " ]\n" +
+ "}");
+
+ IssueClient client = new DefaultIssueClient(requestFactory);
+ List<String> actions = client.actions("ABCDE");
+
+ assertThat(httpServer.requestedPath()).isEqualTo("/api/issues/actions?issue=ABCDE");
+ assertThat(actions).hasSize(2);
+ assertThat(actions).containsOnly("link-to-jira", "tweet");
+ }
+
+ @Test
+ public void should_apply_action() {
+ HttpRequestFactory requestFactory = new HttpRequestFactory(httpServer.url());
+ httpServer.doReturnBody("{\"issue\": {\"key\": \"ABCDE\"}}");
+
+ IssueClient client = new DefaultIssueClient(requestFactory);
+ Issue result = client.doAction("ABCDE", "tweet");
+
+ assertThat(httpServer.requestedPath()).isEqualTo("/api/issues/do_action?issue=ABCDE&actionKey=tweet");
+ assertThat(result).isNotNull();
+ }
}