]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-5341 In /issues/search WS, add parameter 'extra_fields'
authorJulien Lancelot <julien.lancelot@sonarsource.com>
Wed, 28 May 2014 10:43:13 +0000 (12:43 +0200)
committerJulien Lancelot <julien.lancelot@sonarsource.com>
Wed, 28 May 2014 10:43:13 +0000 (12:43 +0200)
25 files changed:
sonar-plugin-api/src/main/java/org/sonar/api/server/ws/Request.java
sonar-plugin-api/src/test/java/org/sonar/api/server/ws/RequestTest.java
sonar-server/src/main/java/org/sonar/server/issue/ws/IssueActionsWriter.java [new file with mode: 0644]
sonar-server/src/main/java/org/sonar/server/issue/ws/IssueSearchAction.java [new file with mode: 0644]
sonar-server/src/main/java/org/sonar/server/issue/ws/IssueShowAction.java
sonar-server/src/main/java/org/sonar/server/issue/ws/IssuesWs.java
sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java
sonar-server/src/main/resources/org/sonar/server/issue/ws/example-search.json
sonar-server/src/test/java/org/sonar/server/issue/ws/IssueActionsWriterTest.java [new file with mode: 0644]
sonar-server/src/test/java/org/sonar/server/issue/ws/IssueSearchActionTest.java [new file with mode: 0644]
sonar-server/src/test/java/org/sonar/server/issue/ws/IssueShowActionTest.java
sonar-server/src/test/java/org/sonar/server/issue/ws/IssuesWsTest.java
sonar-server/src/test/resources/org/sonar/server/issue/ws/IssueSearchActionTest/issues.json [new file with mode: 0644]
sonar-server/src/test/resources/org/sonar/server/issue/ws/IssueSearchActionTest/issues_with_action_plan.json [new file with mode: 0644]
sonar-server/src/test/resources/org/sonar/server/issue/ws/IssueSearchActionTest/issues_with_attributes.json [new file with mode: 0644]
sonar-server/src/test/resources/org/sonar/server/issue/ws/IssueSearchActionTest/issues_with_components.json [new file with mode: 0644]
sonar-server/src/test/resources/org/sonar/server/issue/ws/IssueSearchActionTest/issues_with_dates.json [new file with mode: 0644]
sonar-server/src/test/resources/org/sonar/server/issue/ws/IssueSearchActionTest/issues_with_debt.json [new file with mode: 0644]
sonar-server/src/test/resources/org/sonar/server/issue/ws/IssueSearchActionTest/issues_with_extra_fields.json [new file with mode: 0644]
sonar-server/src/test/resources/org/sonar/server/issue/ws/IssueSearchActionTest/issues_with_rules.json [new file with mode: 0644]
sonar-server/src/test/resources/org/sonar/server/issue/ws/IssueSearchActionTest/issues_with_users.json [new file with mode: 0644]
sonar-server/src/test/resources/org/sonar/server/issue/ws/IssueShowActionTest/show_issue_with_actions_defined_by_plugins.json [deleted file]
sonar-server/src/test/resources/org/sonar/server/issue/ws/IssueShowActionTest/show_issue_with_assign_to_me_action.json [deleted file]
sonar-server/src/test/resources/org/sonar/server/issue/ws/IssueShowActionTest/show_issue_with_set_severity_action.json [deleted file]
sonar-server/src/test/resources/org/sonar/server/issue/ws/IssueShowActionTest/show_issue_without_assign_to_me_action.json [deleted file]

index ee422a760704e12f7c7a17dd98d279f7bec24586..f4c0a40d3f407b85bfad435a82e8afb818828295 100644 (file)
@@ -22,9 +22,11 @@ package org.sonar.api.server.ws;
 import com.google.common.base.Splitter;
 import com.google.common.collect.Lists;
 import org.apache.commons.lang.StringUtils;
+import org.sonar.api.utils.DateUtils;
 
 import javax.annotation.CheckForNull;
 
