Java tutorial
/* * 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.ArrayListMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.io.Resources; 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; import org.sonar.api.rule.RuleKey; import org.sonar.api.rule.Severity; import org.sonar.api.server.ws.Request; 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.issue.db.IssueChangeDao; import org.sonar.core.persistence.DbSession; import org.sonar.markdown.Markdown; import org.sonar.server.db.DbClient; import org.sonar.server.issue.IssueQuery; import org.sonar.server.issue.IssueQueryService; import org.sonar.server.issue.IssueService; import org.sonar.server.issue.actionplan.ActionPlanService; import org.sonar.server.issue.filter.IssueFilterParameters; import org.sonar.server.issue.index.IssueDoc; import org.sonar.server.rule.Rule; import org.sonar.server.rule.RuleService; import org.sonar.server.search.FacetValue; import org.sonar.server.search.QueryContext; import org.sonar.server.search.Result; import org.sonar.server.search.ws.SearchRequestHandler; import org.sonar.server.user.UserSession; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; import java.util.Set; import static com.google.common.collect.Lists.newArrayList; import static com.google.common.collect.Maps.newHashMap; import static com.google.common.collect.Sets.newHashSet; public class SearchAction extends SearchRequestHandler<IssueQuery, Issue> { 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 IssueChangeDao issueChangeDao; private final IssueService service; private final IssueActionsWriter actionsWriter; private final IssueQueryService issueQueryService; private final RuleService ruleService; private final DbClient dbClient; private final ActionPlanService actionPlanService; private final UserFinder userFinder; private final I18n i18n; private final Durations durations; private final Languages languages; public SearchAction(DbClient dbClient, IssueChangeDao issueChangeDao, IssueService service, IssueActionsWriter actionsWriter, IssueQueryService issueQueryService, RuleService ruleService, ActionPlanService actionPlanService, UserFinder userFinder, I18n i18n, Durations durations, Languages languages) { super(SEARCH_ACTION); this.dbClient = dbClient; this.issueChangeDao = issueChangeDao; 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; } @Override protected void doDefinition(WebService.NewAction action) { action.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").setResponseExample(Resources.getResource(this.getClass(), "example-search.json")); addComponentRelatedParams(action); 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.RULES) .setDescription("Comma-separated list of coding rule keys. Format is <repository>:<rule>") .setExampleValue("squid:AvoidCycles"); action.createParam(IssueFilterParameters.TAGS).setDescription("Comma-separated list of tags.") .setExampleValue("security,convention"); action.createParam(IssueFilterParameters.HIDE_RULES).setDescription("To not return rules") .setDefaultValue(false).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_EXTRA_FIELD, TRANSITIONS_EXTRA_FIELD, ASSIGNEE_NAME_EXTRA_FIELD, REPORTER_NAME_EXTRA_FIELD, ACTION_PLAN_NAME_EXTRA_FIELD); 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(SearchRequestHandler.PARAM_SORT).setDescription("Sort field") .setDeprecatedKey(IssueFilterParameters.SORT).setPossibleValues(IssueQuery.SORTS); action.createParam(SearchRequestHandler.PARAM_ASCENDING).setDeprecatedKey(IssueFilterParameters.ASC) .setDescription("Ascending sort").setBooleanPossibleValues(); action.createParam(IssueFilterParameters.IGNORE_PAGING) .setDescription("Return the full list of issues, regardless of paging. For internal use only") .setBooleanPossibleValues().setDefaultValue("false"); action.createParam("format").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) { action.createParam(IssueFilterParameters.COMPONENT_KEYS).setDescription( "To retrieve issues associated to a specific list of components (comma-separated list of component keys). " + "A component can be a project, module, directory or file." + "If this parameter is set, componentUuids must not be set.") .setDeprecatedKey(IssueFilterParameters.COMPONENTS) .setExampleValue("org.apache.struts:struts:org.apache.struts.Action"); action.createParam(IssueFilterParameters.COMPONENTS) .setDescription("Deprecated since 5.1. See componentKeys."); action.createParam(IssueFilterParameters.COMPONENT_UUIDS).setDescription( "To retrieve issues associated to a specific list of components (comma-separated list of component UUIDs). " + INTERNAL_PARAMETER_DISCLAIMER + "A component can be a project, module, directory or file." + "If this parameter is set, componentKeys must not be set.") .setExampleValue("584a89f2-8037-4f7b-b82c-8b45d2d63fb2"); action.createParam(IssueFilterParameters.PROJECTS).setDescription("Deprecated since 5.1. See projectKeys"); action.createParam(IssueFilterParameters.PROJECT_KEYS).setDescription( "To retrieve issues associated to a specific list of projects (comma-separated list of project keys). " + INTERNAL_PARAMETER_DISCLAIMER + "If this parameter is set, projectUuids must not be set.") .setDeprecatedKey(IssueFilterParameters.PROJECTS) .setExampleValue("org.apache.struts:struts:org.apache.struts.Action"); action.createParam(IssueFilterParameters.PROJECT_UUIDS).setDescription( "To retrieve issues associated to a specific list of projects (comma-separated list of project UUIDs). " + INTERNAL_PARAMETER_DISCLAIMER + "Views are not supported. If this parameter is set, projectKeys must not be set.") .setExampleValue("7d8749e8-3070-4903-9188-bdd82933bb92"); action.createParam(IssueFilterParameters.COMPONENT_ROOTS) .setDescription("Deprecated since 5.1. See moduleKeys."); action.createParam(IssueFilterParameters.COMPONENT_ROOT_UUIDS) .setDescription("Deprecated since 5.1. See moduleUuids."); action.createParam(IssueFilterParameters.MODULE_KEYS).setDescription( "To retrieve issues associated to a specific list of modules (comma-separated list of module keys). " + INTERNAL_PARAMETER_DISCLAIMER + "Views are not supported. If this parameter is set, componentRootUuids must not be set.") .setDeprecatedKey(IssueFilterParameters.COMPONENT_ROOTS) .setExampleValue("org.apache.struts:struts"); action.createParam(IssueFilterParameters.MODULE_UUIDS).setDescription( "To retrieve issues associated to a specific list of components and their sub-components (comma-separated list of component UUIDs). " + INTERNAL_PARAMETER_DISCLAIMER + "Views are not supported. If this parameter is set, moduleKeys must not be set.") .setDeprecatedKey(IssueFilterParameters.COMPONENT_ROOT_UUIDS) .setExampleValue("7d8749e8-3070-4903-9188-bdd82933bb92"); action.createParam(IssueFilterParameters.ON_COMPONENT_ONLY).setDescription( "Return only issues at the component's level, not on its descendants (modules, directories, files, etc.)") .setBooleanPossibleValues().setDefaultValue("false"); } @Override protected IssueQuery doQuery(Request request) { return issueQueryService.createFromRequest(request); } @Override protected Result<Issue> doSearch(IssueQuery query, QueryContext context) { Collection<String> components = query.componentUuids(); if (components != null && components.size() == 1 && BooleanUtils.isTrue(query.ignorePaging())) { context.setShowFullResult(true); } return service.search(query, context); } @Override @CheckForNull protected Collection<String> possibleFields() { return Collections.emptyList(); } @Override @CheckForNull protected Collection<String> possibleFacets() { return Arrays.asList(new String[] { IssueFilterParameters.SEVERITIES, IssueFilterParameters.STATUSES, IssueFilterParameters.RESOLUTIONS, IssueFilterParameters.ACTION_PLANS, IssueFilterParameters.PROJECT_UUIDS, IssueFilterParameters.RULES, IssueFilterParameters.ASSIGNEES, IssueFilterParameters.REPORTERS, IssueFilterParameters.COMPONENT_UUIDS, IssueFilterParameters.LANGUAGES, IssueFilterParameters.TAGS, }); } @Override protected void doContextResponse(Request request, QueryContext context, Result<Issue> result, JsonWriter json) { List<String> issueKeys = newArrayList(); Set<RuleKey> ruleKeys = newHashSet(); Set<String> projectUuids = newHashSet(); Set<String> componentUuids = newHashSet(); Set<String> actionPlanKeys = newHashSet(); List<String> userLogins = newArrayList(); Map<String, User> usersByLogin = newHashMap(); Map<String, ComponentDto> componentsByUuid = newHashMap(); Multimap<String, DefaultIssueComment> commentsByIssues = ArrayListMultimap.create(); Collection<ComponentDto> componentDtos = newHashSet(); List<ComponentDto> projectDtos = newArrayList(); Map<String, ComponentDto> projectsByComponentUuid = newHashMap(); for (Issue issue : result.getHits()) { IssueDoc issueDoc = (IssueDoc) issue; issueKeys.add(issue.key()); ruleKeys.add(issue.ruleKey()); projectUuids.add(issueDoc.projectUuid()); componentUuids.add(issueDoc.componentUuid()); actionPlanKeys.add(issue.actionPlanKey()); if (issue.reporter() != null) { userLogins.add(issue.reporter()); } if (issue.assignee() != null) { userLogins.add(issue.assignee()); } } collectRuleKeys(request, result, ruleKeys); collectFacetsData(request, result, projectUuids, componentUuids, userLogins, actionPlanKeys); UserSession userSession = UserSession.get(); if (userSession.isLoggedIn()) { userLogins.add(userSession.login()); } DbSession session = dbClient.openSession(false); try { List<DefaultIssueComment> comments = issueChangeDao.selectCommentsByIssues(session, issueKeys); for (DefaultIssueComment issueComment : comments) { userLogins.add(issueComment.userLogin()); commentsByIssues.put(issueComment.issueKey(), issueComment); } usersByLogin = getUsersByLogin(userLogins); List<ComponentDto> fileDtos = dbClient.componentDao().getByUuids(session, componentUuids); List<ComponentDto> subProjectDtos = dbClient.componentDao().findSubProjectsByComponentUuids(session, componentUuids); componentDtos.addAll(fileDtos); componentDtos.addAll(subProjectDtos); for (ComponentDto component : componentDtos) { projectUuids.add(component.projectUuid()); } projectDtos = dbClient.componentDao().getByUuids(session, projectUuids); componentDtos.addAll(projectDtos); for (ComponentDto componentDto : componentDtos) { componentsByUuid.put(componentDto.uuid(), componentDto); } projectsByComponentUuid = getProjectsByComponentUuid(componentDtos, projectDtos); writeProjects(json, projectDtos); writeComponents(json, componentDtos, projectsByComponentUuid); } finally { session.close(); } Map<String, ActionPlan> actionPlanByKeys = getActionPlanByKeys(actionPlanKeys); writeIssues(result, commentsByIssues, usersByLogin, actionPlanByKeys, componentsByUuid, projectsByComponentUuid, request.paramAsStrings(EXTRA_FIELDS_PARAM), json); writeRules(json, !request.mandatoryParamAsBoolean(IssueFilterParameters.HIDE_RULES) ? ruleService.getByKeys(ruleKeys) : Collections.<Rule>emptyList()); writeUsers(json, usersByLogin); writeActionPlans(json, actionPlanByKeys.values()); writeLanguages(json); // TODO remove legacy paging. Handled by the SearchRequestHandler writeLegacyPaging(context, json, result); } private void collectRuleKeys(Request request, Result<Issue> result, Set<RuleKey> ruleKeys) { Collection<FacetValue> facetRules = result.getFacetValues(IssueFilterParameters.RULES); if (facetRules != null) { for (FacetValue rule : facetRules) { ruleKeys.add(RuleKey.parse(rule.getKey())); } } List<String> rulesFromRequest = request.paramAsStrings(IssueFilterParameters.RULES); if (rulesFromRequest != null) { for (String ruleKey : rulesFromRequest) { ruleKeys.add(RuleKey.parse(ruleKey)); } } } @Override protected void writeFacets(Request request, QueryContext context, Result<?> results, JsonWriter json) { addMandatoryFacetValues(results, IssueFilterParameters.SEVERITIES, Severity.ALL); addMandatoryFacetValues(results, IssueFilterParameters.STATUSES, Issue.STATUSES); List<String> resolutions = Lists.newArrayList(""); resolutions.addAll(Issue.RESOLUTIONS); addMandatoryFacetValues(results, IssueFilterParameters.RESOLUTIONS, resolutions); addMandatoryFacetValues(results, IssueFilterParameters.PROJECT_UUIDS, request.paramAsStrings(IssueFilterParameters.PROJECT_UUIDS)); List<String> assignees = Lists.newArrayList(""); List<String> assigneesFromRequest = request.paramAsStrings(IssueFilterParameters.ASSIGNEES); if (assigneesFromRequest != null) { assignees.addAll(assigneesFromRequest); } UserSession userSession = UserSession.get(); if (userSession.isLoggedIn()) { assignees.add(userSession.login()); } addMandatoryFacetValues(results, IssueFilterParameters.ASSIGNEES, assignees); addMandatoryFacetValues(results, IssueFilterParameters.REPORTERS, request.paramAsStrings(IssueFilterParameters.REPORTERS)); addMandatoryFacetValues(results, IssueFilterParameters.RULES, request.paramAsStrings(IssueFilterParameters.RULES)); addMandatoryFacetValues(results, IssueFilterParameters.LANGUAGES, request.paramAsStrings(IssueFilterParameters.LANGUAGES)); addMandatoryFacetValues(results, IssueFilterParameters.TAGS, request.paramAsStrings(IssueFilterParameters.TAGS)); List<String> actionPlans = Lists.newArrayList(""); List<String> actionPlansFromRequest = request.paramAsStrings(IssueFilterParameters.ACTION_PLANS); if (actionPlansFromRequest != null) { actionPlans.addAll(actionPlansFromRequest); } addMandatoryFacetValues(results, IssueFilterParameters.ACTION_PLANS, actionPlans); addMandatoryFacetValues(results, IssueFilterParameters.COMPONENT_UUIDS, request.paramAsStrings(IssueFilterParameters.COMPONENT_UUIDS)); super.writeFacets(request, context, results, json); } private void addMandatoryFacetValues(Result<?> results, String facetName, @Nullable List<String> mandatoryValues) { Collection<FacetValue> facetValues = results.getFacetValues(facetName); if (facetValues != null) { Map<String, Long> valuesByItem = Maps.newHashMap(); for (FacetValue value : facetValues) { valuesByItem.put(value.getKey(), value.getValue()); } List<String> valuesToAdd = mandatoryValues == null ? Lists.<String>newArrayList() : mandatoryValues; for (String item : valuesToAdd) { if (!valuesByItem.containsKey(item)) { facetValues.add(new FacetValue(item, 0)); } } } } private void collectFacetsData(Request request, Result<Issue> result, Set<String> projectUuids, Set<String> componentUuids, List<String> userLogins, Set<String> actionPlanKeys) { collectFacetKeys(result, IssueFilterParameters.PROJECT_UUIDS, projectUuids); collectParameterValues(request, IssueFilterParameters.PROJECT_UUIDS, projectUuids); collectFacetKeys(result, IssueFilterParameters.COMPONENT_UUIDS, componentUuids); collectParameterValues(request, IssueFilterParameters.COMPONENT_UUIDS, componentUuids); collectParameterValues(request, IssueFilterParameters.COMPONENT_ROOT_UUIDS, componentUuids); collectFacetKeys(result, IssueFilterParameters.ASSIGNEES, userLogins); collectParameterValues(request, IssueFilterParameters.ASSIGNEES, userLogins); collectFacetKeys(result, IssueFilterParameters.REPORTERS, userLogins); collectParameterValues(request, IssueFilterParameters.REPORTERS, userLogins); collectFacetKeys(result, IssueFilterParameters.ACTION_PLANS, actionPlanKeys); collectParameterValues(request, IssueFilterParameters.ACTION_PLANS, actionPlanKeys); } private void collectFacetKeys(Result<Issue> result, String facetName, Collection<String> facetKeys) { Collection<FacetValue> facetValues = result.getFacetValues(facetName); if (facetValues != null) { for (FacetValue project : facetValues) { facetKeys.add(project.getKey()); } } } private void collectParameterValues(Request request, String facetName, Collection<String> facetKeys) { Collection<String> paramValues = request.paramAsStrings(facetName); if (paramValues != null) { facetKeys.addAll(paramValues); } } private void writeLegacyPaging(QueryContext context, JsonWriter json, Result<?> result) { // TODO remove with stas on HTML side json.prop("maxResultsReached", false); long pages = context.getLimit(); if (pages > 0) { pages = result.getTotal() / context.getLimit(); if (result.getTotal() % context.getLimit() > 0) { pages++; } } json.name("paging").beginObject().prop("pageIndex", context.getPage()).prop("pageSize", context.getLimit()) .prop("total", result.getTotal()) // TODO Remove as part of Front-end rework on Issue Domain .prop("fTotal", i18n.formatInteger(UserSession.get().locale(), (int) result.getTotal())) .prop("pages", pages).endObject(); } // TODO change to use the RuleMapper private void writeRules(JsonWriter json, Collection<Rule> rules) { json.name("rules").beginArray(); for (Rule rule : rules) { json.beginObject().prop("key", rule.key().toString()).prop("name", rule.name()) .prop("lang", rule.language()).prop("desc", rule.htmlDescription()) .prop("status", rule.status().toString()); Language lang = languages.get(rule.language()); json.prop("langName", lang == null ? null : lang.getName()); json.endObject(); } json.endArray(); } private void writeIssues(Result<Issue> result, Multimap<String, DefaultIssueComment> commentsByIssues, Map<String, User> usersByLogin, Map<String, ActionPlan> actionPlanByKeys, Map<String, ComponentDto> componentsByUuid, Map<String, ComponentDto> projectsByComponentUuid, @Nullable List<String> extraFields, JsonWriter json) { json.name("issues").beginArray(); for (Issue issue : result.getHits()) { 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("assignee", issue.assignee()).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())); writeTags(issue, json); writeIssueComments(commentsByIssues.get(issue.key()), usersByLogin, json); writeIssueAttributes(issue, json); writeIssueExtraFields(issue, project != null ? project.getKey() : null, usersByLogin, actionPlanByKeys, extraFields, json); json.endObject(); } json.endArray(); } private void writeTags(Issue issue, JsonWriter json) { Collection<String> tags = ((IssueDoc) 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.get().login(); 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("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 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, @Nullable String projectKey, 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) { ComponentDto project = projectsByComponentUuid.get(component.uuid()); json.beginObject().prop("uuid", component.uuid()).prop("key", component.key()) .prop("id", component.getId()).prop("enabled", component.isEnabled()) .prop("qualifier", component.qualifier()).prop("name", component.name()) .prop("longName", component.longName()).prop("path", component.path()) // On a root project, parentProjectId is null but projectId is equal to itself, which make no sense. .prop("projectId", (component.projectUuid() != null && component.parentProjectId() != null) ? project.getId() : null) // TODO should be renamed to parentProjectId .prop("subProjectId", component.parentProjectId()).endObject(); } json.endArray(); } private void writeProjects(JsonWriter json, List<ComponentDto> projects) { json.name("projects").beginArray(); for (ComponentDto project : projects) { json.beginObject().prop("uuid", project.uuid()).prop("key", project.key()).prop("id", project.getId()) .prop("qualifier", project.qualifier()).prop("name", project.name()) .prop("longName", project.longName()).endObject(); } json.endArray(); } private void writeUsers(JsonWriter json, Map<String, User> usersByLogin) { json.name("users").beginArray(); for (User user : usersByLogin.values()) { json.beginObject().prop("login", user.login()).prop("name", user.name()).prop("active", user.active()) .prop("email", user.email()).endObject(); } json.endArray(); } private void writeLanguages(JsonWriter json) { json.name("languages").beginArray(); for (Language language : languages.all()) { json.beginObject().prop("key", language.getKey()).prop("name", language.getName()).endObject(); } json.endArray(); } private void writeActionPlans(JsonWriter json, Collection<ActionPlan> plans) { if (!plans.isEmpty()) { json.name("actionPlans").beginArray(); for (ActionPlan actionPlan : plans) { Date deadLine = actionPlan.deadLine(); Date updatedAt = actionPlan.updatedAt(); json.beginObject().prop("key", actionPlan.key()).prop("name", actionPlan.name()) .prop("status", actionPlan.status()).prop("project", actionPlan.projectKey()) .prop("userLogin", actionPlan.userLogin()).prop("deadLine", isoDate(deadLine)) .prop("fDeadLine", formatDate(deadLine)).prop("createdAt", isoDate(actionPlan.createdAt())) .prop("fCreatedAt", formatDate(actionPlan.createdAt())) .prop("updatedAt", isoDate(actionPlan.updatedAt())) .prop("fUpdatedAt", formatDate(updatedAt)).endObject(); } json.endArray(); } } private Map<String, User> getUsersByLogin(List<String> userLogins) { Map<String, User> usersByLogin = newHashMap(); for (User user : userFinder.findByLogins(userLogins)) { usersByLogin.put(user.login(), user); } return usersByLogin; } private Map<String, ActionPlan> getActionPlanByKeys(Collection<String> actionPlanKeys) { Map<String, ActionPlan> actionPlans = newHashMap(); for (ActionPlan actionPlan : actionPlanService.findByKeys(actionPlanKeys)) { actionPlans.put(actionPlan.key(), actionPlan); } return actionPlans; } private Map<String, ComponentDto> getProjectsByComponentUuid(Collection<ComponentDto> components, Collection<ComponentDto> projects) { Map<String, ComponentDto> projectsByUuid = buildProjectsByUuid(projects); return buildProjectsByComponentUuid(components, projectsByUuid); } private 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 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 String isoDate(@Nullable Date date) { if (date != null) { return DateUtils.formatDateTime(date); } return null; } @CheckForNull private String formatDate(@Nullable Date date) { if (date != null) { return i18n.formatDateTime(UserSession.get().locale(), date); } return null; } @CheckForNull private String formatAgeDate(@Nullable Date date) { if (date != null) { return i18n.ageFromNow(UserSession.get().locale(), date); } return null; } }