diff options
author | Jean-Baptiste Lievremont <jean-baptiste.lievremont@sonarsource.com> | 2015-06-08 11:46:47 +0200 |
---|---|---|
committer | Jean-Baptiste Lievremont <jean-baptiste.lievremont@sonarsource.com> | 2015-06-18 09:34:34 +0200 |
commit | 5df799013fe35f29ae369e8ae35a7bbc8ba07218 (patch) | |
tree | 6dc31b42ab7c75bab42d7e1de43e080c1f7cb2de | |
parent | e183ec7123713007b84cfae85b79645ab6337acf (diff) | |
download | sonarqube-5df799013fe35f29ae369e8ae35a7bbc8ba07218.tar.gz sonarqube-5df799013fe35f29ae369e8ae35a7bbc8ba07218.zip |
SONAR-6582 Extract issue serialization class
Use common issue JSON representation for most actions on issues.
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 |