]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-5530 - Using *Finders for context objects in IssueResult
authorStephane Gamard <stephane.gamard@sonarsource.com>
Wed, 10 Sep 2014 13:40:33 +0000 (15:40 +0200)
committerStephane Gamard <stephane.gamard@sonarsource.com>
Wed, 10 Sep 2014 13:40:55 +0000 (15:40 +0200)
12 files changed:
server/sonar-server/src/main/java/org/sonar/server/db/BaseDao.java
server/sonar-server/src/main/java/org/sonar/server/issue/IssueService.java
server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndex.java
server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueResult.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchAction.java
server/sonar-server/src/main/java/org/sonar/server/rule/RuleService.java
server/sonar-server/src/main/java/org/sonar/server/search/BaseIndex.java
server/sonar-server/src/main/java/org/sonar/server/search/QueryContext.java
server/sonar-server/src/main/java/org/sonar/server/search/ws/SearchRequestHandler.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/issue/IssueServiceMediumTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/db/IssueBackendMediumTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexMediumTest.java

index 12e48a0e914708332ecfcd5ecde70400031b5a99..a16f092c6d1bc6d8f133568b2cd44977858bc000 100644 (file)
@@ -155,12 +155,12 @@ public abstract class BaseDao<MAPPER, DTO extends Dto<KEY>, KEY extends Serializ
     return value;
   }
 
