diff options
author | Stephane Gamard <stephane.gamard@sonarsource.com> | 2014-09-04 15:55:51 +0200 |
---|---|---|
committer | Stephane Gamard <stephane.gamard@sonarsource.com> | 2014-09-04 15:55:51 +0200 |
commit | 0b4912da7d3e242e0c79a0d1a711763b73dfb628 (patch) | |
tree | a521372dcafbbbaabbbf31f3f19102e48d336b75 | |
parent | 7fab7d6899f65aa50b359ee912cc802d4a66e47c (diff) | |
download | sonarqube-0b4912da7d3e242e0c79a0d1a711763b73dfb628.tar.gz sonarqube-0b4912da7d3e242e0c79a0d1a711763b73dfb628.zip |
SONAR-5531 - Wrapped IssueIndex with WS SearchAction
6 files changed, 393 insertions, 8 deletions
diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/IssueService.java b/server/sonar-server/src/main/java/org/sonar/server/issue/IssueService.java index 258160aba25..6c8351a4319 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/IssueService.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/IssueService.java @@ -24,6 +24,7 @@ import com.google.common.base.Strings; import com.google.common.collect.HashMultiset; import com.google.common.collect.Multiset; import org.apache.commons.lang.StringUtils; +import org.elasticsearch.action.search.SearchResponse; import org.sonar.api.ServerComponent; import org.sonar.api.issue.ActionPlan; import org.sonar.api.issue.Issue; @@ -54,7 +55,10 @@ import org.sonar.core.rule.RuleDto; import org.sonar.core.user.AuthorizationDao; import org.sonar.server.db.DbClient; import org.sonar.server.issue.actionplan.ActionPlanService; +import org.sonar.server.issue.index.IssueIndex; +import org.sonar.server.issue.index.IssueResult; import org.sonar.server.search.IndexClient; +import org.sonar.server.search.QueryOptions; import org.sonar.server.user.UserSession; import javax.annotation.Nullable; @@ -324,4 +328,20 @@ public class IssueService implements ServerComponent { return aggregation; } + public Issue getByKey(String key) { + return indexClient.get(IssueIndex.class).getByKey(key); + } + + public IssueResult search(IssueQuery query, QueryOptions options) { + + IssueIndex issueIndex = indexClient.get(IssueIndex.class); + + SearchResponse esResults = issueIndex.search(query, options); + + // Extend the content of the resultSet to make an actual IssueResponse + IssueResult results = new IssueResult(issueIndex, esResults); + + // TODO Implement the logic of search here!!! + return results; + } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndex.java b/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndex.java index 465013a2b91..50bf8fa0862 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndex.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndex.java @@ -20,12 +20,16 @@ package org.sonar.server.issue.index; import com.google.common.base.Preconditions; +import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.common.settings.ImmutableSettings; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.index.query.QueryBuilders; +import org.sonar.api.issue.IssueQuery; import org.sonar.core.issue.db.IssueDto; import org.sonar.server.search.BaseIndex; import org.sonar.server.search.IndexDefinition; import org.sonar.server.search.IndexField; +import org.sonar.server.search.QueryOptions; import org.sonar.server.search.SearchClient; import java.io.IOException; @@ -100,4 +104,13 @@ public class IssueIndex extends BaseIndex<IssueDoc, IssueDto, String> { Preconditions.checkNotNull(fields, "Cannot construct Issue with null response"); return new IssueDoc(fields); } + + public SearchResponse search(IssueQuery query, QueryOptions options) { + + // TODO implement filters and search + + return getClient().execute( + getClient().prepareSearch(getIndexName()) + .setQuery(QueryBuilders.matchAllQuery())); + } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueMapping.java b/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueMapping.java new file mode 100644 index 00000000000..fb0421a9cb7 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueMapping.java @@ -0,0 +1,11 @@ +package org.sonar.server.issue.index; + +import org.sonar.server.search.ws.BaseMapping; + +public class IssueMapping extends BaseMapping<IssueDoc, IssueMappingContext> { + +} + +class IssueMappingContext { + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueResult.java b/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueResult.java index 3f087f49756..c7ae8f393fb 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueResult.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueResult.java @@ -49,7 +49,7 @@ public class IssueResult extends Result<IssueDoc> implements IssueQueryResult { Paging paging; public IssueResult(SearchResponse response) { - super(response); + this(null, response); } public IssueResult(@Nullable BaseIndex<IssueDoc, ?, ?> index, SearchResponse response) { @@ -58,7 +58,7 @@ public class IssueResult extends Result<IssueDoc> implements IssueQueryResult { @Override public List<Issue> issues() { - return ImmutableList.<Issue>copyOf(this.getHits()); + return ImmutableList.<Issue>builder().addAll(this.getHits()).build(); } @Override 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 fa2e8e06c44..cd7f0f7c972 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 @@ -19,18 +19,51 @@ */ package org.sonar.server.issue.ws; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Function; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; import com.google.common.io.Resources; +import org.sonar.api.component.Component; import org.sonar.api.i18n.I18n; +import org.sonar.api.issue.ActionPlan; import org.sonar.api.issue.Issue; +import org.sonar.api.issue.IssueComment; import org.sonar.api.issue.IssueQuery; +import org.sonar.api.issue.IssueQueryResult; +import org.sonar.api.issue.internal.DefaultIssue; +import org.sonar.api.rule.RuleKey; import org.sonar.api.rule.Severity; +import org.sonar.api.rules.Rule; import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.RequestHandler; import org.sonar.api.server.ws.Response; import org.sonar.api.server.ws.WebService; +import org.sonar.api.user.User; +import org.sonar.api.utils.DateUtils; +import org.sonar.api.utils.Duration; import org.sonar.api.utils.Durations; +import org.sonar.api.utils.text.JsonWriter; +import org.sonar.api.web.UserRole; +import org.sonar.core.component.ComponentDto; +import org.sonar.markdown.Markdown; +import org.sonar.server.issue.IssueService; import org.sonar.server.issue.filter.IssueFilterParameters; -import org.sonar.server.issue.index.IssueIndex; +import org.sonar.server.issue.index.IssueMapping; +import org.sonar.server.issue.index.IssueResult; +import org.sonar.server.search.QueryOptions; +import org.sonar.server.search.ws.SearchOptions; +import org.sonar.server.user.UserSession; + +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import static com.google.common.collect.Lists.newArrayList; public class SearchAction implements RequestHandler { @@ -44,13 +77,17 @@ public class SearchAction implements RequestHandler { private static final String EXTRA_FIELDS_PARAM = "extra_fields"; - private final IssueIndex issueIndex; + public static final String PARAM_FACETS = "facets"; + + private final IssueService service; + private final IssueMapping mapping; private final IssueActionsWriter actionsWriter; private final I18n i18n; private final Durations durations; - public SearchAction(IssueIndex index, IssueActionsWriter actionsWriter, I18n i18n, Durations durations) { - this.issueIndex = index; + public SearchAction(IssueService service, IssueMapping mapping, IssueActionsWriter actionsWriter, I18n i18n, Durations durations) { + this.service = service; + this.mapping = mapping; this.actionsWriter = actionsWriter; this.i18n = i18n; this.durations = durations; @@ -64,6 +101,24 @@ public class SearchAction implements RequestHandler { .setHandler(this) .setResponseExample(Resources.getResource(this.getClass(), "example-search.json")); + // Add globalized search options. Will also support legacy params + // Generic search parameters + SearchOptions.defineFieldsParam(action, + ImmutableList.<String>builder().addAll(mapping.supportedFields()).build()); + SearchOptions.definePageParams(action); + + // Issue-specific search parameters + defineIssueSearchParameters(action); + + // Other parameters + action.createParam(PARAM_FACETS) + .setDescription("Compute predefined facets") + .setBooleanPossibleValues() + .setDefaultValue("false"); + } + + public static void defineIssueSearchParameters(WebService.NewAction action) { + action.createParam(IssueFilterParameters.ISSUES) .setDescription("Comma-separated list of issue keys") .setExampleValue("5bccd6e8-f525-43a2-8d76-fcb13dde79ef"); @@ -147,7 +202,293 @@ public class SearchAction implements RequestHandler { } @Override - public void handle(Request request, Response response) throws Exception { + public void handle(Request request, Response response) { + IssueQuery query = createQuery(request); + SearchOptions searchOptions = SearchOptions.create(request); + QueryOptions queryOptions = new QueryOptions(); + mapping.newQueryOptions(searchOptions); + queryOptions.setFacet(request.mandatoryParamAsBoolean(PARAM_FACETS)); + + IssueResult results = service.search(query, queryOptions); + + JsonWriter json = response.newJsonWriter(); + json.beginObject(); + + writePaging(results, json); + writeIssues(results, request.paramAsStrings(EXTRA_FIELDS_PARAM), json); + writeComponents(results, json); + writeProjects(results, json); + writeRules(results, json); + writeUsers(results, json); + writeActionPlans(results, json); + + json.endObject().close(); + } + + private void writePaging(IssueQueryResult result, JsonWriter json) { + json.prop("maxResultsReached", result.maxResultsReached()); + json.name("paging").beginObject() + .prop("pageIndex", result.paging().pageIndex()) + .prop("pageSize", result.paging().pageSize()) + .prop("total", result.paging().total()) + .prop("fTotal", i18n.formatInteger(UserSession.get().locale(), result.paging().total())) + .prop("pages", result.paging().pages()) + .endObject(); + } + + private void writeIssues(IssueQueryResult result, @Nullable List<String> extraFields, JsonWriter json) { + json.name("issues").beginArray(); + + for (Issue i : result.issues()) { + json.beginObject(); + + DefaultIssue issue = (DefaultIssue) i; + String actionPlanKey = issue.actionPlanKey(); + Duration debt = issue.debt(); + Date updateDate = issue.updateDate(); + + json + .prop("key", issue.key()) + .prop("component", issue.componentKey()) + .prop("componentId", issue.componentId()) + .prop("project", issue.projectKey()) + .prop("rule", issue.ruleKey().toString()) + .prop("status", issue.status()) + .prop("resolution", issue.resolution()) + .prop("severity", issue.severity()) + .prop("message", issue.message()) + .prop("line", issue.line()) + .prop("debt", debt != null ? durations.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)) + .prop("fUpdateAge", formatAgeDate(updateDate)) + .prop("closeDate", isoDate(issue.closeDate())); + + writeIssueComments(result, issue, json); + writeIssueAttributes(issue, json); + writeIssueExtraFields(result, issue, extraFields, json); + json.endObject(); + } + + json.endArray(); + } + + private void writeIssueComments(IssueQueryResult queryResult, Issue issue, JsonWriter json) { + if (!issue.comments().isEmpty()) { + json.name("comments").beginArray(); + String login = UserSession.get().login(); + for (IssueComment comment : issue.comments()) { + String userLogin = comment.userLogin(); + User user = userLogin != null ? queryResult.user(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(IssueQueryResult result, Issue issue, @Nullable List<String> extraFields, JsonWriter json) { + if (extraFields != null && UserSession.get().isLoggedIn()) { + if (extraFields.contains(ACTIONS_EXTRA_FIELD)) { + actionsWriter.writeActions(issue, json); + } + + if (extraFields.contains(TRANSITIONS_EXTRA_FIELD)) { + actionsWriter.writeTransitions(issue, json); + } + + String assignee = issue.assignee(); + if (extraFields.contains(ASSIGNEE_NAME_EXTRA_FIELD) && assignee != null) { + User user = result.user(assignee); + json.prop(ASSIGNEE_NAME_EXTRA_FIELD, user != null ? user.name() : null); + } + + String reporter = issue.reporter(); + if (extraFields.contains(REPORTER_NAME_EXTRA_FIELD) && reporter != null) { + User user = result.user(reporter); + json.prop(REPORTER_NAME_EXTRA_FIELD, user != null ? user.name() : null); + } + + String actionPlanKey = issue.actionPlanKey(); + if (extraFields.contains(ACTION_PLAN_NAME_EXTRA_FIELD) && actionPlanKey != null) { + ActionPlan actionPlan = result.actionPlan(issue); + json.prop(ACTION_PLAN_NAME_EXTRA_FIELD, actionPlan != null ? actionPlan.name() : null); + } + } + } + + private void writeComponents(IssueQueryResult result, JsonWriter json) { + json.name("components").beginArray(); + for (Component component : result.components()) { + ComponentDto componentDto = (ComponentDto) component; + json.beginObject() + .prop("key", component.key()) + .prop("id", componentDto.getId()) + .prop("qualifier", component.qualifier()) + .prop("name", component.name()) + .prop("longName", component.longName()) + .prop("path", component.path()) + // On a root project, subProjectId is null but projectId is equal to itself, which make no sense. + .prop("projectId", (componentDto.projectId() != null && componentDto.subProjectId() != null) ? componentDto.projectId() : null) + .prop("subProjectId", componentDto.subProjectId()) + .endObject(); + } + json.endArray(); + } + + private void writeProjects(IssueQueryResult result, JsonWriter json) { + json.name("projects").beginArray(); + for (Component project : result.projects()) { + ComponentDto componentDto = (ComponentDto) project; + json.beginObject() + .prop("key", project.key()) + .prop("id", componentDto.getId()) + .prop("qualifier", project.qualifier()) + .prop("name", project.name()) + .prop("longName", project.longName()) + .endObject(); + } + json.endArray(); + } + + private void writeRules(IssueQueryResult result, JsonWriter json) { + json.name("rules").beginArray(); + for (Rule rule : result.rules()) { + json.beginObject() + .prop("key", rule.ruleKey().toString()) + .prop("name", rule.getName()) + .prop("desc", rule.getDescription()) + .prop("status", rule.getStatus()) + .endObject(); + } + json.endArray(); + } + + private void writeUsers(IssueQueryResult result, JsonWriter json) { + json.name("users").beginArray(); + for (User user : result.users()) { + json.beginObject() + .prop("login", user.login()) + .prop("name", user.name()) + .prop("active", user.active()) + .prop("email", user.email()) + .endObject(); + } + json.endArray(); + } + + private void writeActionPlans(IssueQueryResult result, JsonWriter json) { + if (!result.actionPlans().isEmpty()) { + json.name("actionPlans").beginArray(); + for (ActionPlan actionPlan : result.actionPlans()) { + 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(); + } + } + + @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; + } + + @CheckForNull + private static Collection<RuleKey> stringsToRules(@Nullable Collection<String> rules) { + if (rules != null) { + return newArrayList(Iterables.transform(rules, new Function<String, RuleKey>() { + @Override + public RuleKey apply(@Nullable String s) { + return s != null ? RuleKey.parse(s) : null; + } + })); + } + return null; + } + @VisibleForTesting + static IssueQuery createQuery(Request request) { + IssueQuery.Builder builder = IssueQuery.builder() + .requiredRole(UserRole.USER) + .issueKeys(request.paramAsStrings(IssueFilterParameters.ISSUES)) + .severities(request.paramAsStrings(IssueFilterParameters.SEVERITIES)) + .statuses(request.paramAsStrings(IssueFilterParameters.STATUSES)) + .resolutions(request.paramAsStrings(IssueFilterParameters.RESOLUTIONS)) + .resolved(request.paramAsBoolean(IssueFilterParameters.RESOLVED)) + .components(request.paramAsStrings(IssueFilterParameters.COMPONENTS)) + .componentRoots(request.paramAsStrings(IssueFilterParameters.COMPONENT_ROOTS)) + .rules(stringsToRules(request.paramAsStrings(IssueFilterParameters.RULES))) + .actionPlans(request.paramAsStrings(IssueFilterParameters.ACTION_PLANS)) + .reporters(request.paramAsStrings(IssueFilterParameters.REPORTERS)) + .assignees(request.paramAsStrings(IssueFilterParameters.ASSIGNEES)) + .languages(request.paramAsStrings(IssueFilterParameters.LANGUAGES)) + .assigned(request.paramAsBoolean(IssueFilterParameters.ASSIGNED)) + .planned(request.paramAsBoolean(IssueFilterParameters.PLANNED)) + .hideRules(request.paramAsBoolean(IssueFilterParameters.HIDE_RULES)) + .createdAt(request.paramAsDateTime(IssueFilterParameters.CREATED_AT)) + .createdAfter(request.paramAsDateTime(IssueFilterParameters.CREATED_AFTER)) + .createdBefore(request.paramAsDateTime(IssueFilterParameters.CREATED_BEFORE)) + .pageSize(request.paramAsInt(IssueFilterParameters.PAGE_SIZE)) + .pageIndex(request.paramAsInt(IssueFilterParameters.PAGE_INDEX)); + String sort = request.param(IssueFilterParameters.SORT); + if (!Strings.isNullOrEmpty(sort)) { + builder.sort(sort); + builder.asc(request.paramAsBoolean(IssueFilterParameters.ASC)); + } + return builder.build(); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/search/Result.java b/server/sonar-server/src/main/java/org/sonar/server/search/Result.java index ba45bd11052..85a32c26cf4 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/search/Result.java +++ b/server/sonar-server/src/main/java/org/sonar/server/search/Result.java @@ -33,7 +33,7 @@ import javax.annotation.Nullable; import java.util.*; -public class Result<K extends BaseDoc> { +public class Result<K> { private final List<K> hits; private final Multimap<String, FacetValue> facets; |