aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJean-Baptiste Lievremont <jean-baptiste.lievremont@sonarsource.com>2015-06-08 11:46:47 +0200
committerJean-Baptiste Lievremont <jean-baptiste.lievremont@sonarsource.com>2015-06-18 09:34:34 +0200
commit5df799013fe35f29ae369e8ae35a7bbc8ba07218 (patch)
tree6dc31b42ab7c75bab42d7e1de43e080c1f7cb2de
parente183ec7123713007b84cfae85b79645ab6337acf (diff)
downloadsonarqube-5df799013fe35f29ae369e8ae35a7bbc8ba07218.tar.gz
sonarqube-5df799013fe35f29ae369e8ae35a7bbc8ba07218.zip
SONAR-6582 Extract issue serialization class
Use common issue JSON representation for most actions on issues.
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/issue/InternalRubyIssueService.java92
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueComponentHelper.java96
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueJsonWriter.java213
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchAction.java230
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java6
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/issue/InternalRubyIssueServiceTest.java31
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionMediumTest.java2
-rw-r--r--server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionMediumTest/issue_with_extra_fields.json1
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/controllers/api/issues_controller.rb2
9 files changed, 448 insertions, 225 deletions
diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/InternalRubyIssueService.java b/server/sonar-server/src/main/java/org/sonar/server/issue/InternalRubyIssueService.java
index cdb89954f03..0011cb1a228 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/issue/InternalRubyIssueService.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/issue/InternalRubyIssueService.java
@@ -19,19 +19,30 @@
*/
package org.sonar.server.issue;
+import org.sonar.server.issue.ws.IssueComponentHelper;
+import org.sonar.server.issue.ws.IssueJsonWriter;
+
+import org.elasticsearch.common.collect.Lists;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import java.io.StringWriter;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
+import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.sonar.api.issue.ActionPlan;
import org.sonar.api.issue.Issue;
@@ -42,15 +53,21 @@ import org.sonar.api.issue.internal.DefaultIssueComment;
import org.sonar.api.issue.internal.FieldDiffs;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.server.ServerSide;
+import org.sonar.api.user.User;
import org.sonar.api.utils.SonarException;
+import org.sonar.api.utils.text.JsonWriter;
import org.sonar.api.web.UserRole;
+import org.sonar.core.component.ComponentDto;
import org.sonar.core.issue.ActionPlanStats;
import org.sonar.core.issue.DefaultActionPlan;
import org.sonar.core.issue.db.IssueFilterDto;
import org.sonar.core.issue.workflow.Transition;
+import org.sonar.core.persistence.DbSession;
+import org.sonar.core.persistence.MyBatis;
import org.sonar.core.resource.ResourceDao;
import org.sonar.core.resource.ResourceDto;
import org.sonar.core.resource.ResourceQuery;
+import org.sonar.server.db.DbClient;
import org.sonar.server.es.SearchOptions;
import org.sonar.server.exceptions.BadRequestException;
import org.sonar.server.issue.actionplan.ActionPlanService;
@@ -58,10 +75,11 @@ import org.sonar.server.issue.filter.IssueFilterParameters;
import org.sonar.server.issue.filter.IssueFilterService;
import org.sonar.server.search.QueryContext;
import org.sonar.server.user.UserSession;
+import org.sonar.server.user.index.UserIndex;
import org.sonar.server.util.RubyUtils;
import org.sonar.server.util.Validation;
-
import static com.google.common.collect.Lists.newArrayList;
+import static com.google.common.collect.Maps.newHashMap;
/**
* Used through ruby code <pre>Internal.issues</pre>
@@ -90,6 +108,10 @@ public class InternalRubyIssueService {
private final ActionService actionService;
private final IssueFilterService issueFilterService;
private final IssueBulkChangeService issueBulkChangeService;
+ private final IssueJsonWriter issueWriter;
+ private final IssueComponentHelper issueComponentHelper;
+ private final UserIndex userIndex;
+ private final DbClient dbClient;
private final UserSession userSession;
public InternalRubyIssueService(
@@ -99,6 +121,7 @@ public class InternalRubyIssueService {
IssueChangelogService changelogService, ActionPlanService actionPlanService,
ResourceDao resourceDao, ActionService actionService,
IssueFilterService issueFilterService, IssueBulkChangeService issueBulkChangeService,
+ IssueJsonWriter issueWriter, IssueComponentHelper issueComponentHelper, UserIndex userIndex, DbClient dbClient,
UserSession userSession) {
this.issueService = issueService;
this.issueQueryService = issueQueryService;
@@ -109,6 +132,10 @@ public class InternalRubyIssueService {
this.actionService = actionService;
this.issueFilterService = issueFilterService;
this.issueBulkChangeService = issueBulkChangeService;
+ this.issueWriter = issueWriter;
+ this.issueComponentHelper = issueComponentHelper;
+ this.userIndex = userIndex;
+ this.dbClient = dbClient;
this.userSession = userSession;
}
@@ -667,4 +694,67 @@ public class InternalRubyIssueService {
public boolean isUserIssueAdmin(String projectUuid) {
return userSession.hasProjectPermissionByUuid(UserRole.ISSUE_ADMIN, projectUuid);
}
+
+ /**
+ * Used by issue modification actions currently implemented in Rails
+ * @param issue
+ * @return the JSON representation of the modified issue, as a ready to use string
+ */
+ public String writeIssueJson(Issue issue) {
+ StringWriter writer = new StringWriter();
+ JsonWriter json = JsonWriter.of(writer);
+ DbSession dbSession = dbClient.openSession(false);
+ try {
+ Map<String, User> usersByLogin = getIssueUsersByLogin(issue);
+
+ Set<String> componentUuids = ImmutableSet.of(issue.componentUuid());
+ Set<String> projectUuids = Sets.newHashSet();
+ Set<ComponentDto> componentDtos = Sets.newHashSet();
+ List<ComponentDto> projectDtos = Lists.newArrayList();
+
+ Map<String, ComponentDto> componentsByUuid = Maps.newHashMap();
+ Map<String, ComponentDto> projectsByComponentUuid = Maps.newHashMap();
+
+ List<ComponentDto> fileDtos = dbClient.componentDao().selectByUuids(dbSession, componentUuids);
+ List<ComponentDto> subProjectDtos = dbClient.componentDao().selectSubProjectsByComponentUuids(dbSession, componentUuids);
+ componentDtos.addAll(fileDtos);
+ componentDtos.addAll(subProjectDtos);
+ for (ComponentDto component : componentDtos) {
+ projectUuids.add(component.projectUuid());
+ }
+ projectDtos.addAll(dbClient.componentDao().selectByUuids(dbSession, projectUuids));
+ componentDtos.addAll(projectDtos);
+
+ for (ComponentDto componentDto : componentDtos) {
+ componentsByUuid.put(componentDto.uuid(), componentDto);
+ }
+
+ projectsByComponentUuid = issueComponentHelper.prepareComponentsAndProjects(projectUuids, componentUuids, componentsByUuid, componentDtos, subProjectDtos, dbSession);
+
+ json.beginObject().name("issue");
+ issueWriter.write(json, issue,
+ usersByLogin,
+ componentsByUuid,
+ projectsByComponentUuid,
+ ImmutableMultimap.<String, DefaultIssueComment>of(),
+ ImmutableMap.<String, ActionPlan>of(),
+ ImmutableList.of(IssueJsonWriter.ACTIONS_EXTRA_FIELD, IssueJsonWriter.TRANSITIONS_EXTRA_FIELD));
+ json.endObject().close();
+ } finally {
+ MyBatis.closeQuietly(dbSession);
+ IOUtils.closeQuietly(writer);
+ }
+ return writer.toString();
+ }
+
+ private Map<String, User> getIssueUsersByLogin(Issue issue) {
+ Map<String, User> usersByLogin = Maps.newHashMap();
+ if (issue.assignee() != null) {
+ usersByLogin.put(issue.assignee(), userIndex.getByLogin(issue.assignee()));
+ }
+ if (issue.reporter() != null) {
+ usersByLogin.put(issue.reporter(), userIndex.getByLogin(issue.reporter()));
+ }
+ return usersByLogin;
+ }
}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueComponentHelper.java b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueComponentHelper.java
new file mode 100644
index 00000000000..2d9d2ae6b5a
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueComponentHelper.java
@@ -0,0 +1,96 @@
+/*
+ * 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 static com.google.common.collect.Maps.newHashMap;
+
+import org.sonar.server.db.DbClient;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.sonar.core.component.ComponentDto;
+import org.sonar.core.persistence.DbSession;
+
+/**
+ * This class computes some collections of {@link ComponentDto}s used to serialize issues.
+ */
+public class IssueComponentHelper {
+
+ private final DbClient dbClient;
+
+ public IssueComponentHelper(DbClient dbClient) {
+ this.dbClient = dbClient;
+ }
+
+ public Map<String, ComponentDto> prepareComponentsAndProjects(Set<String> projectUuids, Set<String> componentUuids, Map<String, ComponentDto> componentsByUuid,
+ Collection<ComponentDto> componentDtos, List<ComponentDto> projectDtos, DbSession session) {
+ Map<String, ComponentDto> projectsByComponentUuid;
+ List<ComponentDto> fileDtos = dbClient.componentDao().selectByUuids(session, componentUuids);
+ List<ComponentDto> subProjectDtos = dbClient.componentDao().selectSubProjectsByComponentUuids(session, componentUuids);
+ componentDtos.addAll(fileDtos);
+ componentDtos.addAll(subProjectDtos);
+ for (ComponentDto component : componentDtos) {
+ projectUuids.add(component.projectUuid());
+ }
+ projectDtos.addAll(dbClient.componentDao().selectByUuids(session, projectUuids));
+ componentDtos.addAll(projectDtos);
+
+ for (ComponentDto componentDto : componentDtos) {
+ componentsByUuid.put(componentDto.uuid(), componentDto);
+ }
+
+ projectsByComponentUuid = getProjectsByComponentUuid(componentDtos, projectDtos);
+ return projectsByComponentUuid;
+ }
+
+ private Map<String, ComponentDto> getProjectsByComponentUuid(Collection<ComponentDto> components, Collection<ComponentDto> projects) {
+ Map<String, ComponentDto> projectsByUuid = buildProjectsByUuid(projects);
+ return buildProjectsByComponentUuid(components, projectsByUuid);
+ }
+
+ private static Map<String, ComponentDto> buildProjectsByUuid(Collection<ComponentDto> projects) {
+ Map<String, ComponentDto> projectsByUuid = newHashMap();
+ for (ComponentDto project : projects) {
+ if (project == null) {
+ throw new IllegalStateException("Found a null project in issues");
+ }
+ if (project.uuid() == null) {
+ throw new IllegalStateException("Project has no UUID: " + project.getKey());
+ }
+ projectsByUuid.put(project.uuid(), project);
+ }
+ return projectsByUuid;
+ }
+
+ private static Map<String, ComponentDto> buildProjectsByComponentUuid(Collection<ComponentDto> components, Map<String, ComponentDto> projectsByUuid) {
+ Map<String, ComponentDto> projectsByComponentUuid = newHashMap();
+ for (ComponentDto component : components) {
+ if (component.uuid() == null) {
+ throw new IllegalStateException("Component has no UUID: " + component.getKey());
+ }
+ if (!projectsByUuid.containsKey(component.projectUuid())) {
+ throw new IllegalStateException("Project cannot be found for component: " + component.getKey() + " / " + component.uuid());
+ }
+ projectsByComponentUuid.put(component.uuid(), projectsByUuid.get(component.projectUuid()));
+ }
+ return projectsByComponentUuid;
+ }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueJsonWriter.java b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueJsonWriter.java
new file mode 100644
index 00000000000..fac3c5e367e
--- /dev/null
+++ b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueJsonWriter.java
@@ -0,0 +1,213 @@
+/*
+ * 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.server.user.ws.UserJsonWriter;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Multimap;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.sonar.api.i18n.I18n;
+import org.sonar.api.issue.ActionPlan;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.issue.IssueComment;
+import org.sonar.api.issue.internal.DefaultIssueComment;
+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.core.component.ComponentDto;
+import org.sonar.markdown.Markdown;
+import org.sonar.server.user.UserSession;
+
+public class IssueJsonWriter {
+
+ public static final String ACTIONS_EXTRA_FIELD = "actions";
+ public static final String TRANSITIONS_EXTRA_FIELD = "transitions";
+ public static final String REPORTER_NAME_EXTRA_FIELD = "reporterName";
+ public static final String ACTION_PLAN_NAME_EXTRA_FIELD = "actionPlanName";
+
+ public static final Set<String> EXTRA_FIELDS = ImmutableSet.of(
+ ACTIONS_EXTRA_FIELD, TRANSITIONS_EXTRA_FIELD, REPORTER_NAME_EXTRA_FIELD, ACTION_PLAN_NAME_EXTRA_FIELD);
+
+ private final I18n i18n;
+ private final Durations durations;
+ private final UserSession userSession;
+ private final UserJsonWriter userWriter;
+ private final IssueActionsWriter actionsWriter;
+
+ public IssueJsonWriter(I18n i18n, Durations durations, UserSession userSession, UserJsonWriter userWriter, IssueActionsWriter actionsWriter) {
+ this.i18n = i18n;
+ this.durations = durations;
+ this.userSession = userSession;
+ this.userWriter = userWriter;
+ this.actionsWriter = actionsWriter;
+ }
+
+ public void write(JsonWriter json, Issue issue, Map<String, User> usersByLogin, Map<String, ComponentDto> componentsByUuid,
+ Map<String, ComponentDto> projectsByComponentUuid, Multimap<String, DefaultIssueComment> commentsByIssues, Map<String, ActionPlan> actionPlanByKeys, List<String> extraFields) {
+ json.beginObject();
+
+ String actionPlanKey = issue.actionPlanKey();
+ ComponentDto file = componentsByUuid.get(issue.componentUuid());
+ ComponentDto project = null, subProject = null;
+ if (file != null) {
+ project = projectsByComponentUuid.get(file.uuid());
+ if (!file.projectUuid().equals(file.moduleUuid())) {
+ subProject = componentsByUuid.get(file.moduleUuid());
+ }
+ }
+ Duration debt = issue.debt();
+ Date updateDate = issue.updateDate();
+
+ json
+ .prop("key", issue.key())
+ .prop("component", file != null ? file.getKey() : null)
+ // Only used for the compatibility with the Issues Java WS Client <= 4.4 used by Eclipse
+ .prop("componentId", file != null ? file.getId() : null)
+ .prop("project", project != null ? project.getKey() : null)
+ .prop("subProject", subProject != null ? subProject.getKey() : null)
+ .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.encode(debt) : null)
+ .prop("reporter", issue.reporter())
+ .prop("author", issue.authorLogin())
+ .prop("actionPlan", actionPlanKey)
+ .prop("creationDate", isoDate(issue.creationDate()))
+ .prop("updateDate", isoDate(updateDate))
+ // TODO Remove as part of Front-end rework on Issue Domain
+ .prop("fUpdateAge", formatAgeDate(updateDate))
+ .prop("closeDate", isoDate(issue.closeDate()));
+
+ json.name("assignee");
+ userWriter.write(json, usersByLogin.get(issue.assignee()));
+
+ writeTags(issue, json);
+ writeIssueComments(commentsByIssues.get(issue.key()), usersByLogin, json);
+ writeIssueAttributes(issue, json);
+ writeIssueExtraFields(issue, usersByLogin, actionPlanByKeys, extraFields, json);
+ json.endObject();
+ }
+
+ @CheckForNull
+ private static String isoDate(@Nullable Date date) {
+ if (date != null) {
+ return DateUtils.formatDateTime(date);
+ }
+ return null;
+ }
+
+ private static void writeTags(Issue issue, JsonWriter json) {
+ Collection<String> tags = issue.tags();
+ if (tags != null && !tags.isEmpty()) {
+ json.name("tags").beginArray();
+ for (String tag : tags) {
+ json.value(tag);
+ }
+ json.endArray();
+ }
+ }
+
+ private void writeIssueComments(Collection<DefaultIssueComment> issueComments, Map<String, User> usersByLogin, JsonWriter json) {
+ if (!issueComments.isEmpty()) {
+ json.name("comments").beginArray();
+ String login = userSession.getLogin();
+ for (IssueComment comment : issueComments) {
+ String userLogin = comment.userLogin();
+ User user = userLogin != null ? usersByLogin.get(userLogin) : null;
+ json.beginObject()
+ .prop("key", comment.key())
+ .prop("login", comment.userLogin())
+ .prop("email", user != null ? user.email() : null)
+ .prop("userName", user != null ? user.name() : null)
+ .prop("htmlText", Markdown.convertToHtml(comment.markdownText()))
+ .prop("markdown", comment.markdownText())
+ .prop("updatable", login != null && login.equals(userLogin))
+ .prop("createdAt", DateUtils.formatDateTime(comment.createdAt()))
+ .endObject();
+ }
+ json.endArray();
+ }
+ }
+
+ private static 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(Issue issue, Map<String, User> usersByLogin, Map<String, ActionPlan> actionPlanByKeys,
+ @Nullable List<String> extraFields,
+ JsonWriter json) {
+ if (extraFields != null) {
+ if (extraFields.contains(ACTIONS_EXTRA_FIELD)) {
+ actionsWriter.writeActions(issue, json);
+ }
+
+ if (extraFields.contains(TRANSITIONS_EXTRA_FIELD)) {
+ actionsWriter.writeTransitions(issue, json);
+ }
+
+ writeReporterIfNeeded(issue, usersByLogin, extraFields, json);
+
+ writeActionPlanIfNeeded(issue, actionPlanByKeys, extraFields, json);
+ }
+ }
+
+ private void writeReporterIfNeeded(Issue issue, Map<String, User> usersByLogin, List<String> extraFields, JsonWriter json) {
+ String reporter = issue.reporter();
+ if (extraFields.contains(REPORTER_NAME_EXTRA_FIELD) && reporter != null) {
+ User user = usersByLogin.get(reporter);
+ json.prop(REPORTER_NAME_EXTRA_FIELD, user != null ? user.name() : null);
+ }
+ }
+
+ private void writeActionPlanIfNeeded(Issue issue, Map<String, ActionPlan> actionPlanByKeys, List<String> extraFields, JsonWriter json) {
+ String actionPlanKey = issue.actionPlanKey();
+ if (extraFields.contains(ACTION_PLAN_NAME_EXTRA_FIELD) && actionPlanKey != null) {
+ ActionPlan actionPlan = actionPlanByKeys.get(actionPlanKey);
+ json.prop(ACTION_PLAN_NAME_EXTRA_FIELD, actionPlan != null ? actionPlan.name() : null);
+ }
+ }
+
+ @CheckForNull
+ private String formatAgeDate(@Nullable Date date) {
+ if (date != null) {
+ return i18n.ageFromNow(userSession.locale(), date);
+ }
+ return null;
+ }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchAction.java b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchAction.java
index ab7c665bc22..2851efd96a5 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchAction.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchAction.java
@@ -38,7 +38,6 @@ import org.apache.commons.lang.BooleanUtils;
import org.sonar.api.i18n.I18n;
import org.sonar.api.issue.ActionPlan;
import org.sonar.api.issue.Issue;
-import org.sonar.api.issue.IssueComment;
import org.sonar.api.issue.internal.DefaultIssueComment;
import org.sonar.api.resources.Language;
import org.sonar.api.resources.Languages;
@@ -50,12 +49,9 @@ import org.sonar.api.server.ws.WebService;
import org.sonar.api.user.User;
import org.sonar.api.user.UserFinder;
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.core.component.ComponentDto;
import org.sonar.core.persistence.DbSession;
-import org.sonar.markdown.Markdown;
import org.sonar.server.db.DbClient;
import org.sonar.server.es.SearchOptions;
import org.sonar.server.es.SearchResult;
@@ -69,8 +65,6 @@ import org.sonar.server.issue.index.IssueIndex;
import org.sonar.server.rule.Rule;
import org.sonar.server.rule.RuleService;
import org.sonar.server.user.UserSession;
-import org.sonar.server.user.ws.UserJsonWriter;
-
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Maps.newHashMap;
import static com.google.common.collect.Sets.newHashSet;
@@ -79,18 +73,11 @@ public class SearchAction implements IssuesWsAction {
public static final String SEARCH_ACTION = "search";
- private static final String ACTIONS_EXTRA_FIELD = "actions";
- private static final String TRANSITIONS_EXTRA_FIELD = "transitions";
- private static final String ASSIGNEE_NAME_EXTRA_FIELD = "assigneeName";
- private static final String REPORTER_NAME_EXTRA_FIELD = "reporterName";
- private static final String ACTION_PLAN_NAME_EXTRA_FIELD = "actionPlanName";
-
private static final String EXTRA_FIELDS_PARAM = "extra_fields";
private static final String INTERNAL_PARAMETER_DISCLAIMER = "This parameter is mostly used by the Issues page, please prefer usage of the componentKeys parameter. ";
private final IssueService service;
- private final IssueActionsWriter actionsWriter;
private final IssueQueryService issueQueryService;
private final RuleService ruleService;
@@ -98,26 +85,25 @@ public class SearchAction implements IssuesWsAction {
private final ActionPlanService actionPlanService;
private final UserFinder userFinder;
private final I18n i18n;
- private final Durations durations;
private final Languages languages;
private final UserSession userSession;
- private final UserJsonWriter userWriter;
+ private final IssueJsonWriter issueWriter;
+ private final IssueComponentHelper issueComponentHelper;
- public SearchAction(DbClient dbClient, IssueService service, IssueActionsWriter actionsWriter, IssueQueryService issueQueryService,
- RuleService ruleService, ActionPlanService actionPlanService, UserFinder userFinder, I18n i18n, Durations durations, Languages languages,
- UserSession userSession, UserJsonWriter userWriter) {
+ public SearchAction(DbClient dbClient, IssueService service, IssueQueryService issueQueryService,
+ RuleService ruleService, ActionPlanService actionPlanService, UserFinder userFinder, I18n i18n, Languages languages,
+ UserSession userSession, IssueJsonWriter issueWriter, IssueComponentHelper issueComponentHelper) {
this.dbClient = dbClient;
this.service = service;
- this.actionsWriter = actionsWriter;
this.issueQueryService = issueQueryService;
this.ruleService = ruleService;
this.actionPlanService = actionPlanService;
this.userFinder = userFinder;
this.i18n = i18n;
- this.durations = durations;
this.languages = languages;
this.userSession = userSession;
- this.userWriter = userWriter;
+ this.issueWriter = issueWriter;
+ this.issueComponentHelper = issueComponentHelper;
}
@Override
@@ -193,7 +179,7 @@ public class SearchAction implements IssuesWsAction {
.setExampleValue("java,js");
action.createParam(EXTRA_FIELDS_PARAM)
.setDescription("Add some extra fields on each issue. Available since 4.4")
- .setPossibleValues(ACTIONS_EXTRA_FIELD, TRANSITIONS_EXTRA_FIELD, ASSIGNEE_NAME_EXTRA_FIELD, REPORTER_NAME_EXTRA_FIELD, ACTION_PLAN_NAME_EXTRA_FIELD);
+ .setPossibleValues(IssueJsonWriter.EXTRA_FIELDS);
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)");
@@ -216,7 +202,7 @@ public class SearchAction implements IssuesWsAction {
.setDescription("Only json format is available. This parameter is kept only for backward compatibility and shouldn't be used anymore");
}
- private void addComponentRelatedParams(WebService.NewAction action) {
+ private static void addComponentRelatedParams(WebService.NewAction action) {
action.createParam(IssueFilterParameters.ON_COMPONENT_ONLY)
.setDescription("Return only issues at a component's level, not on its descendants (modules, directories, files, etc). " +
"This parameter is only considered when componentKeys or componentUuids is set. " +
@@ -299,7 +285,7 @@ public class SearchAction implements IssuesWsAction {
json.endObject().close();
}
- private boolean shouldIgnorePaging(Request request) {
+ private static boolean shouldIgnorePaging(Request request) {
List<String> componentUuids = request.paramAsStrings(IssueFilterParameters.COMPONENT_UUIDS);
// Paging can be ignored only when querying issues for a single component (e.g in component viewer)
return componentUuids != null && componentUuids.size() == 1
@@ -325,6 +311,7 @@ public class SearchAction implements IssuesWsAction {
Map<String, ComponentDto> componentsByUuid = newHashMap();
Multimap<String, DefaultIssueComment> commentsByIssues = ArrayListMultimap.create();
Collection<ComponentDto> componentDtos = newHashSet();
+ List<ComponentDto> projectDtos = Lists.newArrayList();
Map<String, ComponentDto> projectsByComponentUuid = newHashMap();
for (IssueDoc issueDoc : result.getDocs()) {
@@ -360,20 +347,7 @@ public class SearchAction implements IssuesWsAction {
}
usersByLogin = getUsersByLogin(userLogins);
- List<ComponentDto> fileDtos = dbClient.componentDao().selectByUuids(session, componentUuids);
- List<ComponentDto> subProjectDtos = dbClient.componentDao().selectSubProjectsByComponentUuids(session, componentUuids);
- componentDtos.addAll(fileDtos);
- componentDtos.addAll(subProjectDtos);
- for (ComponentDto component : componentDtos) {
- projectUuids.add(component.projectUuid());
- }
-
- List<ComponentDto> projectDtos = dbClient.componentDao().selectByUuids(session, projectUuids);
- componentDtos.addAll(projectDtos);
- for (ComponentDto componentDto : componentDtos) {
- componentsByUuid.put(componentDto.uuid(), componentDto);
- }
- projectsByComponentUuid = getProjectsByComponentUuid(componentDtos, projectDtos);
+ projectsByComponentUuid = issueComponentHelper.prepareComponentsAndProjects(projectUuids, componentUuids, componentsByUuid, componentDtos, projectDtos, session);
writeProjects(json, projectDtos);
writeComponents(json, componentDtos, projectsByComponentUuid);
@@ -391,7 +365,7 @@ public class SearchAction implements IssuesWsAction {
writeLanguages(json);
}
- private void collectRuleKeys(Request request, SearchResult<IssueDoc> result, Set<RuleKey> ruleKeys) {
+ private static void collectRuleKeys(Request request, SearchResult<IssueDoc> result, Set<RuleKey> ruleKeys) {
Set<String> facetRules = result.getFacets().getBucketKeys(IssueFilterParameters.RULES);
if (facetRules != null) {
for (String rule : facetRules) {
@@ -481,11 +455,11 @@ public class SearchAction implements IssuesWsAction {
collectParameterValues(request, IssueFilterParameters.ACTION_PLANS, actionPlanKeys);
}
- private void collectBucketKeys(SearchResult<IssueDoc> result, String facetName, Collection<String> bucketKeys) {
+ private static void collectBucketKeys(SearchResult<IssueDoc> result, String facetName, Collection<String> bucketKeys) {
bucketKeys.addAll(result.getFacets().getBucketKeys(facetName));
}
- private void collectParameterValues(Request request, String facetName, Collection<String> facetKeys) {
+ private static void collectParameterValues(Request request, String facetName, Collection<String> facetKeys) {
Collection<String> paramValues = request.paramAsStrings(facetName);
if (paramValues != null) {
facetKeys.addAll(paramValues);
@@ -515,143 +489,12 @@ public class SearchAction implements IssuesWsAction {
json.name("issues").beginArray();
for (IssueDoc issue : result.getDocs()) {
- json.beginObject();
-
- String actionPlanKey = issue.actionPlanKey();
- ComponentDto file = componentsByUuid.get(issue.componentUuid());
- ComponentDto project = null, subProject = null;
- if (file != null) {
- project = projectsByComponentUuid.get(file.uuid());
- if (!file.projectUuid().equals(file.moduleUuid())) {
- subProject = componentsByUuid.get(file.moduleUuid());
- }
- }
- Duration debt = issue.debt();
- Date updateDate = issue.updateDate();
-
- json
- .prop("key", issue.key())
- .prop("component", file != null ? file.getKey() : null)
- // Only used for the compatibility with the Issues Java WS Client <= 4.4 used by Eclipse
- .prop("componentId", file != null ? file.getId() : null)
- .prop("project", project != null ? project.getKey() : null)
- .prop("subProject", subProject != null ? subProject.getKey() : null)
- .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.encode(debt) : null)
- .prop("reporter", issue.reporter())
- .prop("author", issue.authorLogin())
- .prop("actionPlan", actionPlanKey)
- .prop("creationDate", isoDate(issue.creationDate()))
- .prop("updateDate", isoDate(updateDate))
- // TODO Remove as part of Front-end rework on Issue Domain
- .prop("fUpdateAge", formatAgeDate(updateDate))
- .prop("closeDate", isoDate(issue.closeDate()));
-
- json.name("assignee");
- userWriter.write(json, usersByLogin.get(issue.assignee()));
-
- writeTags(issue, json);
- writeIssueComments(commentsByIssues.get(issue.key()), usersByLogin, json);
- writeIssueAttributes(issue, json);
- writeIssueExtraFields(issue, usersByLogin, actionPlanByKeys, extraFields, json);
- json.endObject();
+ issueWriter.write(json, issue, usersByLogin, componentsByUuid, projectsByComponentUuid, commentsByIssues, actionPlanByKeys, extraFields);
}
json.endArray();
}
- private static void writeTags(Issue issue, JsonWriter json) {
- Collection<String> tags = issue.tags();
- if (tags != null && !tags.isEmpty()) {
- json.name("tags").beginArray();
- for (String tag : tags) {
- json.value(tag);
- }
- json.endArray();
- }
- }
-
- private void writeIssueComments(Collection<DefaultIssueComment> issueComments, Map<String, User> usersByLogin, JsonWriter json) {
- if (!issueComments.isEmpty()) {
- json.name("comments").beginArray();
- String login = userSession.getLogin();
- for (IssueComment comment : issueComments) {
- String userLogin = comment.userLogin();
- User user = userLogin != null ? usersByLogin.get(userLogin) : null;
- json.beginObject()
- .prop("key", comment.key())
- .prop("login", comment.userLogin())
- .prop("email", user != null ? user.email() : null)
- .prop("userName", user != null ? user.name() : null)
- .prop("htmlText", Markdown.convertToHtml(comment.markdownText()))
- .prop("markdown", comment.markdownText())
- .prop("updatable", login != null && login.equals(userLogin))
- .prop("createdAt", DateUtils.formatDateTime(comment.createdAt()))
- .endObject();
- }
- json.endArray();
- }
- }
-
- private static 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(Issue issue, Map<String, User> usersByLogin, Map<String, ActionPlan> actionPlanByKeys,
- @Nullable List<String> extraFields,
- JsonWriter json) {
- if (extraFields != null) {
- if (extraFields.contains(ACTIONS_EXTRA_FIELD)) {
- actionsWriter.writeActions(issue, json);
- }
-
- if (extraFields.contains(TRANSITIONS_EXTRA_FIELD)) {
- actionsWriter.writeTransitions(issue, json);
- }
-
- writeAssigneeIfNeeded(issue, usersByLogin, extraFields, json);
-
- writeReporterIfNeeded(issue, usersByLogin, extraFields, json);
-
- writeActionPlanIfNeeded(issue, actionPlanByKeys, extraFields, json);
- }
- }
-
- private void writeAssigneeIfNeeded(Issue issue, Map<String, User> usersByLogin, List<String> extraFields, JsonWriter json) {
- String assignee = issue.assignee();
- if (extraFields.contains(ASSIGNEE_NAME_EXTRA_FIELD) && assignee != null) {
- User user = usersByLogin.get(assignee);
- json.prop(ASSIGNEE_NAME_EXTRA_FIELD, user != null ? user.name() : null);
- }
- }
-
- private void writeReporterIfNeeded(Issue issue, Map<String, User> usersByLogin, List<String> extraFields, JsonWriter json) {
- String reporter = issue.reporter();
- if (extraFields.contains(REPORTER_NAME_EXTRA_FIELD) && reporter != null) {
- User user = usersByLogin.get(reporter);
- json.prop(REPORTER_NAME_EXTRA_FIELD, user != null ? user.name() : null);
- }
- }
-
- private void writeActionPlanIfNeeded(Issue issue, Map<String, ActionPlan> actionPlanByKeys, List<String> extraFields, JsonWriter json) {
- String actionPlanKey = issue.actionPlanKey();
- if (extraFields.contains(ACTION_PLAN_NAME_EXTRA_FIELD) && actionPlanKey != null) {
- ActionPlan actionPlan = actionPlanByKeys.get(actionPlanKey);
- json.prop(ACTION_PLAN_NAME_EXTRA_FIELD, actionPlan != null ? actionPlan.name() : null);
- }
- }
-
private void writeComponents(JsonWriter json, Collection<ComponentDto> components, Map<String, ComponentDto> projectsByComponentUuid) {
json.name("components").beginArray();
for (ComponentDto component : components) {
@@ -754,39 +597,6 @@ public class SearchAction implements IssuesWsAction {
return actionPlans;
}
- private Map<String, ComponentDto> getProjectsByComponentUuid(Collection<ComponentDto> components, Collection<ComponentDto> projects) {
- Map<String, ComponentDto> projectsByUuid = buildProjectsByUuid(projects);
- return buildProjectsByComponentUuid(components, projectsByUuid);
- }
-
- private static Map<String, ComponentDto> buildProjectsByUuid(Collection<ComponentDto> projects) {
- Map<String, ComponentDto> projectsByUuid = newHashMap();
- for (ComponentDto project : projects) {
- if (project == null) {
- throw new IllegalStateException("Found a null project in issues");
- }
- if (project.uuid() == null) {
- throw new IllegalStateException("Project has no UUID: " + project.getKey());
- }
- projectsByUuid.put(project.uuid(), project);
- }
- return projectsByUuid;
- }
-
- private static Map<String, ComponentDto> buildProjectsByComponentUuid(Collection<ComponentDto> components, Map<String, ComponentDto> projectsByUuid) {
- Map<String, ComponentDto> projectsByComponentUuid = newHashMap();
- for (ComponentDto component : components) {
- if (component.uuid() == null) {
- throw new IllegalStateException("Component has no UUID: " + component.getKey());
- }
- if (!projectsByUuid.containsKey(component.projectUuid())) {
- throw new IllegalStateException("Project cannot be found for component: " + component.getKey() + " / " + component.uuid());
- }
- projectsByComponentUuid.put(component.uuid(), projectsByUuid.get(component.projectUuid()));
- }
- return projectsByComponentUuid;
- }
-
@CheckForNull
private static String isoDate(@Nullable Date date) {
if (date != null) {
@@ -803,14 +613,6 @@ public class SearchAction implements IssuesWsAction {
return null;
}
- @CheckForNull
- private String formatAgeDate(@Nullable Date date) {
- if (date != null) {
- return i18n.ageFromNow(userSession.locale(), date);
- }
- return null;
- }
-
protected void addMandatoryFacetValues(SearchResult<IssueDoc> results, String facetName, @Nullable List<String> mandatoryValues) {
Map<String, Long> buckets = results.getFacets().get(facetName);
if (buckets != null && mandatoryValues != null) {
diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
index 4ead39284af..dfccb580160 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
@@ -19,7 +19,8 @@
*/
package org.sonar.server.platform.platformlevel;
-import org.sonar.server.user.ws.UserJsonWriter;
+import org.sonar.server.issue.ws.IssueComponentHelper;
+import org.sonar.server.issue.ws.IssueJsonWriter;
import java.util.List;
import org.sonar.api.config.EmailSettings;
@@ -310,6 +311,7 @@ import org.sonar.server.user.index.UserIndexDefinition;
import org.sonar.server.user.index.UserIndexer;
import org.sonar.server.user.ws.CurrentAction;
import org.sonar.server.user.ws.FavoritesWs;
+import org.sonar.server.user.ws.UserJsonWriter;
import org.sonar.server.user.ws.UserPropertiesWs;
import org.sonar.server.user.ws.UsersWs;
import org.sonar.server.usergroups.ws.UserGroupsModule;
@@ -603,6 +605,8 @@ public class PlatformLevel4 extends PlatformLevel {
IssueBulkChangeService.class,
IssueChangelogFormatter.class,
IssuesWs.class,
+ IssueJsonWriter.class,
+ IssueComponentHelper.class,
org.sonar.server.issue.ws.ShowAction.class,
org.sonar.server.issue.ws.SearchAction.class,
org.sonar.server.issue.ws.TagsAction.class,
diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/InternalRubyIssueServiceTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/InternalRubyIssueServiceTest.java
index f63c5f0f72d..861f2b36ae5 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/issue/InternalRubyIssueServiceTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/issue/InternalRubyIssueServiceTest.java
@@ -22,6 +22,10 @@ package org.sonar.server.issue;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -35,21 +39,21 @@ import org.sonar.api.user.User;
import org.sonar.api.web.UserRole;
import org.sonar.core.issue.DefaultActionPlan;
import org.sonar.core.issue.db.IssueFilterDto;
+import org.sonar.core.persistence.DbSession;
import org.sonar.core.resource.ResourceDao;
import org.sonar.core.resource.ResourceDto;
import org.sonar.core.resource.ResourceQuery;
+import org.sonar.server.db.DbClient;
import org.sonar.server.es.SearchOptions;
import org.sonar.server.exceptions.BadRequestException;
import org.sonar.server.exceptions.Message;
import org.sonar.server.issue.actionplan.ActionPlanService;
import org.sonar.server.issue.filter.IssueFilterService;
+import org.sonar.server.issue.ws.IssueComponentHelper;
+import org.sonar.server.issue.ws.IssueJsonWriter;
import org.sonar.server.tester.UserSessionRule;
import org.sonar.server.user.ThreadLocalUserSession;
-
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
+import org.sonar.server.user.index.UserIndex;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Maps.newHashMap;
@@ -87,6 +91,16 @@ public class InternalRubyIssueServiceTest {
InternalRubyIssueService service;
+ IssueJsonWriter issueWriter;
+
+ IssueComponentHelper issueComponentHelper;
+
+ UserIndex userIndex;
+
+ DbClient dbClient;
+
+ DbSession dbSession;
+
@Before
public void setUp() {
issueService = mock(IssueService.class);
@@ -98,12 +112,17 @@ public class InternalRubyIssueServiceTest {
actionService = mock(ActionService.class);
issueFilterService = mock(IssueFilterService.class);
issueBulkChangeService = mock(IssueBulkChangeService.class);
+ issueWriter = mock(IssueJsonWriter.class);
+ issueComponentHelper = mock(IssueComponentHelper.class);
+ userIndex = mock(UserIndex.class);
+ dbClient = mock(DbClient.class);
+ dbSession = mock(DbSession.class);
ResourceDto project = new ResourceDto().setKey("org.sonar.Sample");
when(resourceDao.getResource(any(ResourceQuery.class))).thenReturn(project);
service = new InternalRubyIssueService(issueService, issueQueryService, commentService, changelogService, actionPlanService, resourceDao, actionService,
- issueFilterService, issueBulkChangeService, userSessionRule);
+ issueFilterService, issueBulkChangeService, issueWriter, issueComponentHelper, userIndex, dbClient, userSessionRule);
}
@Test
diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionMediumTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionMediumTest.java
index 0728a09507e..6cbf0c9d633 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionMediumTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionMediumTest.java
@@ -273,7 +273,7 @@ public class SearchActionMediumTest {
userSessionRule.login("john");
WsTester.Result result = wsTester.newGetRequest(IssuesWs.API_ENDPOINT, SearchAction.SEARCH_ACTION)
- .setParam("extra_fields", "actions,transitions,assigneeName,reporterName,actionPlanName").execute();
+ .setParam("extra_fields", "actions,transitions,reporterName,actionPlanName").execute();
result.assertJson(this.getClass(), "issue_with_extra_fields.json");
}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionMediumTest/issue_with_extra_fields.json b/server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionMediumTest/issue_with_extra_fields.json
index fc4378b4eb6..15a6bc0142b 100644
--- a/server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionMediumTest/issue_with_extra_fields.json
+++ b/server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionMediumTest/issue_with_extra_fields.json
@@ -8,7 +8,6 @@
"active": true,
"email": "simon@email.com"
},
- "assigneeName": "Simon",
"reporter": "fabrice",
"reporterName": "Fabrice",
"actionPlan": "AP-ABCD",
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/api/issues_controller.rb b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/api/issues_controller.rb
index fb7461739c7..020b2511474 100644
--- a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/api/issues_controller.rb
+++ b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/api/issues_controller.rb
@@ -307,7 +307,7 @@ class Api::IssuesController < Api::ApiController
respond_to do |format|
# if the request header "Accept" is "*/*", then the default format is the first one (json)
- format.json { render :json => jsonp(hash), :status => result.httpStatus }
+ format.json { render :json => Internal.issues.writeIssueJson(result.get), :status => result.httpStatus }
format.xml { render :xml => hash.to_xml(:skip_types => true, :root => 'sonar', :status => (result.ok ? 200 : 400)) }
end
end