+import java.util.Date;
 import java.util.List;
 
 /**
@@ -93,6 +95,7 @@ public abstract class Request {
     return values;
   }
 
+  @CheckForNull
   public List<String> paramAsStrings(String key) {
     String value = param(key);
     if (value == null) {
@@ -182,9 +185,16 @@ public abstract class Request {
     return result;
   }
 
-//  @CheckForNull
-//  public Date paramAsDate(String key) {
-//    String s = param(key);
-//    return s == null ? null : Long.parseLong(s);
-//  }
+  @CheckForNull
+  public Date paramAsDate(String key) {
+    String s = param(key);
+    if (s != null) {
+      Date date = DateUtils.parseDateTimeQuietly(s);
+      if (date != null) {
+        return date;
+      }
+      return DateUtils.parseDate(s);
+    }
+    return null;
+  }
 }
index 9d2926460f6d75c6aae883f04deb9d030cdb40bd..a4b34acd65319a805e962166e7e8a85edf2c4186 100644 (file)
@@ -24,8 +24,10 @@ import org.junit.Before;
 import org.junit.Test;
 import org.sonar.api.rule.RuleStatus;
 import org.sonar.api.server.ws.internal.ValidatingRequest;
+import org.sonar.api.utils.DateUtils;
 
 import javax.annotation.Nullable;
+
 import java.util.Map;
 
 import static org.fest.assertions.Assertions.assertThat;
@@ -71,6 +73,7 @@ public class RequestTest {
       action.createParam("a_boolean");
       action.createParam("a_number");
       action.createParam("a_enum");
+      action.createParam("a_date");
 
       action.createParam("a_required_string").setRequired(true);
       action.createParam("a_required_boolean").setRequired(true);
@@ -177,6 +180,12 @@ public class RequestTest {
       RuleStatus.BETA, RuleStatus.READY);
   }
 
+  @Test
+  public void param_as_date() throws Exception {
+    assertThat(request.setParam("a_date", "2014-05-27").paramAsDate("a_date")).isEqualTo(DateUtils.parseDate("2014-05-27"));
+    assertThat(request.setParam("a_date", "2014-05-27T15:50:45+0100").paramAsDate("a_date")).isEqualTo(DateUtils.parseDateTime("2014-05-27T15:50:45+0100"));
+  }
+
   @Test
   public void param_as_strings() throws Exception {
     assertThat(request.paramAsStrings("a_string")).isNull();
diff --git a/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueActionsWriter.java b/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueActionsWriter.java
new file mode 100644 (file)
index 0000000..e4d7cfa
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 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.ws;
+
+import org.sonar.api.ServerComponent;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.issue.action.Action;
+import org.sonar.api.issue.internal.DefaultIssue;
+import org.sonar.api.utils.text.JsonWriter;
+import org.sonar.api.web.UserRole;
+import org.sonar.core.issue.workflow.Transition;
+import org.sonar.server.issue.ActionService;
+import org.sonar.server.issue.IssueService;
+import org.sonar.server.user.UserSession;
+
+import java.util.List;
+
+import static com.google.common.collect.Lists.newArrayList;
+
+public class IssueActionsWriter implements ServerComponent {
+
+  private final IssueService issueService;
+  private final ActionService actionService;
+
+  public IssueActionsWriter(IssueService issueService, ActionService actionService) {
+    this.issueService = issueService;
+    this.actionService = actionService;
+  }
+
+  public void writeTransitions(Issue issue, JsonWriter json) {
+    json.name("transitions").beginArray();
+    if (UserSession.get().isLoggedIn()) {
+      List<Transition> transitions = issueService.listTransitions(issue, UserSession.get());
+      for (Transition transition : transitions) {
+        json.value(transition.key());
+      }
+    }
+    json.endArray();
+  }
+
+  public void writeActions(Issue issue, JsonWriter json) {
+    json.name("actions").beginArray();
+    for (String action : actions((DefaultIssue) issue)) {
+      json.value(action);
+    }
+    json.endArray();
+  }
+
+  private List<String> actions(DefaultIssue issue) {
+    List<String> actions = newArrayList();
+    String login = UserSession.get().login();
+    if (login != null) {
+      actions.add("comment");
+      if (issue.resolution() == null) {
+        actions.add("assign");
+        if (!login.equals(issue.assignee())) {
+          actions.add("assign_to_me");
+        }
+        actions.add("plan");
+        String projectKey = issue.projectKey();
+        if (projectKey != null && UserSession.get().hasProjectPermission(UserRole.ISSUE_ADMIN, projectKey)) {
+          actions.add("set_severity");
+        }
+        for (Action action : actionService.listAvailableActions(issue)) {
+          actions.add(action.key());
+        }
+      }
+    }
+    return actions;
+  }
+
+}
diff --git a/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueSearchAction.java b/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueSearchAction.java
new file mode 100644 (file)
index 0000000..36d9897
--- /dev/null
@@ -0,0 +1,420 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 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.ws;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+import com.google.common.io.Resources;
+import org.sonar.api.component.Component;
+import org.sonar.api.i18n.I18n;
+import org.sonar.api.issue.*;
+import org.sonar.api.issue.internal.DefaultIssue;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rule.Severity;
+import org.sonar.api.rules.Rule;
+import org.sonar.api.server.ws.Request;
+import org.sonar.api.server.ws.RequestHandler;
+import org.sonar.api.server.ws.Response;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.api.user.User;
+import org.sonar.api.utils.DateUtils;
+import org.sonar.api.utils.Duration;
+import org.sonar.api.utils.Durations;
+import org.sonar.api.utils.text.JsonWriter;
+import org.sonar.api.web.UserRole;
+import org.sonar.core.component.ComponentDto;
+import org.sonar.server.issue.filter.IssueFilterParameters;
+import org.sonar.server.user.UserSession;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+import static com.google.common.collect.Lists.newArrayList;
+
+public class IssueSearchAction implements RequestHandler {
+
+  private static final String EXTRA_FIELDS_PARAM = "extra_fields";
+
+  private final IssueFinder issueFinder;
+  private final IssueActionsWriter actionsWriter;
+  private final I18n i18n;
+  private final Durations durations;
+
+  public IssueSearchAction(IssueFinder issueFinder, IssueActionsWriter actionsWriter, I18n i18n, Durations durations) {
+    this.issueFinder = issueFinder;
+    this.actionsWriter = actionsWriter;
+    this.i18n = i18n;
+    this.durations = durations;
+  }
+
+  void define(WebService.NewController controller) {
+    WebService.NewAction action = controller.createAction("search")
+      .setDescription("Get a list of issues. If the number of issues is greater than 10,000, only the first 10,000 ones are returned by the web service. " +
+        "Requires Browse permission on project(s)")
+      .setSince("3.6")
+      .setHandler(this)
+      .setResponseExample(Resources.getResource(this.getClass(), "example-search.json"));
+
+    action.createParam(IssueFilterParameters.ISSUES)
+      .setDescription("Comma-separated list of issue keys")
+      .setExampleValue("5bccd6e8-f525-43a2-8d76-fcb13dde79ef");
+    action.createParam(IssueFilterParameters.SEVERITIES)
+      .setDescription("Comma-separated list of severities")
+      .setExampleValue(Severity.BLOCKER + "," + Severity.CRITICAL)
+      .setPossibleValues(Severity.ALL);
+    action.createParam(IssueFilterParameters.STATUSES)
+      .setDescription("Comma-separated list of statuses")
+      .setExampleValue(Issue.STATUS_OPEN + "," + Issue.STATUS_REOPENED)
+      .setPossibleValues(Issue.STATUSES);
+    action.createParam(IssueFilterParameters.RESOLUTIONS)
+      .setDescription("Comma-separated list of resolutions")
+      .setExampleValue(Issue.RESOLUTION_FIXED + "," + Issue.RESOLUTION_REMOVED)
+      .setPossibleValues(Issue.RESOLUTIONS);
+    action.createParam(IssueFilterParameters.RESOLVED)
+      .setDescription("To match resolved or unresolved issues")
+      .setBooleanPossibleValues();
+    action.createParam(IssueFilterParameters.COMPONENTS)
+      .setDescription("To retrieve issues associated to a specific list of components (comma-separated list of component keys). " +
+        "Note that if you set the value to a project key, only issues associated to this project are retrieved. " +
+        "Issues associated to its sub-components (such as files, packages, etc.) are not retrieved. See also componentRoots")
+      .setExampleValue("org.apache.struts:struts:org.apache.struts.Action");
+    action.createParam(IssueFilterParameters.COMPONENT_ROOTS)
+      .setDescription("To retrieve issues associated to a specific list of components and their sub-components (comma-separated list of component keys). " +
+        "Views are not supported")
+      .setExampleValue("org.apache.struts:struts");
+    action.createParam(IssueFilterParameters.RULES)
+      .setDescription("Comma-separated list of coding rule keys. Format is <repository>:<rule>")
+      .setExampleValue("squid:AvoidCycles");
+    action.createParam(IssueFilterParameters.HIDE_RULES)
+      .setDescription("To not return rules")
+      .setBooleanPossibleValues();
+    action.createParam(IssueFilterParameters.ACTION_PLANS)
+      .setDescription("Comma-separated list of action plan keys (not names)")
+      .setExampleValue("3f19de90-1521-4482-a737-a311758ff513");
+    action.createParam(IssueFilterParameters.PLANNED)
+      .setDescription("To retrieve issues associated to an action plan or not")
+      .setBooleanPossibleValues();
+    action.createParam(IssueFilterParameters.REPORTERS)
+      .setDescription("Comma-separated list of reporter logins")
+      .setExampleValue("admin");
+    action.createParam(IssueFilterParameters.ASSIGNEES)
+      .setDescription("Comma-separated list of assignee logins")
+      .setExampleValue("admin,usera");
+    action.createParam(IssueFilterParameters.ASSIGNED)
+      .setDescription("To retrieve assigned or unassigned issues")
+      .setBooleanPossibleValues();
+    action.createParam(IssueFilterParameters.LANGUAGES)
+      .setDescription("Comma-separated list of languages. Available since 4.4")
+      .setExampleValue("java,js");
+    action.createParam(EXTRA_FIELDS_PARAM)
+      .setDescription("Add some extra fields on each issue. Available since 4.4")
+      .setPossibleValues("actions", "transitions", "assigneeName", "actionPlanName");
+    action.createParam(IssueFilterParameters.CREATED_AT)
+      .setDescription("To retrieve issues created at a given date. Format: date or datetime ISO formats")
+      .setExampleValue("2013-05-01 (or 2013-05-01T13:00:00+0100)");
+    action.createParam(IssueFilterParameters.CREATED_AFTER)
+      .setDescription("To retrieve issues created after the given date (inclusive). Format: date or datetime ISO formats")
+      .setExampleValue("2013-05-01 (or 2013-05-01T13:00:00+0100)");
+    action.createParam(IssueFilterParameters.CREATED_BEFORE)
+      .setDescription("To retrieve issues created before the given date (exclusive). Format: date or datetime ISO formats")
+      .setExampleValue("2013-05-01 (or 2013-05-01T13:00:00+0100)");
+    action.createParam(IssueFilterParameters.PAGE_SIZE)
+      .setDescription("Maximum number of results per page. " +
+        "Default value: 100 (except when the 'components' parameter is set, value is set to \"-1\" in this case). " +
+        "If set to \"-1\", the max possible value is used")
+      .setExampleValue("50");
+    action.createParam(IssueFilterParameters.PAGE_INDEX)
+      .setDescription("Index of the selected page")
+      .setDefaultValue("1")
+      .setExampleValue("2");
+    action.createParam(IssueFilterParameters.SORT)
+      .setDescription("Sort field")
+      .setPossibleValues(IssueQuery.SORTS);
+    action.createParam(IssueFilterParameters.ASC)
+      .setDescription("Ascending sort")
+      .setBooleanPossibleValues();
+  }
+
+  @Override
+  public void handle(Request request, Response response) {
+    IssueQueryResult queryResult = issueFinder.find(createQuery(request));
+
+    JsonWriter json = response.newJsonWriter();
+    json.beginObject();
+
+    writePaging(queryResult, json);
+    writeIssues(queryResult, request.paramAsStrings(EXTRA_FIELDS_PARAM), json);
+    writeComponents(queryResult, json);
+    writeProjects(queryResult, json);
+    writeRules(queryResult, json);
+    writeUsers(queryResult, json);
+
+    json.endObject().close();
+  }
+
+  private void writePaging(IssueQueryResult result, JsonWriter json) {
+    json.prop("maxResultsReached", result.maxResultsReached());
+    json.name("paging").beginObject()
+      .prop("pageIndex", result.paging().pageIndex())
+      .prop("pageSize", result.paging().pageSize())
+      .prop("total", result.paging().total())
+      .prop("fTotal", i18n.formatInteger(UserSession.get().locale(), result.paging().total()))
+      .prop("pages", result.paging().pages())
+      .endObject();
+  }
+
+  private void writeIssues(IssueQueryResult result, @Nullable List<String> extraFields, JsonWriter json) {
+    json.name("issues").beginArray();
+
+    for (Issue i : result.issues()) {
+      json.beginObject();
+
+      DefaultIssue issue = (DefaultIssue) i;
+      String actionPlanKey = issue.actionPlanKey();
+      Duration debt = issue.debt();
+      Date updateDate = issue.updateDate();
+      Date closeDate = issue.closeDate();
+
+      json
+        .prop("key", issue.key())
+        .prop("component", issue.componentKey())
+        .prop("project", issue.projectKey())
+        .prop("rule", issue.ruleKey().toString())
+        .prop("status", issue.status())
+        .prop("resolution", issue.resolution())
+        .prop("severity", issue.severity())
+        .prop("message", issue.message())
+        .prop("line", issue.line())
+        .prop("debt", debt != null ? durations.format(UserSession.get().locale(), debt, Durations.DurationFormat.SHORT) : null)
+        .prop("reporter", issue.reporter())
+        .prop("assignee", issue.assignee())
+        .prop("author", issue.authorLogin())
+        .prop("actionPlan", actionPlanKey)
+        .prop("creationDate", DateUtils.formatDateTime(issue.creationDate()))
+        .prop("updateDate", updateDate != null ? DateUtils.formatDateTime(updateDate) : null)
+        .prop("fUpdateAge", formatAgeDate(updateDate))
+        .prop("closeDate", closeDate != null ? DateUtils.formatDateTime(closeDate) : null);
+
+      writeIssueAttributes(issue, json);
+      writeIssueExtraFields(result, issue, extraFields, json);
+      json.endObject();
+    }
+
+    json.endArray();
+  }
+
+  private void writeIssueAttributes(Issue issue, JsonWriter json) {
+    if (!issue.attributes().isEmpty()) {
+      json.name("attr").beginObject();
+      for (Map.Entry<String, String> entry : issue.attributes().entrySet()) {
+        json.prop(entry.getKey(), entry.getValue());
+      }
+      json.endObject();
+    }
+  }
+
+  private void writeIssueExtraFields(IssueQueryResult result, Issue issue, @Nullable List<String> extraFields, JsonWriter json) {
+    if (extraFields != null) {
+      if (extraFields.contains("actions")) {
+        actionsWriter.writeActions(issue, json);
+      }
+      if (extraFields.contains("transitions")) {
+        actionsWriter.writeTransitions(issue, json);
+      }
+      String assignee = issue.assignee();
+      if (extraFields.contains("assigneeName") && assignee != null) {
+        User user = result.user(assignee);
+        json.prop("assigneeName", user != null ? user.name() : null);
+      }
+      String actionPlanKey = issue.actionPlanKey();
+      if (extraFields.contains("actionPlanName") && actionPlanKey != null) {
+        ActionPlan actionPlan = result.actionPlan(issue);
+        json.prop("actionPlanName", actionPlan != null ? actionPlan.name() : null);
+      }
+    }
+  }
+
+  private void writeComponents(IssueQueryResult result, JsonWriter json) {
+    json.name("components").beginArray();
+    for (Component component : result.components()) {
+      ComponentDto componentDto = (ComponentDto) component;
+      json.beginObject()
+        .prop("key", component.key())
+        .prop("id", componentDto.getId())
+        .prop("qualifier", component.qualifier())
+        .prop("name", component.name())
+        .prop("longName", component.longName())
+        .prop("path", component.path())
+          // On a root project, subProjectId is null but projectId is equal to itself, which make no sense.
+        .prop("projectId", (componentDto.projectId() != null && componentDto.subProjectId() != null) ? componentDto.projectId() : null)
+        .prop("subProjectId", componentDto.subProjectId())
+        .endObject();
+    }
+    json.endArray();
+  }
+
+  private void writeProjects(IssueQueryResult result, JsonWriter json) {
+    json.name("projects").beginArray();
+    for (Component project : result.projects()) {
+      ComponentDto componentDto = (ComponentDto) project;
+      json.beginObject()
+        .prop("key", project.key())
+        .prop("id", componentDto.getId())
+        .prop("qualifier", project.qualifier())
+        .prop("name", project.name())
+        .prop("longName", project.longName())
+        .endObject();
+    }
+    json.endArray();
+  }
+
+  private void writeRules(IssueQueryResult result, JsonWriter json) {
+    json.name("rules").beginArray();
+    for (Rule rule : result.rules()) {
+      json.beginObject()
+        .prop("key", rule.ruleKey().toString())
+        .prop("name", rule.getName())
+        .prop("desc", rule.getDescription())
+        .prop("status", rule.getStatus())
+        .endObject();
+    }
+    json.endArray();
+  }
+
+  private void writeUsers(IssueQueryResult result, JsonWriter json) {
+    json.name("users").beginArray();
+    for (User user : result.users()) {
+      json.beginObject()
+        .prop("login", user.login())
+        .prop("name", user.name())
+        .prop("active", user.active())
+        .prop("email", user.email())
+        .endObject();
+    }
+    json.endArray();
+  }
+
+
+//
+//  private void writeTransitions(Issue issue, JsonWriter json) {
+//    json.name("transitions").beginArray();
+//    if (UserSession.get().isLoggedIn()) {
+//      List<Transition> transitions = issueService.listTransitions(issue, UserSession.get());
+//      for (Transition transition : transitions) {
+//        json.value(transition.key());
+//      }
+//    }
+//    json.endArray();
+//  }
+//
+//  private void writeActions(DefaultIssue issue, JsonWriter json) {
+//    json.name("actions").beginArray();
+//    for (String action : actions(issue)) {
+//      json.value(action);
+//    }
+//    json.endArray();
+//  }
+//
+//  private List<String> actions(DefaultIssue issue) {
+//    List<String> actions = newArrayList();
+//    String login = UserSession.get().login();
+//    if (login != null) {
+//      actions.add("comment");
+//      if (issue.resolution() == null) {
+//        actions.add("assign");
+//        if (!login.equals(issue.assignee())) {
+//          actions.add("assign_to_me");
+//        }
+//        actions.add("plan");
+//        String projectKey = issue.projectKey();
+//        if (projectKey != null && UserSession.get().hasProjectPermission(UserRole.ISSUE_ADMIN, projectKey)) {
+//          actions.add("set_severity");
+//        }
+//        for (Action action : actionService.listAvailableActions(issue)) {
+//          actions.add(action.key());
+//        }
+//      }
+//    }
+//    return actions;
+//  }
+//
+
+  @CheckForNull
+  private String formatAgeDate(@Nullable Date date) {
+    if (date != null) {
+      return i18n.ageFromNow(UserSession.get().locale(), date);
+    }
+    return null;
+  }
+
+  @CheckForNull
+  private static Collection<RuleKey> stringsToRules(@Nullable Collection<String> rules) {
+    if (rules != null) {
+      return newArrayList(Iterables.transform(rules, new Function<String, RuleKey>() {
+        @Override
+        public RuleKey apply(@Nullable String s) {
+          return s != null ? RuleKey.parse(s) : null;
+        }
+      }));
+    }
+    return null;
+  }
+
+  @VisibleForTesting
+  static IssueQuery createQuery(Request request) {
+    IssueQuery.Builder builder = IssueQuery.builder()
+      .requiredRole(UserRole.USER)
+      .issueKeys(request.paramAsStrings(IssueFilterParameters.ISSUES))
+      .severities(request.paramAsStrings(IssueFilterParameters.SEVERITIES))
+      .statuses(request.paramAsStrings(IssueFilterParameters.STATUSES))
+      .resolutions(request.paramAsStrings(IssueFilterParameters.RESOLUTIONS))
+      .resolved(request.paramAsBoolean(IssueFilterParameters.RESOLVED))
+      .components(request.paramAsStrings(IssueFilterParameters.COMPONENTS))
+      .componentRoots(request.paramAsStrings(IssueFilterParameters.COMPONENT_ROOTS))
+      .rules(stringsToRules(request.paramAsStrings(IssueFilterParameters.RULES)))
+      .actionPlans(request.paramAsStrings(IssueFilterParameters.ACTION_PLANS))
+      .reporters(request.paramAsStrings(IssueFilterParameters.REPORTERS))
+      .assignees(request.paramAsStrings(IssueFilterParameters.ASSIGNEES))
+      .languages(request.paramAsStrings(IssueFilterParameters.LANGUAGES))
+      .assigned(request.paramAsBoolean(IssueFilterParameters.ASSIGNED))
+      .planned(request.paramAsBoolean(IssueFilterParameters.PLANNED))
+      .hideRules(request.paramAsBoolean(IssueFilterParameters.HIDE_RULES))
+      .createdAt(request.paramAsDate(IssueFilterParameters.CREATED_AT))
+      .createdAfter(request.paramAsDate(IssueFilterParameters.CREATED_AFTER))
+      .createdBefore(request.paramAsDate(IssueFilterParameters.CREATED_BEFORE))
+      .pageSize(request.paramAsInt(IssueFilterParameters.PAGE_SIZE))
+      .pageIndex(request.paramAsInt(IssueFilterParameters.PAGE_INDEX));
+    String sort = request.param(IssueFilterParameters.SORT);
+    if (!Strings.isNullOrEmpty(sort)) {
+      builder.sort(sort);
+      builder.asc(request.paramAsBoolean(IssueFilterParameters.ASC));
+    }
+    return builder.build();
+  }
+
+}
index ec121b7c2069faf1d9a9ed4b8552dca79f294f01..2a94bd9b2acd1c94380102f5a4d568c8d5a81d63 100644 (file)
@@ -25,7 +25,6 @@ import com.google.common.io.Resources;
 import org.sonar.api.component.Component;
 import org.sonar.api.i18n.I18n;
 import org.sonar.api.issue.*;
-import org.sonar.api.issue.action.Action;
 import org.sonar.api.issue.internal.DefaultIssue;
 import org.sonar.api.issue.internal.FieldDiffs;
 import org.sonar.api.server.debt.DebtCharacteristic;
@@ -41,14 +40,11 @@ import org.sonar.api.utils.Durations;
 import org.sonar.api.utils.text.JsonWriter;
 import org.sonar.api.web.UserRole;
 import org.sonar.core.component.ComponentDto;
-import org.sonar.core.issue.workflow.Transition;
 import org.sonar.markdown.Markdown;
 import org.sonar.server.debt.DebtModelService;
 import org.sonar.server.exceptions.NotFoundException;
-import org.sonar.server.issue.ActionService;
 import org.sonar.server.issue.IssueChangelog;
 import org.sonar.server.issue.IssueChangelogService;
-import org.sonar.server.issue.IssueService;
 import org.sonar.server.user.UserSession;
 
 import javax.annotation.CheckForNull;
@@ -58,24 +54,20 @@ import java.util.Arrays;
 import java.util.Date;
 import java.util.List;
 
-import static com.google.common.collect.Lists.newArrayList;
-
 public class IssueShowAction implements RequestHandler {
 
   private final IssueFinder issueFinder;
-  private final IssueService issueService;
   private final IssueChangelogService issueChangelogService;
-  private final ActionService actionService;
+  private final IssueActionsWriter actionsWriter;
   private final DebtModelService debtModel;
   private final I18n i18n;
   private final Durations durations;
 
-  public IssueShowAction(IssueFinder issueFinder, IssueService issueService, IssueChangelogService issueChangelogService, ActionService actionService,
+  public IssueShowAction(IssueFinder issueFinder, IssueChangelogService issueChangelogService, IssueActionsWriter actionsWriter,
                          DebtModelService debtModel, I18n i18n, Durations durations) {
     this.issueFinder = issueFinder;
-    this.issueService = issueService;
     this.issueChangelogService = issueChangelogService;
-    this.actionService = actionService;
+    this.actionsWriter = actionsWriter;
     this.debtModel = debtModel;
     this.i18n = i18n;
     this.durations = durations;
@@ -96,7 +88,9 @@ public class IssueShowAction implements RequestHandler {
   @Override
   public void handle(Request request, Response response) {
     String issueKey = request.mandatoryParam("key");
-    IssueQueryResult queryResult = issueFinder.find(IssueQuery.builder().issueKeys(Arrays.asList(issueKey)).build());
+    IssueQueryResult queryResult = issueFinder.find(IssueQuery.builder()
+      .requiredRole(UserRole.USER)
+      .issueKeys(Arrays.asList(issueKey)).build());
     if (queryResult.issues().size() != 1) {
       throw new NotFoundException("Issue not found: " + issueKey);
     }
@@ -106,8 +100,8 @@ public class IssueShowAction implements RequestHandler {
     json.beginObject().name("issue").beginObject();
 
     writeIssue(queryResult, issue, json);
-    writeTransitions(issue, json);
-    writeActions(issue, json);
+    actionsWriter.writeActions(issue, json);
+    actionsWriter.writeTransitions(issue, json);
     writeComments(queryResult, issue, json);
     writeChangelog(issue, json);
 
@@ -220,48 +214,6 @@ public class IssueShowAction implements RequestHandler {
     return null;
   }
 
-  private void writeTransitions(Issue issue, JsonWriter json) {
-    json.name("transitions").beginArray();
-    if (UserSession.get().isLoggedIn()) {
-      List<Transition> transitions = issueService.listTransitions(issue, UserSession.get());
-      for (Transition transition : transitions) {
-        json.value(transition.key());
-      }
-    }
-    json.endArray();
-  }
-
-  private void writeActions(DefaultIssue issue, JsonWriter json) {
-    json.name("actions").beginArray();
-    for (String action : actions(issue)) {
-      json.value(action);
-    }
-    json.endArray();
-  }
-
-  private List<String> actions(DefaultIssue issue) {
-    List<String> actions = newArrayList();
-    String login = UserSession.get().login();
-    if (login != null) {
-      actions.add("comment");
-      if (issue.resolution() == null) {
-        actions.add("assign");
-        if (!login.equals(issue.assignee())) {
-          actions.add("assign_to_me");
-        }
-        actions.add("plan");
-        String projectKey = issue.projectKey();
-        if (projectKey != null && UserSession.get().hasProjectPermission(UserRole.ISSUE_ADMIN, projectKey)) {
-          actions.add("set_severity");
-        }
-        for (Action action : actionService.listAvailableActions(issue)) {
-          actions.add(action.key());
-        }
-      }
-    }
-    return actions;
-  }
-
   private void writeComments(IssueQueryResult queryResult, Issue issue, JsonWriter json) {
     json.name("comments").beginArray();
     String login = UserSession.get().login();
@@ -311,7 +263,7 @@ public class IssueShowAction implements RequestHandler {
     json.endArray();
   }
 
-  private void addUserWithLabel(IssueQueryResult result, @Nullable String value, String field, JsonWriter json) {
+  private static void addUserWithLabel(IssueQueryResult result, @Nullable String value, String field, JsonWriter json) {
     if (value != null) {
       User user = result.user(value);
       json
index 302d5a0c4877701baaeda2b9d56a2635a6e67ac9..d7de0ebe9dbe062a8184cb1487b52286c231e0b0 100644 (file)
@@ -21,17 +21,18 @@ package org.sonar.server.issue.ws;
 
 import com.google.common.io.Resources;
 import org.sonar.api.issue.DefaultTransitions;
-import org.sonar.api.issue.Issue;
 import org.sonar.api.rule.Severity;
 import org.sonar.api.server.ws.RailsHandler;
 import org.sonar.api.server.ws.WebService;
 
 public class IssuesWs implements WebService {
 
-  private final IssueShowAction showHandler;
+  private final IssueShowAction showAction;
+  private final IssueSearchAction searchAction;
 
-  public IssuesWs(IssueShowAction showHandler) {
-    this.showHandler = showHandler;
+  public IssuesWs(IssueShowAction showAction, IssueSearchAction searchAction) {
+    this.showAction = showAction;
+    this.searchAction = searchAction;
   }
 
   @Override
@@ -40,8 +41,9 @@ public class IssuesWs implements WebService {
     controller.setDescription("Coding rule issues");
     controller.setSince("3.6");
 
-    showHandler.define(controller);
-    defineSearchAction(controller);
+    showAction.define(controller);
+    searchAction.define(controller);
+
     defineChangelogAction(controller);
     defineAssignAction(controller);
     defineAddCommentAction(controller);
@@ -58,80 +60,6 @@ public class IssuesWs implements WebService {
     controller.done();
   }
 
-  private void defineSearchAction(NewController controller) {
-    WebService.NewAction action = controller.createAction("search")
-      .setDescription("Get a list of issues. If the number of issues is greater than 10,000, only the first 10,000 ones are returned by the web service. " +
-        "Requires Browse permission on project(s)")
-      .setSince("3.6")
-      .setHandler(RailsHandler.INSTANCE)
-      .setResponseExample(Resources.getResource(this.getClass(), "example-search.json"));
-
-    action.createParam("issues")
-      .setDescription("Comma-separated list of issue keys")
-      .setExampleValue("5bccd6e8-f525-43a2-8d76-fcb13dde79ef");
-    action.createParam("severities")
-      .setDescription("Comma-separated list of severities")
-      .setExampleValue(Severity.BLOCKER + "," + Severity.CRITICAL)
-      .setPossibleValues(Severity.ALL);
-    action.createParam("statuses")
-      .setDescription("Comma-separated list of statuses")
-      .setExampleValue(Issue.STATUS_OPEN + "," + Issue.STATUS_REOPENED)
-      .setPossibleValues(Issue.STATUSES);
-    action.createParam("resolutions")
-      .setDescription("Comma-separated list of resolutions")
-      .setExampleValue(Issue.RESOLUTION_FIXED + "," + Issue.RESOLUTION_REMOVED)
-      .setPossibleValues(Issue.RESOLUTIONS);
-    action.createParam("resolved")
-      .setDescription("To match resolved or unresolved issues")
-      .setBooleanPossibleValues();
-    action.createParam("components")
-      .setDescription("To retrieve issues associated to a specific list of components (comma-separated list of component keys). " +
-        "Note that if you set the value to a project key, only issues associated to this project are retrieved. " +
-        "Issues associated to its sub-components (such as files, packages, etc.) are not retrieved. See also componentRoots")
-      .setExampleValue("org.apache.struts:struts:org.apache.struts.Action");
-    action.createParam("componentRoots")
-      .setDescription("To retrieve issues associated to a specific list of components and their sub-components (comma-separated list of component keys). " +
-        "Views are not supported")
-      .setExampleValue("org.apache.struts:struts");
-    action.createParam("rules")
-      .setDescription("Comma-separated list of coding rule keys. Format is <repository>:<rule>")
-      .setExampleValue("squid:AvoidCycles");
-    action.createParam("actionPlans")
-      .setDescription("Comma-separated list of action plan keys (not names)")
-      .setExampleValue("3f19de90-1521-4482-a737-a311758ff513");
-    action.createParam("planned")
-      .setDescription("To retrieve issues associated to an action plan or not")
-      .setBooleanPossibleValues();
-    action.createParam("reporters")
-      .setDescription("Comma-separated list of reporter logins")
-      .setExampleValue("admin");
-    action.createParam("assignees")
-      .setDescription("Comma-separated list of assignee logins")
-      .setExampleValue("admin,usera");
-    action.createParam("assigned")
-      .setDescription("To retrieve assigned or unassigned issues")
-      .setBooleanPossibleValues();
-    action.createParam("extra_fields")
-      .setDescription("Add some extra fields on each issue. Available since 4.4")
-      .setPossibleValues("actions", "transitions");
-    action.createParam("createdAfter")
-      .setDescription("To retrieve issues created after the given date (inclusive). Format: date or datetime ISO formats")
-      .setExampleValue("2013-05-01 (or 2013-05-01T13:00:00+0100)");
-    action.createParam("createdBefore")
-      .setDescription("To retrieve issues created before the given date (exclusive). Format: date or datetime ISO formats")
-      .setExampleValue("2013-05-01 (or 2013-05-01T13:00:00+0100)");
-    action.createParam("pageSize")
-      .setDescription("Maximum number of results per page. " +
-        "Default value: 100 (except when the 'components' parameter is set, value is set to \"-1\" in this case). " +
-        "If set to \"-1\", the max possible value is used")
-      .setExampleValue("50");
-    action.createParam("pageIndex")
-      .setDescription("Index of the selected page")
-      .setDefaultValue("1")
-      .setExampleValue("2");
-    RailsHandler.addFormatParam(action);
-  }
-
   private void defineChangelogAction(NewController controller) {
     WebService.NewAction action = controller.createAction("changelog")
       .setDescription("Display changelog of an issue")
index 180cbcfdf3126c7af3b50d931d08ac31176366a4..99fed3068f401b8318b612833378dbfc8a9b3be1 100644 (file)
@@ -96,6 +96,8 @@ import org.sonar.server.issue.actionplan.ActionPlanWs;
 import org.sonar.server.issue.filter.IssueFilterService;
 import org.sonar.server.issue.filter.IssueFilterWriter;
 import org.sonar.server.issue.filter.IssueFilterWs;
+import org.sonar.server.issue.ws.IssueActionsWriter;
+import org.sonar.server.issue.ws.IssueSearchAction;
 import org.sonar.server.issue.ws.IssueShowAction;
 import org.sonar.server.issue.ws.IssuesWs;
 import org.sonar.server.measure.MeasureFilterEngine;
@@ -421,8 +423,10 @@ class ServerComponents {
     pico.addSingleton(Actions.class);
     pico.addSingleton(IssueBulkChangeService.class);
     pico.addSingleton(IssueChangelogFormatter.class);
-    pico.addSingleton(IssueShowAction.class);
     pico.addSingleton(IssuesWs.class);
+    pico.addSingleton(IssueShowAction.class);
+    pico.addSingleton(IssueSearchAction.class);
+    pico.addSingleton(IssueActionsWriter.class);
 
     // issue filters
     pico.addSingleton(IssueFilterService.class);
index d22e748ea58aec6389f8ae5a222b10f3eb3f893e..8c248e73108abfeec784226a08c9f99b1884a201 100644 (file)
@@ -29,6 +29,9 @@
           "createdAt": "2013-05-13T18:08:34+0200"
         }
       ],
+      "attr": {
+        "jira-issue-key": "SONAR-1234"
+      },
       "actions": ["link-to-jira"],
       "transitions": [
         "unconfirm",
diff --git a/sonar-server/src/test/java/org/sonar/server/issue/ws/IssueActionsWriterTest.java b/sonar-server/src/test/java/org/sonar/server/issue/ws/IssueActionsWriterTest.java
new file mode 100644 (file)
index 0000000..bb8d97e
--- /dev/null
@@ -0,0 +1,209 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 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.ws;
+
+import org.json.JSONException;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.issue.action.Action;
+import org.sonar.api.issue.internal.DefaultIssue;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.utils.text.JsonWriter;
+import org.sonar.api.web.UserRole;
+import org.sonar.core.issue.workflow.Transition;
+import org.sonar.server.issue.ActionService;
+import org.sonar.server.issue.IssueService;
+import org.sonar.server.user.MockUserSession;
+import org.sonar.server.user.UserSession;
+
+import java.io.StringWriter;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public class IssueActionsWriterTest {
+
+  @Mock
+  IssueService issueService;
+
+  @Mock
+  ActionService actionService;
+
+  IssueActionsWriter writer;
+
+  @Before
+  public void setUp() throws Exception {
+    writer = new IssueActionsWriter(issueService, actionService);
+  }
+
+  @Test
+  public void write_all_standard_actions() throws Exception {
+    Issue issue = new DefaultIssue()
+      .setKey("ABCD")
+      .setComponentKey("sample:src/main/xoo/sample/Sample.xoo")
+      .setProjectKey("sample")
+      .setRuleKey(RuleKey.of("squid", "AvoidCycle"));
+
+    MockUserSession.set().setLogin("john").addProjectPermissions(UserRole.ISSUE_ADMIN, "sample");
+
+    testActions(issue,
+      "{\"actions\": " +
+        "[" +
+        "\"comment\", \"assign\", \"assign_to_me\", \"plan\", \"set_severity\"\n" +
+        "]}"
+    );
+  }
+
+  @Test
+  public void write_plugin_actions() throws Exception {
+    Issue issue = new DefaultIssue()
+      .setKey("ABCD")
+      .setComponentKey("sample:src/main/xoo/sample/Sample.xoo")
+      .setProjectKey("sample")
+      .setRuleKey(RuleKey.of("squid", "AvoidCycle"));
+
+    MockUserSession.set().setLogin("john");
+    Action action = mock(Action.class);
+    when(action.key()).thenReturn("link-to-jira");
+    when(actionService.listAvailableActions(eq(issue))).thenReturn(newArrayList(action));
+
+    testActions(issue,
+      "{\"actions\": " +
+        "[" +
+        "\"comment\", \"assign\", \"assign_to_me\", \"plan\", \"link-to-jira\"\n" +
+        "]}"
+    );
+  }
+
+  @Test
+  public void write_only_comment_action() throws Exception {
+    Issue issue = new DefaultIssue()
+      .setKey("ABCD")
+      .setComponentKey("sample:src/main/xoo/sample/Sample.xoo")
+      .setProjectKey("sample")
+      .setRuleKey(RuleKey.of("squid", "AvoidCycle"))
+      .setResolution("CLOSED");
+
+    MockUserSession.set().setLogin("john");
+
+    testActions(issue,
+      "{\"actions\": " +
+        "[" +
+        "\"comment\"" +
+        "]}"
+    );
+  }
+
+  @Test
+  public void write_no_action_if_not_logged() throws Exception {
+    Issue issue = new DefaultIssue()
+      .setKey("ABCD")
+      .setComponentKey("sample:src/main/xoo/sample/Sample.xoo")
+      .setProjectKey("sample")
+      .setRuleKey(RuleKey.of("squid", "AvoidCycle"));
+
+    MockUserSession.set();
+
+    testActions(issue,
+      "{\"actions\": []}"
+    );
+  }
+
+  @Test
+  public void write_actions_without_assign_to_me() throws Exception {
+    Issue issue = new DefaultIssue()
+      .setKey("ABCD")
+      .setComponentKey("sample:src/main/xoo/sample/Sample.xoo")
+      .setProjectKey("sample")
+      .setRuleKey(RuleKey.of("squid", "AvoidCycle"))
+      .setAssignee("john");
+
+    MockUserSession.set().setLogin("john");
+
+    testActions(issue,
+      "{\"actions\": " +
+        "[" +
+        "\"comment\", \"assign\", \"plan\"\n" +
+        "]}"
+    );
+  }
+
+  @Test
+  public void write_transitions() throws Exception {
+    Issue issue = new DefaultIssue()
+      .setKey("ABCD")
+      .setComponentKey("sample:src/main/xoo/sample/Sample.xoo")
+      .setProjectKey("sample")
+      .setRuleKey(RuleKey.of("squid", "AvoidCycle"));
+
+    when(issueService.listTransitions(eq(issue), any(UserSession.class))).thenReturn(newArrayList(Transition.create("reopen", "RESOLVED", "REOPEN")));
+    MockUserSession.set().setLogin("john");
+
+    testTransitions(issue,
+      "{\"transitions\": [\n" +
+        "        \"reopen\"\n" +
+        "      ]}"
+    );
+  }
+
+  @Test
+  public void write_no_transitions() throws Exception {
+    Issue issue = new DefaultIssue()
+      .setKey("ABCD")
+      .setComponentKey("sample:src/main/xoo/sample/Sample.xoo")
+      .setProjectKey("sample")
+      .setRuleKey(RuleKey.of("squid", "AvoidCycle"));
+
+    MockUserSession.set().setLogin("john");
+
+    testTransitions(issue,
+      "{\"transitions\": []}"
+    );
+  }
+
+  private void testActions(Issue issue, String expected) throws JSONException {
+    StringWriter output = new StringWriter();
+    JsonWriter jsonWriter = JsonWriter.of(output);
+    jsonWriter.beginObject();
+    writer.writeActions(issue, jsonWriter);
+    jsonWriter.endObject();
+    JSONAssert.assertEquals(output.toString(), expected, true);
+  }
+
+  private void testTransitions(Issue issue, String expected) throws JSONException {
+    StringWriter output = new StringWriter();
+    JsonWriter jsonWriter = JsonWriter.of(output);
+    jsonWriter.beginObject();
+    writer.writeTransitions(issue, jsonWriter);
+    jsonWriter.endObject();
+    JSONAssert.assertEquals(output.toString(), expected, true);
+  }
+
+}
diff --git a/sonar-server/src/test/java/org/sonar/server/issue/ws/IssueSearchActionTest.java b/sonar-server/src/test/java/org/sonar/server/issue/ws/IssueSearchActionTest.java
new file mode 100644 (file)
index 0000000..5eecc61
--- /dev/null
@@ -0,0 +1,377 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 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.ws;
+
+import com.google.common.collect.Lists;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.sonar.api.component.Component;
+import org.sonar.api.i18n.I18n;
+import org.sonar.api.issue.ActionPlan;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.issue.IssueFinder;
+import org.sonar.api.issue.IssueQuery;
+import org.sonar.api.issue.internal.DefaultIssue;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rules.Rule;
+import org.sonar.api.user.User;
+import org.sonar.api.utils.DateUtils;
+import org.sonar.api.utils.Duration;
+import org.sonar.api.utils.Durations;
+import org.sonar.api.utils.Paging;
+import org.sonar.core.component.ComponentDto;
+import org.sonar.core.issue.DefaultActionPlan;
+import org.sonar.core.issue.DefaultIssueQueryResult;
+import org.sonar.core.issue.workflow.Transition;
+import org.sonar.core.user.DefaultUser;
+import org.sonar.server.issue.ActionService;
+import org.sonar.server.issue.IssueService;
+import org.sonar.server.user.MockUserSession;
+import org.sonar.server.user.UserSession;
+import org.sonar.server.ws.WsTester;
+
+import java.util.*;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static com.google.common.collect.Maps.newHashMap;
+import static org.fest.assertions.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.*;
+
+@RunWith(MockitoJUnitRunner.class)
+public class IssueSearchActionTest {
+
+  @Mock
+  IssueFinder issueFinder;
+
+  @Mock
+  IssueService issueService;
+
+  @Mock
+  ActionService actionService;
+
+  @Mock
+  I18n i18n;
+
+  @Mock
+  Durations durations;
+
+  List<Issue> issues;
+  DefaultIssueQueryResult result;
+
+  Date issueCreationDate;
+
+  WsTester tester;
+
+  @Before
+  public void setUp() throws Exception {
+    issues = new ArrayList<Issue>();
+    result = new DefaultIssueQueryResult(issues);
+    when(issueFinder.find(any(IssueQuery.class))).thenReturn(result);
+
+    issueCreationDate = DateUtils.parseDateTime("2014-01-22T19:10:03+0100");
+    when(i18n.formatDateTime(any(Locale.class), eq(issueCreationDate))).thenReturn("Jan 22, 2014 10:03 AM");
+
+    result.setMaxResultsReached(true);
+    result.setPaging(Paging.create(100, 1, 2));
+    when(i18n.formatInteger(any(Locale.class), eq(2))).thenReturn("2");
+
+    tester = new WsTester(new IssuesWs(mock(IssueShowAction.class), new IssueSearchAction(issueFinder, new IssueActionsWriter(issueService, actionService), i18n, durations)));
+  }
+
+  @Test
+  public void issues() throws Exception {
+    String issueKey = "ABCD";
+    Issue issue = new DefaultIssue()
+      .setKey(issueKey)
+      .setComponentKey("sample:src/main/xoo/sample/Sample.xoo")
+      .setProjectKey("sample")
+      .setRuleKey(RuleKey.of("squid", "AvoidCycle"))
+      .setActionPlanKey("AP-ABCD")
+      .setLine(12)
+      .setEffortToFix(2.0)
+      .setMessage("Fix it")
+      .setResolution("FIXED")
+      .setStatus("CLOSED")
+      .setSeverity("MAJOR")
+      .setAssignee("john")
+      .setReporter("steven")
+      .setAuthorLogin("Henry")
+      .setCreationDate(issueCreationDate);
+    issues.add(issue);
+
+    WsTester.TestRequest request = tester.newGetRequest("api/issues", "search");
+    request.execute().assertJson(getClass(), "issues.json");
+  }
+
+  @Test
+  public void issues_with_components() throws Exception {
+    String issueKey = "ABCD";
+    Issue issue = new DefaultIssue()
+      .setKey(issueKey)
+      .setComponentKey("sample:src/main/xoo/sample/Sample.xoo")
+      .setProjectKey("sample")
+      .setRuleKey(RuleKey.of("squid", "AvoidCycle"))
+      .setActionPlanKey("AP-ABCD")
+      .setLine(12)
+      .setEffortToFix(2.0)
+      .setMessage("Fix it")
+      .setResolution("FIXED")
+      .setStatus("CLOSED")
+      .setSeverity("MAJOR")
+      .setAssignee("john")
+      .setReporter("steven")
+      .setAuthorLogin("Henry")
+      .setCreationDate(issueCreationDate);
+    issues.add(issue);
+
+    ComponentDto component = new ComponentDto()
+      .setId(10L)
+      .setKey("sample:src/main/xoo/sample/Sample.xoo")
+      .setLongName("src/main/xoo/sample/Sample.xoo")
+      .setName("Sample.xoo")
+      .setQualifier("FIL")
+      .setPath("src/main/xoo/sample/Sample.xoo")
+      .setSubProjectId(7L)
+      .setProjectId(7L);
+
+    ComponentDto project = new ComponentDto()
+      .setId(7L)
+      .setKey("sample")
+      .setLongName("Sample")
+      .setName("Sample")
+      .setQualifier("TRK")
+      .setProjectId(7L);
+
+    result.addComponents(Lists.<Component>newArrayList(component, project));
+    result.addProjects(Lists.<Component>newArrayList(project));
+
+    WsTester.TestRequest request = tester.newGetRequest("api/issues", "search");
+    request.execute().assertJson(getClass(), "issues_with_components.json");
+  }
+
+  @Test
+  public void issues_with_rules() throws Exception {
+    String issueKey = "ABCD";
+    Issue issue = new DefaultIssue()
+      .setKey(issueKey)
+      .setComponentKey("sample:src/main/xoo/sample/Sample.xoo")
+      .setProjectKey("sample")
+      .setRuleKey(RuleKey.of("squid", "AvoidCycle"))
+      .setActionPlanKey("AP-ABCD")
+      .setLine(12)
+      .setEffortToFix(2.0)
+      .setMessage("Fix it")
+      .setResolution("FIXED")
+      .setStatus("CLOSED")
+      .setSeverity("MAJOR")
+      .setAssignee("john")
+      .setReporter("steven")
+      .setAuthorLogin("Henry")
+      .setCreationDate(issueCreationDate);
+    issues.add(issue);
+
+    result.addRules(newArrayList(
+      Rule.create("squid", "AvoidCycle").setName("Avoid cycle").setDescription("Avoid cycle description")
+    ));
+
+    WsTester.TestRequest request = tester.newGetRequest("api/issues", "search");
+    request.execute().assertJson(getClass(), "issues_with_rules.json");
+  }
+
+  @Test
+  public void issues_with_users() throws Exception {
+    String issueKey = "ABCD";
+    Issue issue = new DefaultIssue()
+      .setKey(issueKey)
+      .setComponentKey("sample:src/main/xoo/sample/Sample.xoo")
+      .setProjectKey("sample")
+      .setRuleKey(RuleKey.of("squid", "AvoidCycle"))
+      .setActionPlanKey("AP-ABCD")
+      .setLine(12)
+      .setEffortToFix(2.0)
+      .setMessage("Fix it")
+      .setResolution("FIXED")
+      .setStatus("CLOSED")
+      .setSeverity("MAJOR")
+      .setAssignee("john")
+      .setReporter("steven")
+      .setAuthorLogin("Henry")
+      .setCreationDate(issueCreationDate);
+    issues.add(issue);
+
+    result.addUsers(Lists.<User>newArrayList(
+      new DefaultUser().setName("John").setLogin("john").setActive(true).setEmail("john@email.com")
+    ));
+
+    WsTester.TestRequest request = tester.newGetRequest("api/issues", "search");
+    request.execute().assertJson(getClass(), "issues_with_users.json");
+  }
+
+  @Test
+  public void issues_with_dates() throws Exception {
+    Date creationDate = DateUtils.parseDateTime("2014-01-22T19:10:03+0100");
+    Date updateDate = DateUtils.parseDateTime("2014-01-23T19:10:03+0100");
+    Date closedDate = DateUtils.parseDateTime("2014-01-24T19:10:03+0100");
+
+    Issue issue = createStandardIssue()
+      .setCreationDate(creationDate)
+      .setUpdateDate(updateDate)
+      .setCloseDate(closedDate);
+    issues.add(issue);
+
+    when(i18n.formatDateTime(any(Locale.class), eq(creationDate))).thenReturn("Jan 22, 2014 10:03 AM");
+    when(i18n.formatDateTime(any(Locale.class), eq(updateDate))).thenReturn("Jan 23, 2014 10:03 AM");
+    when(i18n.ageFromNow(any(Locale.class), eq(updateDate))).thenReturn("9 days");
+    when(i18n.formatDateTime(any(Locale.class), eq(closedDate))).thenReturn("Jan 24, 2014 10:03 AM");
+
+    WsTester.TestRequest request = tester.newGetRequest("api/issues", "search");
+    request.execute().assertJson(getClass(), "issues_with_dates.json");
+  }
+
+  @Test
+  public void issues_with_debt() throws Exception {
+    Duration debt = (Duration.create(7260L));
+    Issue issue = createStandardIssue().setDebt(debt);
+    issues.add(issue);
+
+    when(durations.format(any(Locale.class), eq(debt), eq(Durations.DurationFormat.SHORT))).thenReturn("2 hours 1 minutes");
+
+    WsTester.TestRequest request = tester.newGetRequest("api/issues", "search");
+    request.execute().assertJson(getClass(), "issues_with_debt.json");
+  }
+
+  @Test
+  public void issues_with_action_plan() throws Exception {
+    Issue issue = createStandardIssue()
+      .setActionPlanKey("AP-ABCD");
+    issues.add(issue);
+
+    MockUserSession.set();
+    WsTester.TestRequest request = tester.newGetRequest("api/issues", "search");
+    request.execute().assertJson(getClass(), "issues_with_action_plan.json");
+  }
+
+  @Test
+  public void issues_with_attributes() throws Exception {
+    Issue issue = createStandardIssue()
+      .setAttribute("jira-issue-key", "SONAR-1234");
+    issues.add(issue);
+
+    WsTester.TestRequest request = tester.newGetRequest("api/issues", "search");
+    request.execute().assertJson(getClass(), "issues_with_attributes.json");
+  }
+
+  @Test
+  public void issues_with_extra_fields() throws Exception {
+    Issue issue = createStandardIssue()
+      .setActionPlanKey("AP-ABCD")
+      .setAssignee("john");
+    issues.add(issue);
+
+    MockUserSession.set().setLogin("john");
+    when(issueService.listTransitions(eq(issue), any(UserSession.class))).thenReturn(newArrayList(Transition.create("reopen", "RESOLVED", "REOPEN")));
+
+    result.addActionPlans(newArrayList((ActionPlan) new DefaultActionPlan().setKey("AP-ABCD").setName("1.0")));
+
+    result.addUsers(Lists.<User>newArrayList(
+      new DefaultUser().setName("John").setLogin("john")
+    ));
+
+    WsTester.TestRequest request = tester.newGetRequest("api/issues", "search").setParam("extra_fields", "actions,transitions,assigneeName,actionPlanName");
+    request.execute().assertJson(getClass(), "issues_with_extra_fields.json");
+  }
+
+  @Test
+  public void verify_issue_query_parameters() throws Exception {
+    Map<String, String> map = newHashMap();
+    map.put("issues", "ABCDE1234");
+    map.put("severities", "MAJOR,MINOR");
+    map.put("statuses", "CLOSED");
+    map.put("resolutions", "FALSE-POSITIVE");
+    map.put("resolved", "true");
+    map.put("components", "org.apache");
+    map.put("componentRoots", "org.sonar");
+    map.put("reporters", "marilyn");
+    map.put("assignees", "joanna");
+    map.put("languages", "xoo");
+    map.put("assigned", "true");
+    map.put("planned", "true");
+    map.put("hideRules", "true");
+    map.put("createdAt", "2013-04-15T09:08:24+0200");
+    map.put("createdAfter", "2013-04-16T09:08:24+0200");
+    map.put("createdBefore", "2013-04-17T09:08:24+0200");
+    map.put("rules", "squid:AvoidCycle,findbugs:NullReference");
+    map.put("pageSize", "10");
+    map.put("pageIndex", "50");
+    map.put("sort", "CREATION_DATE");
+    map.put("asc", "true");
+
+    WsTester.TestRequest request = tester.newGetRequest("api/issues", "search").setParams(map);
+    request.execute();
+
+    ArgumentCaptor<IssueQuery> captor = ArgumentCaptor.forClass(IssueQuery.class);
+    verify(issueFinder).find(captor.capture());
+
+    IssueQuery query = captor.getValue();
+    assertThat(query.requiredRole()).isEqualTo("user");
+    assertThat(query.issueKeys()).containsOnly("ABCDE1234");
+    assertThat(query.severities()).containsOnly("MAJOR", "MINOR");
+    assertThat(query.statuses()).containsOnly("CLOSED");
+    assertThat(query.resolutions()).containsOnly("FALSE-POSITIVE");
+    assertThat(query.resolved()).isTrue();
+    assertThat(query.components()).containsOnly("org.apache");
+    assertThat(query.componentRoots()).containsOnly("org.sonar");
+    assertThat(query.reporters()).containsOnly("marilyn");
+    assertThat(query.assignees()).containsOnly("joanna");
+    assertThat(query.languages()).containsOnly("xoo");
+    assertThat(query.assigned()).isTrue();
+    assertThat(query.planned()).isTrue();
+    assertThat(query.hideRules()).isTrue();
+    assertThat(query.createdAt()).isEqualTo(DateUtils.parseDateTime("2013-04-15T09:08:24+0200"));
+    assertThat(query.createdAfter()).isEqualTo(DateUtils.parseDateTime("2013-04-16T09:08:24+0200"));
+    assertThat(query.createdBefore()).isEqualTo(DateUtils.parseDateTime("2013-04-17T09:08:24+0200"));
+    assertThat(query.rules()).containsOnly(RuleKey.of("squid", "AvoidCycle"), RuleKey.of("findbugs", "NullReference"));
+    assertThat(query.pageSize()).isEqualTo(10);
+    assertThat(query.pageIndex()).isEqualTo(50);
+    assertThat(query.sort()).isEqualTo("CREATION_DATE");
+    assertThat(query.asc()).isTrue();
+  }
+
+  private DefaultIssue createStandardIssue() {
+    return createIssue();
+  }
+
+  private DefaultIssue createIssue() {
+    return new DefaultIssue()
+      .setKey("ABCD")
+      .setComponentKey("sample:src/main/xoo/sample/Sample.xoo")
+      .setProjectKey("sample")
+      .setRuleKey(RuleKey.of("squid", "AvoidCycle"))
+      .setCreationDate(issueCreationDate);
+  }
+
+}
index fde5db793d79e66d31f6667c5e8513a1a86b229a..35a83cb0ce763155236c9fa3208504236e2005b3 100644 (file)
@@ -17,6 +17,7 @@
  * 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.ws;
 
 import com.google.common.collect.Lists;
@@ -31,7 +32,6 @@ import org.sonar.api.issue.ActionPlan;
 import org.sonar.api.issue.Issue;
 import org.sonar.api.issue.IssueFinder;
 import org.sonar.api.issue.IssueQuery;
-import org.sonar.api.issue.action.Action;
 import org.sonar.api.issue.internal.DefaultIssue;
 import org.sonar.api.issue.internal.DefaultIssueComment;
 import org.sonar.api.issue.internal.FieldDiffs;
@@ -42,7 +42,6 @@ import org.sonar.api.user.User;
 import org.sonar.api.utils.DateUtils;
 import org.sonar.api.utils.Duration;
 import org.sonar.api.utils.Durations;
-import org.sonar.api.web.UserRole;
 import org.sonar.core.component.ComponentDto;
 import org.sonar.core.issue.DefaultActionPlan;
 import org.sonar.core.issue.DefaultIssueQueryResult;
@@ -95,7 +94,7 @@ public class IssueShowActionTest {
   List<Issue> issues;
   DefaultIssueQueryResult result;
 
-  private Date issue_creation_date;
+  Date issueCreationDate;
 
   WsTester tester;
 
@@ -108,12 +107,13 @@ public class IssueShowActionTest {
 
     when(issueChangelogService.changelog(any(Issue.class))).thenReturn(mock(IssueChangelog.class));
 
-    issue_creation_date = DateUtils.parseDateTime("2014-01-22T19:10:03+0100");
-    when(i18n.formatDateTime(any(Locale.class), eq(issue_creation_date))).thenReturn("Jan 22, 2014 10:03 AM");
+    issueCreationDate = DateUtils.parseDateTime("2014-01-22T19:10:03+0100");
+    when(i18n.formatDateTime(any(Locale.class), eq(issueCreationDate))).thenReturn("Jan 22, 2014 10:03 AM");
 
     when(i18n.message(any(Locale.class), eq("created"), eq((String) null))).thenReturn("Created");
 
-    tester = new WsTester(new IssuesWs(new IssueShowAction(issueFinder, issueService, issueChangelogService, actionService, debtModel, i18n, durations)));
+    tester = new WsTester(new IssuesWs(new IssueShowAction(issueFinder, issueChangelogService, new IssueActionsWriter(issueService, actionService), debtModel, i18n, durations),
+      mock(IssueSearchAction.class)));
   }
 
   @Test
@@ -125,18 +125,18 @@ public class IssueShowActionTest {
       .setProjectKey("org.sonar.Sonar")
       .setRuleKey(RuleKey.of("squid", "AvoidCycle"))
       .setLine(12)
-      .setEffortToFix(2.0)
       .setMessage("Fix it")
       .setResolution("FIXED")
       .setStatus("CLOSED")
       .setSeverity("MAJOR")
-      .setCreationDate(issue_creation_date);
+      .setCreationDate(issueCreationDate);
     issues.add(issue);
 
     result.addComponents(Lists.<Component>newArrayList(new ComponentDto()
         .setId(10L)
         .setKey("org.sonar.server.issue.IssueClient")
         .setLongName("SonarQube :: Issue Client")
+        .setName("SonarQube :: Issue Client")
         .setQualifier("FIL")
         .setSubProjectId(1L)
         .setProjectId(1L)
@@ -146,6 +146,7 @@ public class IssueShowActionTest {
         .setId(1L)
         .setKey("org.sonar.Sonar")
         .setLongName("SonarQube")
+        .setName("SonarQube")
         .setProjectId(1L)
     ));
 
@@ -163,12 +164,11 @@ public class IssueShowActionTest {
       .setProjectKey("org.sonar.Sonar")
       .setRuleKey(RuleKey.of("squid", "AvoidCycle"))
       .setLine(12)
-      .setEffortToFix(2.0)
       .setMessage("Fix it")
       .setResolution("FIXED")
       .setStatus("CLOSED")
       .setSeverity("MAJOR")
-      .setCreationDate(issue_creation_date);
+      .setCreationDate(issueCreationDate);
     issues.add(issue);
 
     // File
@@ -214,7 +214,7 @@ public class IssueShowActionTest {
       .setResolution("FIXED")
       .setStatus("CLOSED")
       .setSeverity("MAJOR")
-      .setCreationDate(issue_creation_date);
+      .setCreationDate(issueCreationDate);
     issues.add(issue);
 
     // File
@@ -444,59 +444,6 @@ public class IssueShowActionTest {
     request.execute().assertJson(getClass(), "show_issue_with_actions.json");
   }
 
-  @Test
-  public void show_issue_with_set_severity_action() throws Exception {
-    DefaultIssue issue = createStandardIssue()
-      .setStatus("OPEN");
-    issues.add(issue);
-
-    MockUserSession.set().setLogin("john").addProjectPermissions(UserRole.ISSUE_ADMIN, issue.projectKey());
-    WsTester.TestRequest request = tester.newGetRequest("api/issues", "show").setParam("key", issue.key());
-    request.execute().assertJson(getClass(), "show_issue_with_set_severity_action.json");
-  }
-
-  @Test
-  public void show_issue_with_assign_to_me_action() throws Exception {
-    DefaultIssue issue = createStandardIssue()
-      .setStatus("OPEN");
-    issues.add(issue);
-
-    MockUserSession.set().setLogin("john");
-    WsTester.TestRequest request = tester.newGetRequest("api/issues", "show").setParam("key", issue.key());
-    request.execute().assertJson(getClass(), "show_issue_with_assign_to_me_action.json");
-  }
-
-  @Test
-  public void show_issue_without_assign_to_me_action() throws Exception {
-    DefaultIssue issue = createStandardIssue()
-      .setStatus("OPEN")
-      .setAssignee("john");
-    issues.add(issue);
-
-    result.addUsers(Lists.<User>newArrayList(
-      new DefaultUser().setLogin("john").setName("John")
-    ));
-
-    MockUserSession.set().setLogin("john");
-    WsTester.TestRequest request = tester.newGetRequest("api/issues", "show").setParam("key", issue.key());
-    request.execute().assertJson(getClass(), "show_issue_without_assign_to_me_action.json");
-  }
-
-  @Test
-  public void show_issue_with_actions_defined_by_plugins() throws Exception {
-    Issue issue = createStandardIssue()
-      .setStatus("OPEN");
-    issues.add(issue);
-
-    Action action = mock(Action.class);
-    when(action.key()).thenReturn("link-to-jira");
-    when(actionService.listAvailableActions(issue)).thenReturn(newArrayList(action));
-
-    MockUserSession.set().setLogin("john");
-    WsTester.TestRequest request = tester.newGetRequest("api/issues", "show").setParam("key", issue.key());
-    request.execute().assertJson(getClass(), "show_issue_with_actions_defined_by_plugins.json");
-  }
-
   @Test
   public void show_issue_with_changelog() throws Exception {
     Issue issue = createStandardIssue();
@@ -538,7 +485,7 @@ public class IssueShowActionTest {
       .setComponentKey("org.sonar.server.issue.IssueClient")
       .setProjectKey("org.sonar.Sonar")
       .setRuleKey(RuleKey.of("squid", "AvoidCycle"))
-      .setCreationDate(issue_creation_date);
+      .setCreationDate(issueCreationDate);
   }
 
   private void addComponentAndProject() {
index 30d6babddc3a9ef7a505f82b0f0dbd82315ea44e..c425e7328eb22c07f2ba4b623caf0afe59faf8e8 100644 (file)
@@ -27,9 +27,7 @@ import org.sonar.api.server.ws.RailsHandler;
 import org.sonar.api.server.ws.WebService;
 import org.sonar.api.utils.Durations;
 import org.sonar.server.debt.DebtModelService;
-import org.sonar.server.issue.ActionService;
 import org.sonar.server.issue.IssueChangelogService;
-import org.sonar.server.issue.IssueService;
 import org.sonar.server.ws.WsTester;
 
 import static org.fest.assertions.Assertions.assertThat;
@@ -39,13 +37,22 @@ public class IssuesWsTest {
 
   IssueShowAction showAction;
 
+  IssueSearchAction searchAction;
+
   WsTester tester;
 
   @Before
   public void setUp() throws Exception {
-    showAction = new IssueShowAction(mock(IssueFinder.class), mock(IssueService.class), mock(IssueChangelogService.class), mock(ActionService.class),
-      mock(DebtModelService.class), mock(I18n.class), mock(Durations.class));
-    tester = new WsTester(new IssuesWs(showAction));
+    IssueFinder issueFinder = mock(IssueFinder.class);
+    IssueChangelogService issueChangelogService = mock(IssueChangelogService.class);
+    IssueActionsWriter actionsWriter = mock(IssueActionsWriter.class);
+    DebtModelService debtModelService = mock(DebtModelService.class);
+    I18n i18n = mock(I18n.class);
+    Durations durations = mock(Durations.class);
+
+    showAction = new IssueShowAction(issueFinder, issueChangelogService, actionsWriter, debtModelService, i18n, durations);
+    searchAction = new IssueSearchAction(issueFinder, actionsWriter, i18n, durations);
+    tester = new WsTester(new IssuesWs(showAction, searchAction));
   }
 
   @Test
@@ -87,9 +94,9 @@ public class IssuesWsTest {
     assertThat(show.since()).isEqualTo("3.6");
     assertThat(show.isPost()).isFalse();
     assertThat(show.isInternal()).isFalse();
-    assertThat(show.handler()).isInstanceOf(RailsHandler.class);
+    assertThat(show.handler()).isSameAs(searchAction);
     assertThat(show.responseExampleAsString()).isNotEmpty();
-    assertThat(show.params()).hasSize(19);
+    assertThat(show.params()).hasSize(23);
   }
 
   @Test
diff --git a/sonar-server/src/test/resources/org/sonar/server/issue/ws/IssueSearchActionTest/issues.json b/sonar-server/src/test/resources/org/sonar/server/issue/ws/IssueSearchActionTest/issues.json
new file mode 100644 (file)
index 0000000..8d14e1c
--- /dev/null
@@ -0,0 +1,32 @@
+{
+  "maxResultsReached": true,
+  "paging": {
+    "pageIndex": 1,
+    "pageSize": 100,
+    "total": 2,
+    "fTotal": "2",
+    "pages": 1
+  },
+  "issues": [
+    {
+      "key": "ABCD",
+      "component": "sample:src/main/xoo/sample/Sample.xoo",
+      "project": "sample",
+      "rule": "squid:AvoidCycle",
+      "resolution": "FIXED",
+      "status": "CLOSED",
+      "severity": "MAJOR",
+      "message": "Fix it",
+      "line": 12,
+      "reporter": "steven",
+      "assignee": "john",
+      "author": "Henry",
+      "actionPlan": "AP-ABCD",
+      "creationDate": "2014-01-22T19:10:03+0100"
+    }
+  ],
+  "components": [],
+  "projects": [],
+  "rules": [],
+  "users": []
+}
diff --git a/sonar-server/src/test/resources/org/sonar/server/issue/ws/IssueSearchActionTest/issues_with_action_plan.json b/sonar-server/src/test/resources/org/sonar/server/issue/ws/IssueSearchActionTest/issues_with_action_plan.json
new file mode 100644 (file)
index 0000000..6804fb0
--- /dev/null
@@ -0,0 +1,24 @@
+{
+  "maxResultsReached": true,
+  "paging": {
+    "pageIndex": 1,
+    "pageSize": 100,
+    "total": 2,
+    "fTotal": "2",
+    "pages": 1
+  },
+  "issues": [
+    {
+      "key": "ABCD",
+      "component": "sample:src/main/xoo/sample/Sample.xoo",
+      "project": "sample",
+      "rule": "squid:AvoidCycle",
+      "actionPlan" : "AP-ABCD",
+      "creationDate": "2014-01-22T19:10:03+0100"
+    }
+  ],
+  "components": [],
+  "projects": [],
+  "rules": [],
+  "users": []
+}
diff --git a/sonar-server/src/test/resources/org/sonar/server/issue/ws/IssueSearchActionTest/issues_with_attributes.json b/sonar-server/src/test/resources/org/sonar/server/issue/ws/IssueSearchActionTest/issues_with_attributes.json
new file mode 100644 (file)
index 0000000..9d03dcd
--- /dev/null
@@ -0,0 +1,26 @@
+{
+  "maxResultsReached": true,
+  "paging": {
+    "pageIndex": 1,
+    "pageSize": 100,
+    "total": 2,
+    "fTotal": "2",
+    "pages": 1
+  },
+  "issues": [
+    {
+      "key": "ABCD",
+      "component": "sample:src/main/xoo/sample/Sample.xoo",
+      "project": "sample",
+      "rule": "squid:AvoidCycle",
+      "creationDate": "2014-01-22T19:10:03+0100",
+      "attr": {
+        "jira-issue-key": "SONAR-1234"
+      }
+    }
+  ],
+  "components": [],
+  "projects": [],
+  "rules": [],
+  "users": []
+}
diff --git a/sonar-server/src/test/resources/org/sonar/server/issue/ws/IssueSearchActionTest/issues_with_components.json b/sonar-server/src/test/resources/org/sonar/server/issue/ws/IssueSearchActionTest/issues_with_components.json
new file mode 100644 (file)
index 0000000..7799166
--- /dev/null
@@ -0,0 +1,58 @@
+{
+  "maxResultsReached": true,
+  "paging": {
+    "pageIndex": 1,
+    "pageSize": 100,
+    "total": 2,
+    "fTotal": "2",
+    "pages": 1
+  },
+  "issues": [
+    {
+      "key": "ABCD",
+      "component": "sample:src/main/xoo/sample/Sample.xoo",
+      "project": "sample",
+      "rule": "squid:AvoidCycle",
+      "resolution": "FIXED",
+      "status": "CLOSED",
+      "severity": "MAJOR",
+      "message": "Fix it",
+      "line": 12,
+      "reporter": "steven",
+      "assignee": "john",
+      "author": "Henry",
+      "actionPlan": "AP-ABCD",
+      "creationDate": "2014-01-22T19:10:03+0100"
+    }
+  ],
+  "components": [
+    {
+      "key": "sample:src/main/xoo/sample/Sample.xoo",
+      "id": 10,
+      "qualifier": "FIL",
+      "name": "Sample.xoo",
+      "longName": "src/main/xoo/sample/Sample.xoo",
+      "path": "src/main/xoo/sample/Sample.xoo",
+      "projectId": 7,
+      "subProjectId": 7
+    },
+    {
+      "key": "sample",
+      "id": 7,
+      "qualifier": "TRK",
+      "name": "Sample",
+      "longName": "Sample"
+    }
+  ],
+  "projects": [
+    {
+      "key": "sample",
+      "id": 7,
+      "qualifier": "TRK",
+      "name": "Sample",
+      "longName": "Sample"
+    }
+  ],
+  "rules": [],
+  "users": []
+}
diff --git a/sonar-server/src/test/resources/org/sonar/server/issue/ws/IssueSearchActionTest/issues_with_dates.json b/sonar-server/src/test/resources/org/sonar/server/issue/ws/IssueSearchActionTest/issues_with_dates.json
new file mode 100644 (file)
index 0000000..814789b
--- /dev/null
@@ -0,0 +1,26 @@
+{
+  "maxResultsReached": true,
+  "paging": {
+    "pageIndex": 1,
+    "pageSize": 100,
+    "total": 2,
+    "fTotal": "2",
+    "pages": 1
+  },
+  "issues": [
+    {
+      "key": "ABCD",
+      "component": "sample:src/main/xoo/sample/Sample.xoo",
+      "project": "sample",
+      "rule": "squid:AvoidCycle",
+      "creationDate": "2014-01-22T19:10:03+0100",
+      "updateDate": "2014-01-23T19:10:03+0100",
+      "fUpdateAge": "9 days",
+      "closeDate": "2014-01-24T19:10:03+0100"
+    }
+  ],
+  "components": [],
+  "projects": [],
+  "rules": [],
+  "users": []
+}
diff --git a/sonar-server/src/test/resources/org/sonar/server/issue/ws/IssueSearchActionTest/issues_with_debt.json b/sonar-server/src/test/resources/org/sonar/server/issue/ws/IssueSearchActionTest/issues_with_debt.json
new file mode 100644 (file)
index 0000000..e5054e6
--- /dev/null
@@ -0,0 +1,24 @@
+{
+  "maxResultsReached": true,
+  "paging": {
+    "pageIndex": 1,
+    "pageSize": 100,
+    "total": 2,
+    "fTotal": "2",
+    "pages": 1
+  },
+  "issues": [
+    {
+      "key": "ABCD",
+      "component": "sample:src/main/xoo/sample/Sample.xoo",
+      "project": "sample",
+      "rule": "squid:AvoidCycle",
+      "debt": "2 hours 1 minutes",
+      "creationDate": "2014-01-22T19:10:03+0100"
+    }
+  ],
+  "components": [],
+  "projects": [],
+  "rules": [],
+  "users": []
+}
diff --git a/sonar-server/src/test/resources/org/sonar/server/issue/ws/IssueSearchActionTest/issues_with_extra_fields.json b/sonar-server/src/test/resources/org/sonar/server/issue/ws/IssueSearchActionTest/issues_with_extra_fields.json
new file mode 100644 (file)
index 0000000..399a702
--- /dev/null
@@ -0,0 +1,39 @@
+{
+  "maxResultsReached": true,
+  "paging": {
+    "pageIndex": 1,
+    "pageSize": 100,
+    "total": 2,
+    "fTotal": "2",
+    "pages": 1
+  },
+  "issues": [
+    {
+      "key": "ABCD",
+      "component": "sample:src/main/xoo/sample/Sample.xoo",
+      "project": "sample",
+      "rule": "squid:AvoidCycle",
+      "creationDate": "2014-01-22T19:10:03+0100",
+      "assignee": "john",
+      "assigneeName" : "John",
+      "actionPlan" : "AP-ABCD",
+      "actionPlanName" : "1.0",
+      "actions": [
+        "comment", "assign", "plan"
+      ],
+      "transitions": [
+        "reopen"
+      ]
+    }
+  ],
+  "components": [],
+  "projects": [],
+  "rules": [],
+  "users": [
+    {
+      "login": "john",
+      "name": "John",
+      "active": false
+    }
+  ]
+}
diff --git a/sonar-server/src/test/resources/org/sonar/server/issue/ws/IssueSearchActionTest/issues_with_rules.json b/sonar-server/src/test/resources/org/sonar/server/issue/ws/IssueSearchActionTest/issues_with_rules.json
new file mode 100644 (file)
index 0000000..58568a5
--- /dev/null
@@ -0,0 +1,39 @@
+{
+  "maxResultsReached": true,
+  "paging": {
+    "pageIndex": 1,
+    "pageSize": 100,
+    "total": 2,
+    "fTotal": "2",
+    "pages": 1
+  },
+  "issues": [
+    {
+      "key": "ABCD",
+      "component": "sample:src/main/xoo/sample/Sample.xoo",
+      "project": "sample",
+      "rule": "squid:AvoidCycle",
+      "resolution": "FIXED",
+      "status": "CLOSED",
+      "severity": "MAJOR",
+      "message": "Fix it",
+      "line": 12,
+      "reporter": "steven",
+      "assignee": "john",
+      "author": "Henry",
+      "actionPlan": "AP-ABCD",
+      "creationDate": "2014-01-22T19:10:03+0100"
+    }
+  ],
+  "components": [],
+  "projects": [],
+  "rules": [
+    {
+      "key": "squid:AvoidCycle",
+      "name": "Avoid cycle",
+      "desc": "Avoid cycle description",
+      "status": "READY"
+    }
+  ],
+  "users": []
+}
diff --git a/sonar-server/src/test/resources/org/sonar/server/issue/ws/IssueSearchActionTest/issues_with_users.json b/sonar-server/src/test/resources/org/sonar/server/issue/ws/IssueSearchActionTest/issues_with_users.json
new file mode 100644 (file)
index 0000000..2d8c3ac
--- /dev/null
@@ -0,0 +1,39 @@
+{
+  "maxResultsReached": true,
+  "paging": {
+    "pageIndex": 1,
+    "pageSize": 100,
+    "total": 2,
+    "fTotal": "2",
+    "pages": 1
+  },
+  "issues": [
+    {
+      "key": "ABCD",
+      "component": "sample:src/main/xoo/sample/Sample.xoo",
+      "project": "sample",
+      "rule": "squid:AvoidCycle",
+      "resolution": "FIXED",
+      "status": "CLOSED",
+      "severity": "MAJOR",
+      "message": "Fix it",
+      "line": 12,
+      "reporter": "steven",
+      "assignee": "john",
+      "author": "Henry",
+      "actionPlan": "AP-ABCD",
+      "creationDate": "2014-01-22T19:10:03+0100"
+    }
+  ],
+  "components": [],
+  "projects": [],
+  "rules": [],
+  "users": [
+    {
+      "login": "john",
+      "name": "John",
+      "active": true,
+      "email": "john@email.com"
+    }
+  ]
+}
diff --git a/sonar-server/src/test/resources/org/sonar/server/issue/ws/IssueShowActionTest/show_issue_with_actions_defined_by_plugins.json b/sonar-server/src/test/resources/org/sonar/server/issue/ws/IssueShowActionTest/show_issue_with_actions_defined_by_plugins.json
deleted file mode 100644 (file)
index 9f83417..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-{
-  "issue": {
-    "key": "ABCD",
-    "component": "org.sonar.server.issue.IssueClient",
-    "componentLongName": "SonarQube :: Issue Client",
-    "componentQualifier": "FIL",
-    "project": "org.sonar.Sonar",
-    "projectName": "SonarQube",
-    "rule": "squid:AvoidCycle",
-    "ruleName": "Avoid cycle",
-    "status": "OPEN",
-    "creationDate": "2014-01-22T19:10:03+0100",
-    "fCreationDate": "Jan 22, 2014 10:03 AM",
-    "transitions": [],
-    "actions": [
-       "comment", "assign", "assign_to_me", "plan", "link-to-jira"
-    ],
-    "comments": [],
-    "changelog": [
-      {
-        "creationDate": "2014-01-22T19:10:03+0100",
-        "fCreationDate": "Jan 22, 2014 10:03 AM",
-        "diffs": ["Created"]
-      }
-    ]
-  }
-}
diff --git a/sonar-server/src/test/resources/org/sonar/server/issue/ws/IssueShowActionTest/show_issue_with_assign_to_me_action.json b/sonar-server/src/test/resources/org/sonar/server/issue/ws/IssueShowActionTest/show_issue_with_assign_to_me_action.json
deleted file mode 100644 (file)
index eb7f97f..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-{
-  "issue": {
-    "key": "ABCD",
-    "component": "org.sonar.server.issue.IssueClient",
-    "componentLongName": "SonarQube :: Issue Client",
-    "componentQualifier": "FIL",
-    "project": "org.sonar.Sonar",
-    "projectName": "SonarQube",
-    "rule": "squid:AvoidCycle",
-    "ruleName": "Avoid cycle",
-    "status": "OPEN",
-    "creationDate": "2014-01-22T19:10:03+0100",
-    "fCreationDate": "Jan 22, 2014 10:03 AM",
-    "transitions": [],
-    "actions": [
-       "comment", "assign", "assign_to_me", "plan"
-    ],
-    "comments": [],
-    "changelog": [
-      {
-        "creationDate": "2014-01-22T19:10:03+0100",
-        "fCreationDate": "Jan 22, 2014 10:03 AM",
-        "diffs": ["Created"]
-      }
-    ]
-  }
-}
diff --git a/sonar-server/src/test/resources/org/sonar/server/issue/ws/IssueShowActionTest/show_issue_with_set_severity_action.json b/sonar-server/src/test/resources/org/sonar/server/issue/ws/IssueShowActionTest/show_issue_with_set_severity_action.json
deleted file mode 100644 (file)
index 242d99c..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-{
-  "issue": {
-    "key": "ABCD",
-    "component": "org.sonar.server.issue.IssueClient",
-    "componentLongName": "SonarQube :: Issue Client",
-    "componentQualifier": "FIL",
-    "project": "org.sonar.Sonar",
-    "projectName": "SonarQube",
-    "rule": "squid:AvoidCycle",
-    "ruleName": "Avoid cycle",
-    "status": "OPEN",
-    "creationDate": "2014-01-22T19:10:03+0100",
-    "fCreationDate": "Jan 22, 2014 10:03 AM",
-    "transitions": [],
-    "actions": [
-       "comment", "assign", "assign_to_me", "plan", "set_severity"
-    ],
-    "comments": [],
-    "changelog": [
-      {
-        "creationDate": "2014-01-22T19:10:03+0100",
-        "fCreationDate": "Jan 22, 2014 10:03 AM",
-        "diffs": ["Created"]
-      }
-    ]
-  }
-}
diff --git a/sonar-server/src/test/resources/org/sonar/server/issue/ws/IssueShowActionTest/show_issue_without_assign_to_me_action.json b/sonar-server/src/test/resources/org/sonar/server/issue/ws/IssueShowActionTest/show_issue_without_assign_to_me_action.json
deleted file mode 100644 (file)
index ffde399..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-{
-  "issue": {
-    "key": "ABCD",
-    "component": "org.sonar.server.issue.IssueClient",
-    "componentLongName": "SonarQube :: Issue Client",
-    "componentQualifier": "FIL",
-    "project": "org.sonar.Sonar",
-    "projectName": "SonarQube",
-    "rule": "squid:AvoidCycle",
-    "ruleName": "Avoid cycle",
-    "assignee": "john",
-    "assigneeName": "John",
-    "status": "OPEN",
-    "creationDate": "2014-01-22T19:10:03+0100",
-    "fCreationDate": "Jan 22, 2014 10:03 AM",
-    "transitions": [],
-    "actions": [
-       "comment", "assign", "plan"
-    ],
-    "comments": [],
-    "changelog": [
-      {
-        "creationDate": "2014-01-22T19:10:03+0100",
-        "fCreationDate": "Jan 22, 2014 10:03 AM",
-        "diffs": ["Created"]
-      }
-    ]
-  }
-}