-  public Collection<DTO> getByKeys(DbSession session, KEY... keys) {
+  public List<DTO> getByKeys(DbSession session, KEY... keys) {
     return getByKeys(session, ImmutableList.<KEY>copyOf(keys));
   }
 
-  public Collection<DTO> getByKeys(DbSession session, Collection<KEY> keys) {
-    Collection<DTO> results = new ArrayList<DTO>();
+  public List<DTO> getByKeys(DbSession session, Collection<KEY> keys) {
+    List<DTO> results = new ArrayList<DTO>();
     for (KEY key : keys) {
       results.add(this.getByKey(session, key));
     }
@@ -306,7 +306,7 @@ public abstract class BaseDao<MAPPER, DTO extends Dto<KEY>, KEY extends Serializ
 
   // Synchronization methods
 
-  protected ResultHandler getSynchronizationResultHandler(final DbSession session){
+  protected ResultHandler getSynchronizationResultHandler(final DbSession session) {
     return new ResultHandler() {
       @Override
       public void handleResult(ResultContext resultContext) {
@@ -316,7 +316,7 @@ public abstract class BaseDao<MAPPER, DTO extends Dto<KEY>, KEY extends Serializ
     };
   }
 
-  protected Map getSynchronizationParams(Date date){
+  protected Map getSynchronizationParams(Date date) {
     Map<String, Object> params = newHashMap();
     params.put("date", new Timestamp(date.getTime()));
     return params;
index 4ab13b1b6f169a31890fd37dd04e836a5ac6e224..0337ce921749a17564557c83628e8c04d966cb05 100644 (file)
@@ -24,7 +24,6 @@ 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.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.sonar.api.ServerComponent;
@@ -40,7 +39,6 @@ import org.sonar.api.rules.Rule;
 import org.sonar.api.rules.RuleFinder;
 import org.sonar.api.user.User;
 import org.sonar.api.user.UserFinder;
-import org.sonar.api.utils.Paging;
 import org.sonar.api.web.UserRole;
 import org.sonar.core.issue.DefaultIssueBuilder;
 import org.sonar.core.issue.IssueNotifications;
@@ -59,15 +57,16 @@ 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.rule.index.RuleIndex;
 import org.sonar.server.search.IndexClient;
 import org.sonar.server.search.QueryContext;
 import org.sonar.server.user.UserSession;
 
 import javax.annotation.Nullable;
-
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
 
 /**
  * @since 3.6
@@ -335,46 +334,11 @@ public class IssueService implements ServerComponent {
     return indexClient.get(IssueIndex.class).getByKey(key);
   }
 
-  public IssueResult search(IssueQuery query, QueryContext options) {
+  public org.sonar.server.search.Result<Issue> search(IssueQuery query, QueryContext 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 result = new IssueResult(issueIndex, esResults);
-    result.setPaging(Paging.create(
-      options.getLimit(),
-      (options.getOffset() * options.getLimit()) + 1,
-      new Long(esResults.getHits().getTotalHits()).intValue()));
-
-    // Insert the projects and component name;
-    Set<RuleKey> ruleKeys = new HashSet<RuleKey>();
-    Set<String> projectKeys = new HashSet<String>();
-    Set<String> componentKeys = new HashSet<String>();
-    Set<String> actionPlanKeys = new HashSet<String>();
-    List<String> userLogins = new ArrayList<String>();
-
-    DbSession session = dbClient.openSession(false);
-    for (Issue issue : result.getHits()) {
-      ruleKeys.add(issue.ruleKey());
-      projectKeys.add(issue.projectKey());
-      componentKeys.add(issue.componentKey());
-      actionPlanKeys.add(issue.actionPlanKey());
-      userLogins.add(issue.authorLogin());
-    }
-
-    try {
-      // TODO Rule vs Rule problem
-      indexClient.get(RuleIndex.class).getByKeys(ruleKeys);
-      result.addProjects(dbClient.componentDao().getByKeys(session, projectKeys));
-      result.addComponents(dbClient.componentDao().getByKeys(session, componentKeys));
-      result.addUsers(userFinder.findByLogins(userLogins));
-      result.addActionPlans(actionPlanService.findByKeys(actionPlanKeys));
-    } finally {
-      session.close();
-    }
+    return issueIndex.search(query, options);
 
-    return result;
   }
 }
index a58b008a963dab75b3a289f733502b19f253cf9c..cd6535ea6bbd879a1da6a9249d9490b714da39a9 100644 (file)
@@ -28,6 +28,7 @@ import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.unit.TimeValue;
 import org.elasticsearch.index.query.*;
 import org.elasticsearch.search.aggregations.AggregationBuilders;
+import org.sonar.api.issue.Issue;
 import org.sonar.api.issue.IssueQuery;
 import org.sonar.api.web.UserRole;
 import org.sonar.core.issue.db.IssueDto;
@@ -39,7 +40,7 @@ import java.util.HashMap;
 import java.util.Map;
 import java.util.Set;
 
-public class IssueIndex extends BaseIndex<IssueDoc, IssueDto, String> {
+public class IssueIndex extends BaseIndex<Issue, IssueDto, String> {
 
   public IssueIndex(IssueNormalizer normalizer, SearchClient client) {
     super(IndexDefinition.ISSUES, normalizer, client);
@@ -108,7 +109,7 @@ public class IssueIndex extends BaseIndex<IssueDoc, IssueDto, String> {
     return new IssueDoc(fields);
   }
 
-  public SearchResponse search(IssueQuery query, QueryContext options) {
+  public Result<Issue> search(IssueQuery query, QueryContext options) {
 
     SearchRequestBuilder esSearch = getClient()
       .prepareSearch(this.getIndexName())
@@ -207,7 +208,8 @@ public class IssueIndex extends BaseIndex<IssueDoc, IssueDto, String> {
     // esSearch.addAggregation(AggregationBuilders.sum("totalDuration")
     // .field(IssueNormalizer.IssueField.DEBT.field()));
 
-    return getClient().execute(esSearch);
+    SearchResponse response = getClient().execute(esSearch);
+    return new Result<Issue>(this, response);
   }
 
   private void matchFilter(BoolFilterBuilder filter, IndexField field, Collection<?> values) {
index 66a7efe38a86a5f341701fe1fb67ff502bd91573..faef9948524056a1a9355e0dae1cce15286b8480 100644 (file)
@@ -39,6 +39,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+@Deprecated
 public class IssueResult extends Result<IssueDoc> implements IssueQueryResult {
 
   private final Map<String, Rule> rules;
@@ -145,7 +146,7 @@ public class IssueResult extends Result<IssueDoc> implements IssueQueryResult {
   public void addComponents(Collection<ComponentDto> components) {
     for (ComponentDto component : components) {
       this.components.put(component.key(), component);
- }
   }
   }
 
   public void addUsers(Collection<User> users) {
index f19c3248a8e9ea8de2bae8ff60232ef4c01b4130..82638f6dda67a9792a8f356f2134ce3d622a73e3 100644 (file)
  */
 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.Iterables;
 import com.google.common.io.Resources;
-import org.sonar.api.component.Component;
 import org.sonar.api.i18n.I18n;
-import org.sonar.api.issue.*;
+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.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.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.api.web.UserRole;
 import org.sonar.core.component.ComponentDto;
+import org.sonar.core.persistence.DbSession;
 import org.sonar.markdown.Markdown;
+import org.sonar.server.component.DefaultComponentFinder;
+import org.sonar.server.db.DbClient;
 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.IssueResult;
-import org.sonar.server.search.FacetValue;
+import org.sonar.server.rule.Rule;
+import org.sonar.server.rule.RuleService;
 import org.sonar.server.search.QueryContext;
 import org.sonar.server.search.Result;
-import org.sonar.server.search.ws.SearchOptions;
+import org.sonar.server.search.ws.SearchRequestHandler;
 import org.sonar.server.user.UserSession;
 
 import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
 
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 import static com.google.common.collect.Lists.newArrayList;
 
-public class SearchAction implements RequestHandler {
+public class SearchAction extends SearchRequestHandler<IssueQuery, Issue> {
 
   public static final String SEARCH_ACTION = "search";
 
@@ -70,45 +80,40 @@ public class SearchAction implements RequestHandler {
 
   private static final String EXTRA_FIELDS_PARAM = "extra_fields";
 
-  public static final String PARAM_FACETS = "facets";
-
   private final IssueService service;
   private final IssueActionsWriter actionsWriter;
+
+  private final RuleService ruleService;
+  private final DbClient dbClient;
+  private final DefaultComponentFinder componentFinder;
+  private final ActionPlanService actionPlanService;
+  private final UserFinder userFinder;
   private final I18n i18n;
   private final Durations durations;
 
-  public SearchAction(IssueService service, IssueActionsWriter actionsWriter, I18n i18n, Durations durations) {
+  public SearchAction(IssueService service, IssueActionsWriter actionsWriter, RuleService ruleService, DbClient dbClient, DefaultComponentFinder componentFinder,
+    ActionPlanService actionPlanService, UserFinder userFinder, I18n i18n, Durations durations) {
+    super(SEARCH_ACTION);
     this.service = service;
     this.actionsWriter = actionsWriter;
+    this.ruleService = ruleService;
+    this.dbClient = dbClient;
+    this.componentFinder = componentFinder;
+    this.actionPlanService = actionPlanService;
+    this.userFinder = userFinder;
     this.i18n = i18n;
     this.durations = durations;
   }
 
-  void define(WebService.NewController controller) {
-    WebService.NewAction action = controller.createAction(SEARCH_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)")
+  @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")
       .setHandler(this)
       .setResponseExample(Resources.getResource(this.getClass(), "example-search.json"));
 
-    // Add globalized search options. Will also support legacy params
-    // Generic search parameters
-    SearchOptions.definePageParams(action);
-    SearchOptions.defineFieldsParam(action, Collections.<String>emptyList());
-
-    // 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");
@@ -192,72 +197,114 @@ public class SearchAction implements RequestHandler {
   }
 
   @Override
-  public void handle(Request request, Response response) {
-    IssueQuery query = createQuery(request);
-    SearchOptions searchOptions = SearchOptions.create(request);
-    QueryContext queryContext = new QueryContext();
-    queryContext.setPage(searchOptions.page(), searchOptions.pageSize());
-    queryContext.setFacet(request.mandatoryParamAsBoolean(PARAM_FACETS));
+  protected IssueQuery doQuery(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();
+  }
 
-    IssueResult results = service.search(query, queryContext);
+  @Override
+  protected Result<Issue> doSearch(IssueQuery query, QueryContext context) {
+    return service.search(query, context);
+  }
 
-    JsonWriter json = response.newJsonWriter();
-    json.beginObject();
+  @Override
+  protected void doResultResponse(Request request, QueryContext context, Result<Issue> result, JsonWriter json) {
+    writeIssues(result, request.paramAsStrings(EXTRA_FIELDS_PARAM), json);
+  }
 
-    writePaging(results, json);
-    writeIssues(results, request.paramAsStrings(EXTRA_FIELDS_PARAM), json);
+  @Override
+  protected void doContextResponse(Request request, QueryContext context, Result<Issue> result, JsonWriter json) {
+
+    // Insert the projects and component name;
+    Set<RuleKey> ruleKeys = new HashSet<RuleKey>();
+    Set<String> projectKeys = new HashSet<String>();
+    Set<String> componentKeys = new HashSet<String>();
+    Set<String> actionPlanKeys = new HashSet<String>();
+    List<String> userLogins = new ArrayList<String>();
+    //
+    // DbSession session = dbClient.openSession(false);
+    for (Issue issue : result.getHits()) {
+      ruleKeys.add(issue.ruleKey());
+      projectKeys.add(issue.projectKey());
+      componentKeys.add(issue.componentKey());
+      actionPlanKeys.add(issue.actionPlanKey());
+      userLogins.add(issue.authorLogin());
+    }
 
-    // TODO normalize component name for snippet -- Mighty change over time (file move)
-    writeComponents(results, json);
-    // TODO normalize project Name for snippet -- Carefull might change over time (Project Rename)
-    writeProjects(results, json);
+    writeRules(json, ruleService.getByKeys(ruleKeys));
+    writeUsers(json, userFinder.findByLogins(userLogins));
+    writeActionPlans(json, actionPlanService.findByKeys(actionPlanKeys));
 
-    // TODO Not certain that this is required (Legacy)
-    // writeRules(results, json);
-    // writeUsers(results, json);
-    // writeActionPlans(results, json);
+    DbSession session = dbClient.openSession(false);
+    try {
+      writeProjects(json, dbClient.componentDao().getByKeys(session, projectKeys));
+      writeComponents(json, dbClient.componentDao().getByKeys(session, componentKeys));
+    } finally {
+      session.close();
+   }
 
-    if (queryContext.isFacet()) {
-      writeFacets(results, json);
-    }
+    // TODO remove legacy paging. Handled by the SearchRequestHandler
+    writeLegacyPaging(context, json, result);
+  }
 
-    json.endObject().close();
+  private void writeLegacyPaging(QueryContext context, JsonWriter json, Result<?> result) {
+    // TODO remove with stas on HTML side
+    json.prop("maxResultsReached", false);
+   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", Math.ceil(result.getTotal() / (context.getLimit() * 1.0)))
+     .endObject();
   }
 
-  private void writeFacets(Result<?> results, JsonWriter json) {
-    json.name("facets").beginArray();
-    for (Map.Entry<String, Collection<FacetValue>> facet : results.getFacets().entrySet()) {
-      json.beginObject();
-      json.prop("property", facet.getKey());
-      json.name("values").beginArray();
-      for (FacetValue facetValue : facet.getValue()) {
-        json.beginObject();
-        json.prop("val", facetValue.getKey());
-        json.prop("count", facetValue.getValue());
-        json.endObject();
-      }
-      json.endArray().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("desc", rule.htmlDescription())
+        .prop("status", rule.status().toString())
+        .endObject();
     }
     json.endArray();
   }
 
-  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())
-      // TODO Remove as part of Front-end rework on Issue Domain
-      .prop("fTotal", i18n.formatInteger(UserSession.get().locale(), result.paging().total()))
-      .prop("pages", result.paging().pages())
-      .endObject();
-  }
+  private void writeIssues(Result<Issue> result, @Nullable List<String> extraFields, JsonWriter json) {
+  json.name("issues").beginArray();
 
-  private void writeIssues(IssueQueryResult result, @Nullable List<String> extraFields, JsonWriter json) {
-    json.name("issues").beginArray();
-
-    for (Issue issue : result.issues()) {
-      json.beginObject();
+    for (Issue issue : result.getHits()) {
+   json.beginObject();
 
       String actionPlanKey = issue.actionPlanKey();
       Duration debt = issue.debt();
@@ -284,9 +331,11 @@ public class SearchAction implements RequestHandler {
         .prop("fUpdateAge", formatAgeDate(updateDate))
         .prop("closeDate", isoDate(issue.closeDate()));
 
-      writeIssueComments(result, issue, json);
+      // TODO add comments
+      // writeIssueComments(result, issue, json);
       writeIssueAttributes(issue, json);
-      writeIssueExtraFields(result, issue, extraFields, json);
+      // TODO Add fields
+      // writeIssueExtraFields(result, issue, extraFields, json);
       json.endObject();
     }
 
@@ -354,32 +403,30 @@ public class SearchAction implements RequestHandler {
     }
   }
 
-  private void writeComponents(IssueQueryResult result, JsonWriter json) {
+  private void writeComponents(JsonWriter json, List<ComponentDto> components) {
     json.name("components").beginArray();
-    for (Component component : result.components()) {
-      ComponentDto componentDto = (ComponentDto) component;
+    for (ComponentDto component : components) {
       json.beginObject()
         .prop("key", component.key())
-        .prop("id", componentDto.getId())
-        .prop("qualifier", component.qualifier())
+        .prop("id", component.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())
+        .prop("projectId", (component.projectId() != null && component.subProjectId() != null) ? component.projectId() : null)
+        .prop("subProjectId", component.subProjectId())
         .endObject();
     }
     json.endArray();
   }
 
-  private void writeProjects(IssueQueryResult result, JsonWriter json) {
+  private void writeProjects(JsonWriter json, List<ComponentDto> projects) {
     json.name("projects").beginArray();
-    for (Component project : result.projects()) {
-      ComponentDto componentDto = (ComponentDto) project;
+    for (ComponentDto project : projects) {
       json.beginObject()
         .prop("key", project.key())
-        .prop("id", componentDto.getId())
+        .prop("id", project.getId())
         .prop("qualifier", project.qualifier())
         .prop("name", project.name())
         .prop("longName", project.longName())
@@ -388,22 +435,9 @@ public class SearchAction implements RequestHandler {
     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) {
+  private void writeUsers(JsonWriter json, List<User> users) {
     json.name("users").beginArray();
-    for (User user : result.users()) {
+    for (User user : users) {
       json.beginObject()
         .prop("login", user.login())
         .prop("name", user.name())
@@ -414,10 +448,10 @@ public class SearchAction implements RequestHandler {
     json.endArray();
   }
 
-  private void writeActionPlans(IssueQueryResult result, JsonWriter json) {
-    if (!result.actionPlans().isEmpty()) {
+  private void writeActionPlans(JsonWriter json, List<ActionPlan> plans) {
+    if (!plans.isEmpty()) {
       json.name("actionPlans").beginArray();
-      for (ActionPlan actionPlan : result.actionPlans()) {
+      for (ActionPlan actionPlan : plans) {
         Date deadLine = actionPlan.deadLine();
         Date updatedAt = actionPlan.updatedAt();
 
@@ -475,36 +509,4 @@ public class SearchAction implements RequestHandler {
     }
     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();
-  }
 }
index 85fab0b327a2d39e604dfcf8679a86043f6f0e7c..45850dd4dcd94afb1963a64ad4bf184f337bf07f 100644 (file)
@@ -32,6 +32,8 @@ import org.sonar.server.user.UserSession;
 
 import javax.annotation.CheckForNull;
 
+import java.util.Collection;
+import java.util.List;
 import java.util.Set;
 
 /**
@@ -56,6 +58,10 @@ public class RuleService implements ServerComponent {
     return index.getByKey(key);
   }
 
+  public List<Rule> getByKeys(Collection<RuleKey> keys) {
+    return index.getByKeys(keys);
+  }
+
   public Rule getNonNullByKey(RuleKey key) {
     Rule rule = index.getByKey(key);
     if (rule == null) {
index 948b5e8d8f816ba37397943a22f2d5854f6c4cb2..64a11932dfa7ff8c39b0bd961e9259f5c5fc659d 100644 (file)
@@ -401,7 +401,7 @@ public abstract class BaseIndex<DOMAIN, DTO extends Dto<KEY>, KEY extends Serial
     return null;
   }
 
-  public Collection<DOMAIN> getByKeys(Collection<KEY> keys) {
+  public List<DOMAIN> getByKeys(Collection<KEY> keys) {
     List<DOMAIN> results = new ArrayList<DOMAIN>();
     MultiGetRequestBuilder request = client.prepareMultiGet()
       .setPreference("_local");
index a8dda8755d4c1a855a0d0448bbded1af0a98baba..b2cfae1854bcb450db77d0b612ad4b7a79554eb9 100644 (file)
@@ -112,6 +112,11 @@ public class QueryContext {
     return this;
   }
 
+  public int getPage() {
+    double page = new Double(getOffset() + 1) / new Double(getLimit());
+    return (int) Math.ceil(page);
+  }
+
   /**
    * Limit on the number of results to return. Defaults to {@link #DEFAULT_LIMIT}.
    */
@@ -162,5 +167,4 @@ public class QueryContext {
   public Set<String> getUserGroups() {
     return userGroups;
   }
-
 }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/search/ws/SearchRequestHandler.java b/server/sonar-server/src/main/java/org/sonar/server/search/ws/SearchRequestHandler.java
new file mode 100644 (file)
index 0000000..52b8983
--- /dev/null
@@ -0,0 +1,156 @@
+/*
+ * 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.search.ws;
+
+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.utils.text.JsonWriter;
+import org.sonar.server.search.FacetValue;
+import org.sonar.server.search.QueryContext;
+import org.sonar.server.search.Result;
+
+import javax.annotation.CheckForNull;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+public abstract class SearchRequestHandler<QUERY, DOMAIN> implements RequestHandler {
+
+  public static final String PARAM_TEXT_QUERY = "q";
+  public static final String PARAM_PAGE = "p";
+  public static final String PARAM_PAGE_SIZE = "ps";
+  public static final String PARAM_FIELDS = "f";
+  public static final String PARAM_SORT = "s";
+  public static final String PARAM_ASCENDING = "asc";
+
+  public static final String PARAM_FACETS = "facets";
+
+  private int pageSize;
+  private int page;
+  private List<String> fields;
+
+  private final String actionName;
+
+  /**
+   * The fields to be returned in JSON response. <code>null</code> means that
+   * all the fields must be returned.
+   */
+  @CheckForNull
+  public List<String> fields() {
+    return fields;
+  }
+
+  protected SearchRequestHandler(String actionName) {
+    this.actionName = actionName;
+  }
+
+  protected abstract Result<DOMAIN> doSearch(QUERY query, QueryContext context);
+
+  protected abstract QUERY doQuery(Request request);
+
+  protected abstract void doResultResponse(Request request, QueryContext context, Result<DOMAIN> result, JsonWriter json);
+
+  protected abstract void doContextResponse(Request request, QueryContext context, Result<DOMAIN> result, JsonWriter json);
+
+  protected abstract void doDefinition(WebService.NewAction action);
+
+  public final void define(WebService.NewController controller) {
+    WebService.NewAction action = controller.createAction(this.actionName);
+
+    action
+      .createParam(PARAM_PAGE)
+      .setDescription("1-based page number")
+      .setExampleValue("42")
+      .setDefaultValue("1");
+
+    action
+      .createParam(PARAM_PAGE_SIZE)
+      .setDescription("Page size. Must be greater than 0.")
+      .setExampleValue("20")
+      .setDefaultValue(String.valueOf(QueryContext.DEFAULT_LIMIT));
+
+    action.createParam(PARAM_FACETS)
+      .setDescription("Compute predefined facets")
+      .setBooleanPossibleValues()
+      .setDefaultValue("false");
+
+    WebService.NewParam newParam = action
+      .createParam(PARAM_FIELDS)
+      .setDescription("Comma-separated list of the fields to be returned in response. All the fields are returned by default.");
+
+    // .setPossibleValues(possibleFields);
+    // if (possibleFields != null && possibleFields.size() > 1) {
+    // Iterator<String> it = possibleFields.iterator();
+    // newParam.setExampleValue(String.format("%s,%s", it.next(), it.next()));
+    // }
+
+    this.doDefinition(action);
+  }
+
+  @Override
+  public final void handle(Request request, Response response) throws Exception {
+    QueryContext context = getQueryContext(request);
+    QUERY query = doQuery(request);
+    Result<DOMAIN> result = doSearch(query, context);
+
+    JsonWriter json = response.newJsonWriter().beginObject();
+    this.writeStatistics(json, result);
+    doResultResponse(request, context, result, json);
+    doContextResponse(request, context, result, json);
+    if (context.isFacet()) {
+      writeFacets(result, json);
+    }
+    json.endObject().close();
+  }
+
+  private final QueryContext getQueryContext(Request request) {
+    return new QueryContext()
+      .addFieldsToReturn(request.paramAsStrings(PARAM_FIELDS))
+      .setFacet(request.paramAsBoolean(PARAM_FACETS))
+      .setPage(request.mandatoryParamAsInt(PARAM_PAGE),
+        request.mandatoryParamAsInt(PARAM_PAGE_SIZE));
+  }
+
+  protected void writeStatistics(JsonWriter json, Result searchResult) {
+    json.prop("total", searchResult.getTotal());
+    json.prop(PARAM_PAGE, page);
+    json.prop(PARAM_PAGE_SIZE, pageSize);
+  }
+
+  protected void writeFacets(Result<?> results, JsonWriter json) {
+    json.name("facets").beginArray();
+    for (Map.Entry<String, Collection<FacetValue>> facet : results.getFacets().entrySet()) {
+      json.beginObject();
+      json.prop("property", facet.getKey());
+      json.name("values").beginArray();
+      for (FacetValue facetValue : facet.getValue()) {
+        json.beginObject();
+        json.prop("val", facetValue.getKey());
+        json.prop("count", facetValue.getValue());
+        json.endObject();
+      }
+      json.endArray().endObject();
+    }
+    json.endArray();
+  }
+
+}
index 40813271558fad2b824e90ab7afd8551b35eeadb..b6fb6e7dc62e0ae1bd05ec029c56128d9ce94878 100644 (file)
@@ -23,6 +23,7 @@ import org.junit.After;
 import org.junit.Before;
 import org.junit.ClassRule;
 import org.junit.Test;
+import org.sonar.api.issue.Issue;
 import org.sonar.api.issue.IssueQuery;
 import org.sonar.api.security.DefaultGroups;
 import org.sonar.api.utils.DateUtils;
@@ -35,7 +36,6 @@ import org.sonar.core.rule.RuleDto;
 import org.sonar.server.component.persistence.ComponentDao;
 import org.sonar.server.db.DbClient;
 import org.sonar.server.issue.db.IssueDao;
-import org.sonar.server.issue.index.IssueResult;
 import org.sonar.server.rule.RuleTesting;
 import org.sonar.server.rule.db.RuleDao;
 import org.sonar.server.search.QueryContext;
@@ -102,7 +102,7 @@ public class IssueServiceMediumTest {
     tester.get(IssueDao.class).insert(session, issue1, issue2);
     session.commit();
 
-    IssueResult result = service.search(IssueQuery.builder().build(), new QueryContext());
+    org.sonar.server.search.Result<Issue> result = service.search(IssueQuery.builder().build(), new QueryContext());
     assertThat(result.getHits()).hasSize(2);
     assertThat(result.getFacets()).isEmpty();
 
@@ -111,18 +111,6 @@ public class IssueServiceMediumTest {
     assertThat(result.getFacetKeys("actionPlan")).hasSize(2);
   }
 
-  @Test
-  public void has_component_and_project() throws Exception {
-    IssueDto issue1 = getIssue().setActionPlanKey("P1");
-    IssueDto issue2 = getIssue().setActionPlanKey("P2");
-    tester.get(IssueDao.class).insert(session, issue1, issue2);
-    session.commit();
-
-    IssueResult result = service.search(IssueQuery.builder().build(), new QueryContext());
-    assertThat(result.projects()).hasSize(1);
-    assertThat(result.components()).hasSize(1);
-  }
-
   private IssueDto getIssue() {
     return new IssueDto()
       .setIssueCreationDate(DateUtils.parseDate("2014-09-04"))
index 19837277ed6a71da62266320c133090cd0321948..0d667e41a2e3722a0bb4c50ae668733af2188db1 100644 (file)
@@ -24,6 +24,7 @@ import org.junit.After;
 import org.junit.Before;
 import org.junit.ClassRule;
 import org.junit.Test;
+import org.sonar.api.issue.Issue;
 import org.sonar.api.rule.RuleKey;
 import org.sonar.core.component.ComponentDto;
 import org.sonar.core.issue.db.IssueDto;
@@ -31,7 +32,6 @@ import org.sonar.core.persistence.DbSession;
 import org.sonar.core.rule.RuleDto;
 import org.sonar.server.component.persistence.ComponentDao;
 import org.sonar.server.db.DbClient;
-import org.sonar.server.issue.index.IssueDoc;
 import org.sonar.server.issue.index.IssueIndex;
 import org.sonar.server.platform.Platform;
 import org.sonar.server.rule.RuleTesting;
@@ -104,7 +104,7 @@ public class IssueBackendMediumTest {
     assertThat(indexClient.get(IssueIndex.class).countAll()).isEqualTo(1);
 
     // should find by key
-    IssueDoc issueDoc = indexClient.get(IssueIndex.class).getByKey(issue.getKey());
+    Issue issueDoc = indexClient.get(IssueIndex.class).getByKey(issue.getKey());
     assertThat(issueDoc).isNotNull();
 
     // Check all normalized fields
index 7a52af104c4fb7c1f1120be83d931406aa8c7441..cf8058d72cd628d844c6c1a86589a4feae012a1a 100644 (file)
 package org.sonar.server.issue.index;
 
 import com.google.common.collect.ImmutableList;
-import org.elasticsearch.action.search.SearchResponse;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.ClassRule;
 import org.junit.Test;
+import org.sonar.api.issue.Issue;
 import org.sonar.api.issue.IssueQuery;
 import org.sonar.api.security.DefaultGroups;
 import org.sonar.api.utils.DateUtils;
@@ -41,6 +41,7 @@ import org.sonar.server.db.DbClient;
 import org.sonar.server.rule.RuleTesting;
 import org.sonar.server.rule.db.RuleDao;
 import org.sonar.server.search.QueryContext;
+import org.sonar.server.search.Result;
 import org.sonar.server.tester.ServerTester;
 import org.sonar.server.user.MockUserSession;
 
@@ -112,19 +113,19 @@ public class IssueIndexMediumTest {
 
     IssueQuery.Builder query = IssueQuery.builder();
     query.actionPlans(ImmutableList.of(plan1));
-    SearchResponse result = index.search(query.build(), new QueryContext());
+    Result<Issue> result = index.search(query.build(), new QueryContext());
 
-    assertThat(result.getHits().getTotalHits()).isEqualTo(1L);
+    assertThat(result.getHits()).hasSize(1);
 
     query = IssueQuery.builder();
     query.actionPlans(ImmutableList.of(plan2));
     result = index.search(query.build(), new QueryContext());
-    assertThat(result.getHits().getTotalHits()).isEqualTo(1L);
+    assertThat(result.getHits()).hasSize(1);
 
     query = IssueQuery.builder();
     query.actionPlans(ImmutableList.of(plan2, plan1));
     result = index.search(query.build(), new QueryContext());
-    assertThat(result.getHits().getTotalHits()).isEqualTo(2L);
+    assertThat(result.getHits()).hasSize(2);
   }
 
   @Test
@@ -137,13 +138,13 @@ public class IssueIndexMediumTest {
     session.commit();
 
     IssueQuery.Builder query = IssueQuery.builder();
-    SearchResponse result = index.search(query.build(), new QueryContext());
-    assertThat(result.getHits().getTotalHits()).isEqualTo(2L);
+    Result<Issue> result = index.search(query.build(), new QueryContext());
+    assertThat(result.getHits()).hasSize(2);
 
     query = IssueQuery.builder();
     query.assigned(true);
     result = index.search(query.build(), new QueryContext());
-    assertThat(result.getHits().getTotalHits()).isEqualTo(1L);
+    assertThat(result.getHits()).hasSize(1);
   }
 
   @Test
@@ -159,18 +160,18 @@ public class IssueIndexMediumTest {
     session.commit();
 
     IssueQuery.Builder query = IssueQuery.builder();
-    SearchResponse result = index.search(query.build(), new QueryContext());
-    assertThat(result.getHits().getTotalHits()).isEqualTo(3L);
+    Result<Issue> result = index.search(query.build(), new QueryContext());
+    assertThat(result.getHits()).hasSize(3);
 
     query = IssueQuery.builder();
     query.assignees(ImmutableList.of(assignee1));
     result = index.search(query.build(), new QueryContext());
-    assertThat(result.getHits().getTotalHits()).isEqualTo(1L);
+    assertThat(result.getHits()).hasSize(1);
 
     query = IssueQuery.builder();
     query.assignees(ImmutableList.of(assignee1, assignee2));
     result = index.search(query.build(), new QueryContext());
-    assertThat(result.getHits().getTotalHits()).isEqualTo(2L);
+    assertThat(result.getHits()).hasSize(2);
   }
 
   @Test
@@ -197,7 +198,6 @@ public class IssueIndexMediumTest {
     db.groupDao().insert(session, groupDto);
     tester.get(PermissionFacade.class).insertGroupPermission(project2.getId(), groupDto.getName(), UserRole.USER, session);
 
-
     db.issueAuthorizationDao().synchronizeAfter(session, new Date(0));
 
     session.commit();
@@ -206,16 +206,16 @@ public class IssueIndexMediumTest {
     IssueQuery.Builder query = IssueQuery.builder();
 
     MockUserSession.set().setUserGroups("sonar-users");
-    assertThat(index.search(query.build(), new QueryContext()).getHits().getTotalHits()).isEqualTo(1L);
+    assertThat(index.search(query.build(), new QueryContext()).getHits()).hasSize(1);
 
     MockUserSession.set().setUserGroups("sonar-admins");
-    assertThat(index.search(query.build(), new QueryContext()).getHits().getTotalHits()).isEqualTo(1L);
+    assertThat(index.search(query.build(), new QueryContext()).getHits()).hasSize(1);
 
     MockUserSession.set().setUserGroups("sonar-users", "sonar-admins");
-    assertThat(index.search(query.build(), new QueryContext()).getHits().getTotalHits()).isEqualTo(2L);
+    assertThat(index.search(query.build(), new QueryContext()).getHits()).hasSize(2);
 
     MockUserSession.set().setUserGroups("another group");
-    assertThat(index.search(query.build(), new QueryContext()).getHits().getTotalHits()).isEqualTo(0);
+    assertThat(index.search(query.build(), new QueryContext()).getHits()).hasSize(0);
   }
 
   @Test
@@ -250,13 +250,13 @@ public class IssueIndexMediumTest {
     IssueQuery.Builder query = IssueQuery.builder();
 
     MockUserSession.set().setLogin("john");
-    assertThat(index.search(query.build(), new QueryContext()).getHits().getTotalHits()).isEqualTo(1L);
+    assertThat(index.search(query.build(), new QueryContext()).getHits()).hasSize(1);
 
     MockUserSession.set().setLogin("max");
-    assertThat(index.search(query.build(), new QueryContext()).getHits().getTotalHits()).isEqualTo(1L);
+    assertThat(index.search(query.build(), new QueryContext()).getHits()).hasSize(1);
 
     MockUserSession.set().setLogin("another guy");
-    assertThat(index.search(query.build(), new QueryContext()).getHits().getTotalHits()).isEqualTo(0L);
+    assertThat(index.search(query.build(), new QueryContext()).getHits()).hasSize(0);
   }
 
   @Test
@@ -291,7 +291,7 @@ public class IssueIndexMediumTest {
     IssueQuery.Builder query = IssueQuery.builder();
 
     MockUserSession.set().setLogin("john").setUserGroups("sonar-users");
-    assertThat(index.search(query.build(), new QueryContext()).getHits().getTotalHits()).isEqualTo(1L);
+    assertThat(index.search(query.build(), new QueryContext()).getHits()).hasSize(1);
   }
 
   private IssueDto createIssue() {