Browse Source

SONAR-5531 - Wrapped IssueIndex with WS SearchAction

tags/5.0-RC1
Stephane Gamard 9 years ago
parent
commit
0b4912da7d

+ 20
- 0
server/sonar-server/src/main/java/org/sonar/server/issue/IssueService.java View File

@@ -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;
}
}

+ 13
- 0
server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndex.java View File

@@ -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()));
}
}

+ 11
- 0
server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueMapping.java View File

@@ -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 {

}

+ 2
- 2
server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueResult.java View File

@@ -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

+ 346
- 5
server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchAction.java View File

@@ -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();
}
}

+ 1
- 1
server/sonar-server/src/main/java/org/sonar/server/search/Result.java View File

@@ -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;

Loading…
Cancel
Save