]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-4315 Add Issue Action WS and add Action links to Issue detail
authorJulien Lancelot <julien.lancelot@gmail.com>
Mon, 3 Jun 2013 09:16:23 +0000 (11:16 +0200)
committerJulien Lancelot <julien.lancelot@gmail.com>
Mon, 3 Jun 2013 09:16:23 +0000 (11:16 +0200)
19 files changed:
sonar-plugin-api/src/main/java/org/sonar/api/issue/action/Action.java
sonar-plugin-api/src/main/java/org/sonar/api/issue/action/Actions.java [deleted file]
sonar-plugin-api/src/main/java/org/sonar/api/workflow/function/CommentFunction.java [deleted file]
sonar-plugin-api/src/main/java/org/sonar/api/workflow/function/package-info.java [deleted file]
sonar-plugin-api/src/test/java/org/sonar/api/workflow/function/CommentFunctionTest.java [deleted file]
sonar-server/src/main/java/org/sonar/server/issue/ActionService.java
sonar-server/src/main/java/org/sonar/server/issue/DefaultActions.java [deleted file]
sonar-server/src/main/java/org/sonar/server/issue/ExtendWorkflow.java [deleted file]
sonar-server/src/main/java/org/sonar/server/issue/InternalRubyIssueService.java
sonar-server/src/main/java/org/sonar/server/platform/Platform.java
sonar-server/src/main/webapp/WEB-INF/app/controllers/issue_controller.rb
sonar-server/src/main/webapp/WEB-INF/app/views/issue/_issue.html.erb
sonar-server/src/main/webapp/javascripts/issue.js
sonar-server/src/test/java/org/sonar/server/issue/ActionServiceTest.java
sonar-server/src/test/java/org/sonar/server/issue/DefaultActionsTest.java [deleted file]
sonar-ws-client/src/main/java/org/sonar/wsclient/issue/DefaultIssueClient.java
sonar-ws-client/src/main/java/org/sonar/wsclient/issue/IssueClient.java
sonar-ws-client/src/main/java/org/sonar/wsclient/issue/IssueJsonParser.java
sonar-ws-client/src/test/java/org/sonar/wsclient/issue/DefaultIssueClientTest.java

index 113b1ea7fe31fc1b798df890f13c5a9646f15a84..6da2f1ecbc4824838321bba0a2bb9e3604bfda01 100644 (file)
@@ -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 (file)
index d883ab8..0000000
+++ /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 (file)
index 697e9b5..0000000
+++ /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 (file)
index 3285b8f..0000000
+++ /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 (file)
index 1d0b9ba..0000000
+++ /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);
-  }
-}
index 41b9a79a12e9bb2fee122cdff066a28537122728..301d0d2bf15878f35b99495c33d3ccd59ea1208a 100644 (file)
@@ -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;
@@ -132,11 +141,6 @@ public class ActionService implements ServerComponent {
       return issue;
     }
 
-    @Override
-    public Map<String, String> parameters() {
-      return parameters;
-    }
-
     @Override
     public Function.Context setAttribute(String key, @Nullable String value) {
       updater.setAttribute(issue, key, value, changeContext);
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 (file)
index 7e80303..0000000
+++ /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 (file)
index 2cdc076..0000000
+++ /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()
-    );
-  }
-}
-
index 4944d000b081e096be943406b716af3dd63b36b8..adb53c4b0c6bd507ea2563a5e754a86813373521 100644 (file)
@@ -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
index 57972aa6eb040b760efabbbc085242bb04196652..f17401cce00d91131aff9e51952a7f37795467b9 100644 (file)
@@ -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);
 
index fe6e05e03970f8a5a3be6a5b23da8f7dc497fc26..9f1fb5c4f511705d2eef4b0de965e651f5878a9d 100644 (file)
@@ -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)
index de7d6ba281204703ad520dafc395d2f59d3e8804..7ea645906f36d42152ce696a1c8f783ab72730cc 100644 (file)
@@ -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' -%>
       <% 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">
                 <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 %>
index e034916d25118cbf2c2d11062c7766bb12342782..ca2e2818e703a4468f418e2b026da9adf7afb59c 100644 (file)
@@ -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)
index c5a796232fa39ebbc2070df21ace9cb077d37931..7ef368d9f95525ee2064505aee9cc0c7a24f0a4c 100644 (file)
 
 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 (file)
index 08622ff..0000000
+++ /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();
-  }
-
-}
index 727a2e4c4097c0ea6b4289159ccc0476327faded..4dd1972e2babc779cff70345cc59f69a54a77469 100644 (file)
@@ -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);
index 2130d3c92aeca01280f96441dba9e5f0cd3c8832..3527938710a83fb97b0677e94959455a0dccd4c5 100644 (file)
@@ -44,4 +44,8 @@ public interface IssueClient {
 
   Issue doTransition(String issueKey, String transition);
 
+  List<String> actions(String issueKey);
+
+  Issue doAction(String issueKey, String action);
+
 }
index 635a624758bb4e6ff068e18efe7fb5bfe9653e5e..906929ce308e94669b83ef75a8f8add352fd4a3d 100644 (file)
@@ -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;
+  }
 }
index 41118a61ff9388736a6dc9f0b06e37f03ad260d2..7ad6f6df3c836f20153f960409500bd864619853 100644 (file)
@@ -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();
+  }
 }