]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6615 ws custom_measures/search search for custom measures 399/head
authorTeryk Bellahsene <teryk.bellahsene@sonarsource.com>
Thu, 25 Jun 2015 16:58:09 +0000 (18:58 +0200)
committerTeryk Bellahsene <teryk.bellahsene@sonarsource.com>
Fri, 26 Jun 2015 07:40:24 +0000 (09:40 +0200)
19 files changed:
server/sonar-server/src/main/java/org/sonar/server/measure/custom/persistence/CustomMeasureDao.java
server/sonar-server/src/main/java/org/sonar/server/measure/custom/ws/CreateAction.java
server/sonar-server/src/main/java/org/sonar/server/measure/custom/ws/CustomMeasureJsonWriter.java
server/sonar-server/src/main/java/org/sonar/server/measure/custom/ws/CustomMeasuresWsModule.java
server/sonar-server/src/main/java/org/sonar/server/measure/custom/ws/SearchAction.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/measure/custom/ws/UpdateAction.java
server/sonar-server/src/main/java/org/sonar/server/metric/persistence/MetricDao.java
server/sonar-server/src/main/java/org/sonar/server/metric/ws/MetricJsonWriter.java
server/sonar-server/src/main/java/org/sonar/server/ws/JsonWriterUtils.java
server/sonar-server/src/main/resources/org/sonar/server/measure/custom/ws/example-search.json [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/CustomMeasuresWsModuleTest.java
server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/MetricsActionTest.java
server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/SearchActionTest.java [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/measure/custom/ws/SearchActionTest/empty.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/measure/custom/ws/SearchActionTest/metrics.json [new file with mode: 0644]
sonar-core/src/main/java/org/sonar/core/measure/custom/db/CustomMeasureMapper.java
sonar-core/src/main/java/org/sonar/core/metric/db/MetricMapper.java
sonar-core/src/main/resources/org/sonar/core/measure/custom/db/CustomMeasureMapper.xml
sonar-core/src/main/resources/org/sonar/core/metric/db/MetricMapper.xml

index 6a454ee272038833b70b1e5daf296bc077ace573..7f660ea5e5d17a0c3533f0729182ee963e5dfdc3 100644 (file)
@@ -24,12 +24,14 @@ import com.google.common.base.Function;
 import java.util.List;
 import javax.annotation.CheckForNull;
 import javax.annotation.Nonnull;
+import org.apache.ibatis.session.RowBounds;
 import org.sonar.api.server.ServerSide;
 import org.sonar.core.measure.custom.db.CustomMeasureDto;
 import org.sonar.core.measure.custom.db.CustomMeasureMapper;
 import org.sonar.core.persistence.DaoComponent;
 import org.sonar.core.persistence.DaoUtils;
 import org.sonar.core.persistence.DbSession;
+import org.sonar.server.es.SearchOptions;
 import org.sonar.server.exceptions.NotFoundException;
 
 @ServerSide
@@ -77,6 +79,10 @@ public class CustomMeasureDao implements DaoComponent {
     return mapper(session).countByComponentIdAndMetricId(componentUuid, metricId);
   }
 
+  public List<CustomMeasureDto> selectByComponentUuid(DbSession session, String componentUuid, SearchOptions searchOptions) {
+    return mapper(session).selectByComponentUuid(componentUuid, new RowBounds(searchOptions.getOffset(), searchOptions.getLimit()));
+  }
+
   public List<CustomMeasureDto> selectByComponentUuid(DbSession session, String componentUuid) {
     return mapper(session).selectByComponentUuid(componentUuid);
   }
@@ -84,4 +90,8 @@ public class CustomMeasureDao implements DaoComponent {
   private CustomMeasureMapper mapper(DbSession session) {
     return session.getMapper(CustomMeasureMapper.class);
   }
+
+  public int countByComponentUuid(DbSession dbSession, String uuid) {
+    return mapper(dbSession).countByComponentUuid(uuid);
+  }
 }
index 7618cd5d41a39c735757fbd53d81849170d511a8..a809a623028397f217adf74e8af75bb73342578b 100644 (file)
@@ -132,7 +132,7 @@ public class CreateAction implements CustomMeasuresWsAction {
       dbSession.commit();
 
       JsonWriter json = response.newJsonWriter();
-      customMeasureJsonWriter.write(json, measure, metric, component, user);
+      customMeasureJsonWriter.write(json, measure, metric, component, user, CustomMeasureJsonWriter.OPTIONAL_FIELDS);
       json.close();
     } finally {
       MyBatis.closeQuietly(dbSession);
index f2b9f2618bc76728cd284b1a84246baff2efd627..026d0cb9fa0daa6b6be7aacaf81cf46e2273e7fb 100644 (file)
 
 package org.sonar.server.measure.custom.ws;
 
+import com.google.common.collect.ImmutableSet;
+import java.util.Collection;
 import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import org.sonar.api.measures.Metric;
-import org.sonar.api.server.ServerSide;
 import org.sonar.api.user.User;
 import org.sonar.api.utils.text.JsonWriter;
 import org.sonar.core.component.ComponentDto;
 import org.sonar.core.measure.custom.db.CustomMeasureDto;
 import org.sonar.core.metric.db.MetricDto;
+import org.sonar.server.metric.ws.MetricJsonWriter;
 import org.sonar.server.user.ws.UserJsonWriter;
 
-import static org.sonar.server.measure.custom.ws.CreateAction.PARAM_DESCRIPTION;
-import static org.sonar.server.measure.custom.ws.CreateAction.PARAM_PROJECT_ID;
-import static org.sonar.server.measure.custom.ws.CreateAction.PARAM_PROJECT_KEY;
-import static org.sonar.server.measure.custom.ws.CreateAction.PARAM_VALUE;
+import static org.sonar.server.ws.JsonWriterUtils.isFieldNeeded;
+import static org.sonar.server.ws.JsonWriterUtils.writeIfNeeded;
 
-@ServerSide
 public class CustomMeasureJsonWriter {
   private static final String FIELD_ID = "id";
-  private static final String FIELD_PROJECT_ID = PARAM_PROJECT_ID;
-  private static final String FIELD_PROJECT_KEY = PARAM_PROJECT_KEY;
-  private static final String FIELD_VALUE = PARAM_VALUE;
-  private static final String FIELD_DESCRIPTION = PARAM_DESCRIPTION;
+  private static final String FIELD_PROJECT_ID = "projectId";
+  private static final String FIELD_PROJECT_KEY = "projectKey";
+  private static final String FIELD_VALUE = "value";
+  private static final String FIELD_DESCRIPTION = "description";
   private static final String FIELD_METRIC = "metric";
-  private static final String FIELD_METRIC_KEY = "key";
-  private static final String FIELD_METRIC_ID = "id";
-  private static final String FIELD_METRIC_TYPE = "type";
   private static final String FIELD_CREATED_AT = "createdAt";
   private static final String FIELD_UPDATED_AT = "updatedAt";
   private static final String FIELD_USER = "user";
 
+  public static final Set<String> OPTIONAL_FIELDS = ImmutableSet.of(FIELD_PROJECT_ID, FIELD_PROJECT_KEY, FIELD_VALUE, FIELD_DESCRIPTION, FIELD_METRIC, FIELD_CREATED_AT,
+    FIELD_UPDATED_AT, FIELD_USER);
+
   private final UserJsonWriter userJsonWriter;
 
   public CustomMeasureJsonWriter(UserJsonWriter userJsonWriter) {
     this.userJsonWriter = userJsonWriter;
   }
 
-  public void write(JsonWriter json, CustomMeasureDto measure, MetricDto metric, ComponentDto component, User user) {
+  public void write(JsonWriter json, CustomMeasureDto measure, MetricDto metric, ComponentDto component, User user, Collection<String> fieldsToReturn) {
     json.beginObject();
     json.prop(FIELD_ID, String.valueOf(measure.getId()));
-    json.name(FIELD_METRIC);
-    writeMetric(json, metric);
-    json.prop(FIELD_PROJECT_ID, component.uuid());
-    json.prop(FIELD_PROJECT_KEY, component.key());
-    json.prop(FIELD_DESCRIPTION, measure.getDescription());
-    json.prop(FIELD_VALUE, measureValue(measure, metric));
-    json.propDateTime(FIELD_CREATED_AT, new Date(measure.getCreatedAt()));
-    json.propDateTime(FIELD_UPDATED_AT, new Date(measure.getUpdatedAt()));
-    json.name(FIELD_USER);
-    userJsonWriter.write(json, user);
+    writeIfNeeded(json, measureValue(measure, metric), FIELD_VALUE, fieldsToReturn);
+    writeIfNeeded(json, measure.getDescription(), FIELD_DESCRIPTION, fieldsToReturn);
+    if (isFieldNeeded(FIELD_METRIC, fieldsToReturn)) {
+      json.name(FIELD_METRIC);
+      MetricJsonWriter.write(json, metric, MetricJsonWriter.MANDATORY_FIELDS);
+    }
+    writeIfNeeded(json, component.uuid(), FIELD_PROJECT_ID, fieldsToReturn);
+    writeIfNeeded(json, component.key(), FIELD_PROJECT_KEY, fieldsToReturn);
+    writeIfNeeded(json, new Date(measure.getCreatedAt()), FIELD_CREATED_AT, fieldsToReturn);
+    writeIfNeeded(json, new Date(measure.getUpdatedAt()), FIELD_UPDATED_AT, fieldsToReturn);
+
+    if (isFieldNeeded(FIELD_USER, fieldsToReturn)) {
+      json.name(FIELD_USER);
+      userJsonWriter.write(json, user);
+    }
+
     json.endObject();
   }
 
@@ -99,12 +107,13 @@ public class CustomMeasureJsonWriter {
     }
   }
 
-  private static void writeMetric(JsonWriter json, MetricDto metric) {
-    json.beginObject();
-    json.prop(FIELD_METRIC_ID, String.valueOf(metric.getId()));
-    json.prop(FIELD_METRIC_KEY, metric.getKey());
-    json.prop(FIELD_METRIC_TYPE, metric.getValueType());
-    json.endObject();
+  public void write(JsonWriter json, List<CustomMeasureDto> customMeasures, ComponentDto project, Map<Integer, MetricDto> metricsById, Map<String, User> usersByLogin,
+    Collection<String> fieldsToReturn) {
+    json.name("customMeasures");
+    json.beginArray();
+    for (CustomMeasureDto customMeasure : customMeasures) {
+      write(json, customMeasure, metricsById.get(customMeasure.getMetricId()), project, usersByLogin.get(customMeasure.getUserLogin()), fieldsToReturn);
+    }
+    json.endArray();
   }
-
 }
index 136f332dcfe6e2c702e959f00102019574bcd18b..1b40340d5a523d16e513e11ecee28c89e8645c72 100644 (file)
@@ -30,6 +30,7 @@ public class CustomMeasuresWsModule extends Module {
       CreateAction.class,
       UpdateAction.class,
       DeleteAction.class,
+      SearchAction.class,
       MetricsAction.class,
       CustomMeasureJsonWriter.class,
       CustomMeasureValidator.class);
diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/custom/ws/SearchAction.java b/server/sonar-server/src/main/java/org/sonar/server/measure/custom/ws/SearchAction.java
new file mode 100644 (file)
index 0000000..02f3342
--- /dev/null
@@ -0,0 +1,180 @@
+/*
+ * 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.measure.custom.ws;
+
+import com.google.common.base.Function;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import org.sonar.api.server.ws.Request;
+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.text.JsonWriter;
+import org.sonar.core.component.ComponentDto;
+import org.sonar.core.measure.custom.db.CustomMeasureDto;
+import org.sonar.core.metric.db.MetricDto;
+import org.sonar.core.persistence.DbSession;
+import org.sonar.core.persistence.MyBatis;
+import org.sonar.server.db.DbClient;
+import org.sonar.server.es.SearchOptions;
+import org.sonar.server.user.index.UserIndex;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.collect.Sets.newHashSet;
+
+public class SearchAction implements CustomMeasuresWsAction {
+
+  public static final String ACTION = "search";
+  public static final String PARAM_PROJECT_ID = "projectId";
+  public static final String PARAM_PROJECT_KEY = "projectKey";
+
+  private final DbClient dbClient;
+  private final UserIndex userIndex;
+  private final CustomMeasureJsonWriter customMeasureJsonWriter;
+
+  public SearchAction(DbClient dbClient, UserIndex userIndex, CustomMeasureJsonWriter customMeasureJsonWriter) {
+    this.dbClient = dbClient;
+    this.userIndex = userIndex;
+    this.customMeasureJsonWriter = customMeasureJsonWriter;
+  }
+
+  @Override
+  public void define(WebService.NewController context) {
+    WebService.NewAction action = context.createAction(ACTION)
+      .setDescription("List custom measures. The project id or project key must be provided.")
+      .setSince("5.2")
+      .addFieldsParam(CustomMeasureJsonWriter.OPTIONAL_FIELDS)
+      .addPagingParams(100)
+      .setResponseExample(getClass().getResource("example-search.json"))
+      .setHandler(this);
+
+    action.createParam(PARAM_PROJECT_ID)
+      .setDescription("Project id")
+      .setExampleValue("ce4c03d6-430f-40a9-b777-ad877c00aa4d");
+
+    action.createParam(PARAM_PROJECT_KEY)
+      .setDescription("Project key")
+      .setExampleValue("org.apache.hbas:hbase");
+  }
+
+  @Override
+  public void handle(Request request, Response response) throws Exception {
+    String projectUuid = request.param(PARAM_PROJECT_ID);
+    String projectKey = request.param(PARAM_PROJECT_KEY);
+    List<String> fieldsToReturn = request.paramAsStrings(WebService.Param.FIELDS);
+    SearchOptions searchOptions = new SearchOptions()
+      .setPage(request.mandatoryParamAsInt(WebService.Param.PAGE),
+        request.mandatoryParamAsInt(WebService.Param.PAGE_SIZE));
+
+    DbSession dbSession = dbClient.openSession(false);
+    try {
+      ComponentDto project = searchProject(dbSession, projectUuid, projectKey);
+      List<CustomMeasureDto> customMeasures = searchCustomMeasures(dbSession, project, searchOptions);
+      int nbCustomMeasures = countTotalOfCustomMeasures(dbSession, project);
+      Map<String, User> usersByLogin = usersByLogin(customMeasures);
+      Map<Integer, MetricDto> metricsById = metricsById(dbSession, customMeasures);
+
+      writeResponse(response, customMeasures, nbCustomMeasures, project, metricsById, usersByLogin, searchOptions, fieldsToReturn);
+    } finally {
+      MyBatis.closeQuietly(dbSession);
+    }
+  }
+
+  private int countTotalOfCustomMeasures(DbSession dbSession, ComponentDto project) {
+    return dbClient.customMeasureDao().countByComponentUuid(dbSession, project.uuid());
+  }
+
+  private List<CustomMeasureDto> searchCustomMeasures(DbSession dbSession, ComponentDto project, SearchOptions searchOptions) {
+    return dbClient.customMeasureDao().selectByComponentUuid(dbSession, project.uuid(), searchOptions);
+  }
+
+  private void writeResponse(Response response, List<CustomMeasureDto> customMeasures, int nbCustomMeasures, ComponentDto project, Map<Integer, MetricDto> metricsById,
+    Map<String, User> usersByLogin, SearchOptions searchOptions, List<String> fieldsToReturn) {
+    JsonWriter json = response.newJsonWriter();
+    json.beginObject();
+    customMeasureJsonWriter.write(json, customMeasures, project, metricsById, usersByLogin, fieldsToReturn);
+    searchOptions.writeJson(json, nbCustomMeasures);
+    json.endObject();
+    json.close();
+  }
+
+  private Map<Integer, MetricDto> metricsById(DbSession dbSession, List<CustomMeasureDto> customMeasures) {
+    List<MetricDto> metrics = dbClient.metricDao().selectByIds(dbSession, newHashSet(Lists.transform(customMeasures, CustomMeasureToMetricIdFunction.INSTANCE)));
+    return Maps.uniqueIndex(metrics, MetricToIdFunction.INSTANCE);
+  }
+
+  private Map<String, User> usersByLogin(List<CustomMeasureDto> customMeasures) {
+    return FluentIterable.from(customMeasures)
+      .transform(CustomMeasureToUserLoginFunction.INSTANCE)
+      .toMap(new UserLoginToUserFunction());
+  }
+
+  private ComponentDto searchProject(DbSession dbSession, @Nullable String projectUuid, @Nullable String projectKey) {
+    checkArgument(projectUuid != null ^ projectKey != null, "The project id or project key must be provided, not both.");
+    ComponentDto project;
+    if (projectUuid != null) {
+      project = dbClient.componentDao().selectByUuid(dbSession, projectUuid);
+    } else {
+      project = dbClient.componentDao().selectByKey(dbSession, projectKey);
+    }
+
+    return project;
+  }
+
+  private enum CustomMeasureToUserLoginFunction implements Function<CustomMeasureDto, String> {
+    INSTANCE;
+
+    @Override
+    public String apply(@Nonnull CustomMeasureDto customMeasure) {
+      return customMeasure.getUserLogin();
+    }
+  }
+
+  private final class UserLoginToUserFunction implements Function<String, User> {
+    @Override
+    public User apply(@Nonnull String userLogin) {
+      return userIndex.getByLogin(userLogin);
+    }
+  }
+
+  private enum CustomMeasureToMetricIdFunction implements Function<CustomMeasureDto, Integer> {
+    INSTANCE;
+
+    @Override
+    public Integer apply(@Nonnull CustomMeasureDto customMeasure) {
+      return customMeasure.getMetricId();
+    }
+  }
+
+  private enum MetricToIdFunction implements Function<MetricDto, Integer> {
+    INSTANCE;
+
+    @Override
+    public Integer apply(@Nonnull MetricDto metric) {
+      return metric.getId();
+    }
+  }
+}
index a5edb38b2f065c162a5aaeec2291186ea4a80dcc..60ef4a642cd29363ff6ddc8b91815423f80c76f7 100644 (file)
@@ -100,7 +100,6 @@ public class UpdateAction implements CustomMeasuresWsAction {
       checkPermissions(component);
       User user = userIndex.getByLogin(userSession.getLogin());
 
-
       setValue(customMeasure, value, metric);
       setDescription(customMeasure, description);
       customMeasure.setUserLogin(user.login());
@@ -109,7 +108,7 @@ public class UpdateAction implements CustomMeasuresWsAction {
       dbSession.commit();
 
       JsonWriter json = response.newJsonWriter();
-      customMeasureJsonWriter.write(json, customMeasure, metric, component, user);
+      customMeasureJsonWriter.write(json, customMeasure, metric, component, user, CustomMeasureJsonWriter.OPTIONAL_FIELDS);
       json.close();
     } finally {
       MyBatis.closeQuietly(dbSession);
index 3a72aecf2e42e8960e90a4c4881dbb088e8cac0a..1b0b5a4a9e66d7735a1bacf71792a116de3f08bf 100644 (file)
@@ -25,9 +25,11 @@ import com.google.common.base.Predicate;
 import com.google.common.collect.Collections2;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import javax.annotation.CheckForNull;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -111,6 +113,16 @@ public class MetricDao implements DaoComponent {
     return mapper(session).selectAvailableCustomMetricsByComponentUuid(projectKey);
   }
 
+  public List<MetricDto> selectByIds(final DbSession session, Set<Integer> idsSet) {
+    List<Integer> ids = new ArrayList<>(idsSet);
+    return DaoUtils.executeLargeInputs(ids, new Function<List<Integer>, List<MetricDto>>() {
+      @Override
+      public List<MetricDto> apply(@Nonnull List<Integer> ids) {
+        return mapper(session).selectByIds(ids);
+      }
+    });
+  }
+
   private static class NotEmptyPredicate implements Predicate<String> {
 
     @Override
index 0e752b2276f625aa36158931e3cc699ef2c76b9a..474a518c3b46a4fe3805bc65a56a2724e94faf43 100644 (file)
@@ -41,7 +41,7 @@ public class MetricJsonWriter {
   public static final String FIELD_HIDDEN = "hidden";
   public static final String FIELD_CUSTOM = "custom";
   public static final Set<String> OPTIONAL_FIELDS = ImmutableSet.of(FIELD_NAME, FIELD_DESCRIPTION, FIELD_DOMAIN, FIELD_DIRECTION, FIELD_QUALITATIVE, FIELD_HIDDEN, FIELD_CUSTOM);
-  private static final Set<String> MANDATORY_FIELDS = ImmutableSet.of(FIELD_ID, FIELD_KEY, FIELD_TYPE);
+  public static final Set<String> MANDATORY_FIELDS = ImmutableSet.of(FIELD_ID, FIELD_KEY, FIELD_TYPE);
   public static final Set<String> ALL_FIELDS = Sets.union(MANDATORY_FIELDS, OPTIONAL_FIELDS);
 
   private MetricJsonWriter() {
@@ -52,19 +52,23 @@ public class MetricJsonWriter {
     json.name("metrics");
     json.beginArray();
     for (MetricDto metric : metrics) {
-      json.beginObject();
-      json.prop(FIELD_ID, String.valueOf(metric.getId()));
-      json.prop(FIELD_KEY, metric.getKey());
-      json.prop(FIELD_TYPE, metric.getValueType());
-      writeIfNeeded(json, metric.getShortName(), FIELD_NAME, fieldsToReturn);
-      writeIfNeeded(json, metric.getDescription(), FIELD_DESCRIPTION, fieldsToReturn);
-      writeIfNeeded(json, metric.getDomain(), FIELD_DOMAIN, fieldsToReturn);
-      writeIfNeeded(json, metric.getDirection(), FIELD_DIRECTION, fieldsToReturn);
-      writeIfNeeded(json, metric.isQualitative(), FIELD_QUALITATIVE, fieldsToReturn);
-      writeIfNeeded(json, metric.isHidden(), FIELD_HIDDEN, fieldsToReturn);
-      writeIfNeeded(json, metric.isUserManaged(), FIELD_CUSTOM, fieldsToReturn);
-      json.endObject();
+      write(json, metric, fieldsToReturn);
     }
     json.endArray();
   }
+
+  public static void write(JsonWriter json, MetricDto metric, Set<String> fieldsToReturn) {
+    json.beginObject();
+    json.prop(FIELD_ID, String.valueOf(metric.getId()));
+    json.prop(FIELD_KEY, metric.getKey());
+    json.prop(FIELD_TYPE, metric.getValueType());
+    writeIfNeeded(json, metric.getShortName(), FIELD_NAME, fieldsToReturn);
+    writeIfNeeded(json, metric.getDescription(), FIELD_DESCRIPTION, fieldsToReturn);
+    writeIfNeeded(json, metric.getDomain(), FIELD_DOMAIN, fieldsToReturn);
+    writeIfNeeded(json, metric.getDirection(), FIELD_DIRECTION, fieldsToReturn);
+    writeIfNeeded(json, metric.isQualitative(), FIELD_QUALITATIVE, fieldsToReturn);
+    writeIfNeeded(json, metric.isHidden(), FIELD_HIDDEN, fieldsToReturn);
+    writeIfNeeded(json, metric.isUserManaged(), FIELD_CUSTOM, fieldsToReturn);
+    json.endObject();
+  }
 }
index 16cc16e990686b12d00fc430c72302ee4af7a2f0..bd6548d82ac116a9208085041b48dd960a194148 100644 (file)
@@ -20,6 +20,7 @@
 package org.sonar.server.ws;
 
 import java.util.Collection;
+import java.util.Date;
 import javax.annotation.Nullable;
 import org.sonar.api.utils.text.JsonWriter;
 
@@ -53,6 +54,12 @@ public class JsonWriterUtils {
     }
   }
 
+  public static void writeIfNeeded(JsonWriter json, @Nullable Date value, String field, Collection<String> fields) {
+    if (isFieldNeeded(field, fields)) {
+      json.propDateTime(field, value);
+    }
+  }
+
   public static boolean isFieldNeeded(String field, @Nullable Collection<String> fields) {
     return fields == null || fields.isEmpty() || fields.contains(field);
   }
diff --git a/server/sonar-server/src/main/resources/org/sonar/server/measure/custom/ws/example-search.json b/server/sonar-server/src/main/resources/org/sonar/server/measure/custom/ws/example-search.json
new file mode 100644 (file)
index 0000000..bbdb201
--- /dev/null
@@ -0,0 +1,55 @@
+{
+  "customMeasures": [
+    {
+      "description": "New arrivals",
+      "metric": {
+        "key": "team_size",
+        "type": "INT"
+      },
+      "projectId": "project-uuid",
+      "projectKey": "project-key",
+      "user": {
+        "active": true,
+        "email": "login@login.com",
+        "login": "login",
+        "name": "Stan Smith"
+      },
+      "value": "42"
+    },
+    {
+      "description": "New funds",
+      "metric": {
+        "key": "burned_budget",
+        "type": "INT"
+      },
+      "projectId": "project-uuid",
+      "projectKey": "project-key",
+      "user": {
+        "active": true,
+        "email": "login@login.com",
+        "login": "login",
+        "name": "Stan Smith"
+      },
+      "value": "1000000"
+    },
+    {
+      "description": "Great coverage",
+      "metric": {
+        "key": "uncovered_lines",
+        "type": "INT"
+      },
+      "projectId": "project-uuid",
+      "projectKey": "project-key",
+      "user": {
+        "active": true,
+        "email": "login@login.com",
+        "login": "login",
+        "name": "Stan Smith"
+      },
+      "value": "1"
+    }
+  ],
+  "p": 1,
+  "ps": 100,
+  "total": 3
+}
index b8c4f3fd306cbb0af607c865d003bc0b264d3354..fdbed1017bec672b123e284bdef9a7d8f5a9aea7 100644 (file)
@@ -30,6 +30,6 @@ public class CustomMeasuresWsModuleTest {
   public void verify_count_of_added_components() {
     ComponentContainer container = new ComponentContainer();
     new CustomMeasuresWsModule().configure(container);
-    assertThat(container.size()).isEqualTo(9);
+    assertThat(container.size()).isEqualTo(10);
   }
 }
index 0225dcb57080d78ce8ea55cb7046760b295c273c..5dfafb50df04819f93862001359faf916199b0ec 100644 (file)
@@ -22,11 +22,13 @@ package org.sonar.server.measure.custom.ws;
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.experimental.categories.Category;
 import org.junit.rules.ExpectedException;
+import org.sonar.api.config.Settings;
 import org.sonar.api.measures.Metric;
 import org.sonar.core.component.ComponentDto;
 import org.sonar.core.measure.custom.db.CustomMeasureDto;
@@ -36,8 +38,11 @@ import org.sonar.core.persistence.DbTester;
 import org.sonar.server.component.ComponentTesting;
 import org.sonar.server.component.db.ComponentDao;
 import org.sonar.server.db.DbClient;
+import org.sonar.server.es.EsTester;
 import org.sonar.server.measure.custom.persistence.CustomMeasureDao;
 import org.sonar.server.metric.persistence.MetricDao;
+import org.sonar.server.user.index.UserDoc;
+import org.sonar.server.user.index.UserIndexDefinition;
 import org.sonar.server.ws.WsTester;
 import org.sonar.test.DbTests;
 
@@ -55,11 +60,22 @@ public class MetricsActionTest {
   @Rule
   public ExpectedException expectedException = ExpectedException.none();
   @ClassRule
+  public static EsTester es = new EsTester().addDefinitions(new UserIndexDefinition(new Settings()));
+  @ClassRule
   public static DbTester db = new DbTester();
   DbClient dbClient;
   DbSession dbSession;
   WsTester ws;
 
+  @BeforeClass
+  public static void setUpClass() throws Exception {
+    es.putDocuments(UserIndexDefinition.INDEX, UserIndexDefinition.TYPE_USER, new UserDoc()
+      .setLogin("login")
+      .setName("Login")
+      .setEmail("login@login.com")
+      .setActive(true));
+  }
+
   @Before
   public void setUp() {
     dbClient = new DbClient(db.database(), db.myBatis(), new MetricDao(), new ComponentDao(), new CustomMeasureDao());
diff --git a/server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/SearchActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/SearchActionTest.java
new file mode 100644 (file)
index 0000000..be3a25e
--- /dev/null
@@ -0,0 +1,274 @@
+/*
+ * 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.measure.custom.ws;
+
+import org.apache.commons.lang.StringUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.config.Settings;
+import org.sonar.api.measures.Metric.ValueType;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.core.component.ComponentDto;
+import org.sonar.core.measure.custom.db.CustomMeasureDto;
+import org.sonar.core.metric.db.MetricDto;
+import org.sonar.core.persistence.DbSession;
+import org.sonar.core.persistence.DbTester;
+import org.sonar.server.component.ComponentTesting;
+import org.sonar.server.component.db.ComponentDao;
+import org.sonar.server.db.DbClient;
+import org.sonar.server.es.EsTester;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.measure.custom.persistence.CustomMeasureDao;
+import org.sonar.server.metric.persistence.MetricDao;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.user.index.UserDoc;
+import org.sonar.server.user.index.UserIndex;
+import org.sonar.server.user.index.UserIndexDefinition;
+import org.sonar.server.user.ws.UserJsonWriter;
+import org.sonar.server.ws.WsTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.server.measure.custom.persistence.CustomMeasureTesting.newCustomMeasureDto;
+import static org.sonar.server.metric.ws.MetricTesting.newMetricDto;
+
+public class SearchActionTest {
+  private static final String DEFAULT_PROJECT_UUID = "project-uuid";
+  private static final String DEFAULT_PROJECT_KEY = "project-key";
+  private static final String METRIC_KEY_PREFIX = "metric-key-";
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+  @Rule
+  public UserSessionRule userSessionRule = UserSessionRule.standalone();
+  @ClassRule
+  public static DbTester db = new DbTester();
+  @ClassRule
+  public static EsTester es = new EsTester().addDefinitions(new UserIndexDefinition(new Settings()));
+  WsTester ws;
+  DbClient dbClient;
+  DbSession dbSession;
+  ComponentDto defaultProject;
+
+  @BeforeClass
+  public static void setUpClass() throws Exception {
+    es.putDocuments(UserIndexDefinition.INDEX, UserIndexDefinition.TYPE_USER, new UserDoc()
+      .setLogin("login")
+      .setName("Login")
+      .setEmail("login@login.com")
+      .setActive(true));
+  }
+
+  @Before
+  public void setUp() {
+    dbClient = new DbClient(db.database(), db.myBatis(), new CustomMeasureDao(), new ComponentDao(), new MetricDao());
+    dbSession = dbClient.openSession(false);
+    db.truncateTables();
+    CustomMeasureJsonWriter customMeasureJsonWriter = new CustomMeasureJsonWriter(new UserJsonWriter(userSessionRule));
+    UserIndex userIndex = new UserIndex(es.client());
+    ws = new WsTester(new CustomMeasuresWs(new SearchAction(dbClient, userIndex, customMeasureJsonWriter)));
+    defaultProject = insertDefaultProject();
+  }
+
+  @After
+  public void tearDown() {
+    dbSession.close();
+  }
+
+  @Test
+  public void json_well_formatted() throws Exception {
+    MetricDto metric1 = insertCustomMetric(1);
+    MetricDto metric2 = insertCustomMetric(2);
+    MetricDto metric3 = insertCustomMetric(3);
+    CustomMeasureDto customMeasure1 = insertCustomMeasure(1, metric1);
+    CustomMeasureDto customMeasure2 = insertCustomMeasure(2, metric2);
+    CustomMeasureDto customMeasure3 = insertCustomMeasure(3, metric3);
+
+    WsTester.Result response = newRequest()
+      .setParam(SearchAction.PARAM_PROJECT_ID, DEFAULT_PROJECT_UUID)
+      .execute();
+
+    response.assertJson(getClass(), "metrics.json");
+    String responseAsString = response.outputAsString();
+    assertThat(responseAsString).matches(nameValuePattern("id", metric1.getId().toString()));
+    assertThat(responseAsString).matches(nameValuePattern("id", metric2.getId().toString()));
+    assertThat(responseAsString).matches(nameValuePattern("id", metric3.getId().toString()));
+    assertThat(responseAsString).matches(nameValuePattern("id", String.valueOf(customMeasure1.getId())));
+    assertThat(responseAsString).matches(nameValuePattern("id", String.valueOf(customMeasure2.getId())));
+    assertThat(responseAsString).matches(nameValuePattern("id", String.valueOf(customMeasure3.getId())));
+    assertThat(responseAsString).contains("createdAt", "updatedAt");
+  }
+
+  @Test
+  public void search_by_project_uuid() throws Exception {
+    MetricDto metric1 = insertCustomMetric(1);
+    MetricDto metric2 = insertCustomMetric(2);
+    MetricDto metric3 = insertCustomMetric(3);
+    insertCustomMeasure(1, metric1);
+    insertCustomMeasure(2, metric2);
+    insertCustomMeasure(3, metric3);
+
+    String response = newRequest()
+      .setParam(SearchAction.PARAM_PROJECT_ID, DEFAULT_PROJECT_UUID)
+      .execute().outputAsString();
+
+    assertThat(response).contains("text-value-1", "text-value-2", "text-value-3");
+  }
+
+  @Test
+  public void search_by_project_key() throws Exception {
+    MetricDto metric1 = insertCustomMetric(1);
+    MetricDto metric2 = insertCustomMetric(2);
+    MetricDto metric3 = insertCustomMetric(3);
+    insertCustomMeasure(1, metric1);
+    insertCustomMeasure(2, metric2);
+    insertCustomMeasure(3, metric3);
+
+    String response = newRequest()
+      .setParam(SearchAction.PARAM_PROJECT_KEY, DEFAULT_PROJECT_KEY)
+      .execute().outputAsString();
+
+    assertThat(response).contains("text-value-1", "text-value-2", "text-value-3");
+  }
+
+  @Test
+  public void search_with_pagination() throws Exception {
+    for (int i = 0; i < 10; i++) {
+      MetricDto metric = insertCustomMetric(i);
+      insertCustomMeasure(i, metric);
+    }
+
+    String response = newRequest()
+      .setParam(SearchAction.PARAM_PROJECT_KEY, DEFAULT_PROJECT_KEY)
+      .setParam(WebService.Param.PAGE, "3")
+      .setParam(WebService.Param.PAGE_SIZE, "4")
+      .execute().outputAsString();
+
+    assertThat(StringUtils.countMatches(response, "text-value")).isEqualTo(2);
+  }
+
+  @Test
+  public void search_with_selectable_fields() throws Exception {
+    MetricDto metric = insertCustomMetric(1);
+    insertCustomMeasure(1, metric);
+
+    String response = newRequest()
+      .setParam(SearchAction.PARAM_PROJECT_KEY, DEFAULT_PROJECT_KEY)
+      .setParam(WebService.Param.FIELDS, "value, description")
+      .execute().outputAsString();
+
+    assertThat(response).contains("id", "value", "description")
+      .doesNotContain("createdAt")
+      .doesNotContain("updatedAt")
+      .doesNotContain("user")
+      .doesNotContain("metric");
+  }
+
+  @Test
+  public void empty_json_when_no_measure() throws Exception {
+    WsTester.Result response = newRequest()
+      .setParam(SearchAction.PARAM_PROJECT_KEY, DEFAULT_PROJECT_KEY)
+      .execute();
+
+    response.assertJson(getClass(), "empty.json");
+  }
+
+  @Test
+  public void fail_when_project_id_and_project_key_provided() throws Exception {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("The project id or project key must be provided, not both.");
+
+    newRequest()
+      .setParam(SearchAction.PARAM_PROJECT_ID, DEFAULT_PROJECT_UUID)
+      .setParam(SearchAction.PARAM_PROJECT_KEY, DEFAULT_PROJECT_KEY)
+      .execute();
+  }
+
+  @Test
+  public void fail_when_project_id_nor_project_key_provided() throws Exception {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("The project id or project key must be provided, not both.");
+    newRequest().execute();
+  }
+
+  @Test
+  public void fail_when_project_not_found_in_db() throws Exception {
+    expectedException.expect(NotFoundException.class);
+    expectedException.expectMessage("Component with uuid 'wrong-project-uuid' not found");
+
+    newRequest().setParam(SearchAction.PARAM_PROJECT_ID, "wrong-project-uuid").execute();
+  }
+
+  private ComponentDto insertDefaultProject() {
+    return insertProject(DEFAULT_PROJECT_UUID, DEFAULT_PROJECT_KEY);
+  }
+
+  private ComponentDto insertProject(String projectUuid, String projectKey) {
+    ComponentDto project = ComponentTesting.newProjectDto(projectUuid)
+      .setKey(projectKey);
+    dbClient.componentDao().insert(dbSession, project);
+    dbSession.commit();
+
+    return project;
+  }
+
+  private MetricDto insertCustomMetric(int id) {
+    MetricDto metric = newCustomMetric(METRIC_KEY_PREFIX + id);
+    dbClient.metricDao().insert(dbSession, metric);
+    dbSession.commit();
+
+    return metric;
+  }
+
+  private static MetricDto newCustomMetric(String metricKey) {
+    return newMetricDto().setEnabled(true).setUserManaged(true).setKey(metricKey).setValueType(ValueType.STRING.name());
+  }
+
+  private CustomMeasureDto insertCustomMeasure(int id, MetricDto metric) {
+    CustomMeasureDto customMeasure = newCustomMeasure(id, metric);
+    dbClient.customMeasureDao().insert(dbSession, customMeasure);
+    dbSession.commit();
+
+    return customMeasure;
+  }
+
+  private CustomMeasureDto newCustomMeasure(int id, MetricDto metric) {
+    return newCustomMeasureDto()
+      .setUserLogin("login")
+      .setValue(id)
+      .setTextValue("text-value-" + id)
+      .setDescription("description-" + id)
+      .setMetricId(metric.getId())
+      .setComponentUuid(defaultProject.uuid());
+  }
+
+  private WsTester.TestRequest newRequest() {
+    return ws.newGetRequest(CustomMeasuresWs.ENDPOINT, SearchAction.ACTION);
+  }
+
+  private static String nameValuePattern(String name, String value) {
+    return String.format(".*\"%s\"\\s*:\\s*\"%s\".*", name, value);
+  }
+}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/measure/custom/ws/SearchActionTest/empty.json b/server/sonar-server/src/test/resources/org/sonar/server/measure/custom/ws/SearchActionTest/empty.json
new file mode 100644 (file)
index 0000000..07b3a29
--- /dev/null
@@ -0,0 +1,7 @@
+{
+  "customMeasures": [
+  ],
+  "p": 1,
+  "ps": 100,
+  "total": 0
+}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/measure/custom/ws/SearchActionTest/metrics.json b/server/sonar-server/src/test/resources/org/sonar/server/measure/custom/ws/SearchActionTest/metrics.json
new file mode 100644 (file)
index 0000000..bf3bd9f
--- /dev/null
@@ -0,0 +1,55 @@
+{
+  "customMeasures": [
+    {
+      "description": "description-1",
+      "metric": {
+        "key": "metric-key-1",
+        "type": "STRING"
+      },
+      "projectId": "project-uuid",
+      "projectKey": "project-key",
+      "user": {
+        "active": true,
+        "email": "login@login.com",
+        "login": "login",
+        "name": "Login"
+      },
+      "value": "text-value-1"
+    },
+    {
+      "description": "description-2",
+      "metric": {
+        "key": "metric-key-2",
+        "type": "STRING"
+      },
+      "projectId": "project-uuid",
+      "projectKey": "project-key",
+      "user": {
+        "active": true,
+        "email": "login@login.com",
+        "login": "login",
+        "name": "Login"
+      },
+      "value": "text-value-2"
+    },
+    {
+      "description": "description-3",
+      "metric": {
+        "key": "metric-key-3",
+        "type": "STRING"
+      },
+      "projectId": "project-uuid",
+      "projectKey": "project-key",
+      "user": {
+        "active": true,
+        "email": "login@login.com",
+        "login": "login",
+        "name": "Login"
+      },
+      "value": "text-value-3"
+    }
+  ],
+  "p": 1,
+  "ps": 100,
+  "total": 3
+}
index 444753996da777e57909f82f4440f401857e4760..58bbb04b966b42b0770d46cdadc7e9e912a33d68 100644 (file)
@@ -22,6 +22,7 @@ package org.sonar.core.measure.custom.db;
 
 import java.util.List;
 import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.session.RowBounds;
 
 public interface CustomMeasureMapper {
   void insert(CustomMeasureDto customMeasure);
@@ -38,5 +39,9 @@ public interface CustomMeasureMapper {
 
   List<CustomMeasureDto> selectByComponentUuid(String s);
 
+  List<CustomMeasureDto> selectByComponentUuid(String s, RowBounds rowBounds);
+
+  int countByComponentUuid(String componentUuid);
+
   int countByComponentIdAndMetricId(@Param("componentUuid") String componentUuid, @Param("metricId") int metricId);
 }
index 7644057f92e5bdf433e580cab6f4b864c674f2ca..aea1761b828221a32278ff8822711249d9adb3f0 100644 (file)
@@ -28,8 +28,14 @@ import org.apache.ibatis.session.RowBounds;
 
 public interface MetricMapper {
 
+  MetricDto selectById(long id);
+
+  List<MetricDto> selectByIds(@Param("ids") List<Integer> ids);
+
   MetricDto selectByKey(@Param("key") String key);
 
+  List<MetricDto> selectByKeys(@Param("keys") List<String> keys);
+
   List<MetricDto> selectAllEnabled();
 
   List<MetricDto> selectAllEnabled(Map<String, Object> properties, RowBounds rowBounds);
@@ -38,8 +44,6 @@ public interface MetricMapper {
 
   List<String> selectDomains();
 
-  List<MetricDto> selectByKeys(@Param("keys") List<String> keys);
-
   void disableByIds(@Param("ids") List<Integer> ids);
 
   void disableByKey(@Param("key") String key);
@@ -48,8 +52,6 @@ public interface MetricMapper {
 
   void update(MetricDto metric);
 
-  MetricDto selectById(long id);
-
   List<MetricDto> selectAvailableCustomMetricsByComponentUuid(String projectUuid);
 
   List<MetricDto> selectAvailableCustomMetricsByComponentKey(String projectKey);
index e19060dbc1a822174173f11d7c2cff2d58b6e8d9..8d39aac0143b1e1469172d318745fbd7dc92ebcd 100644 (file)
     where id=#{id}
   </delete>
 
+  <select id="countByComponentUuid" resultType="Integer">
+    select count(*)
+    from manual_measures m
+    where m.component_uuid=#{componentUuid}
+  </select>
+
   <select id="countByComponentIdAndMetricId" resultType="Integer">
     select count(*)
     from manual_measures m
index 3812362721b9a2507634adb107dcde29a8dd66b2..f11d214d376e400409c8c28494a2ea0ffb6238cc 100644 (file)
     </where>
   </select>
 
+  <select id="selectByIds" resultType="org.sonar.core.metric.db.MetricDto">
+    SELECT
+    <include refid="metricColumns"/>
+    FROM metrics m
+    <where>
+      AND m.id in
+      <foreach item="id" collection="ids" open="(" separator="," close=")">
+        #{id}
+      </foreach>
+    </where>
+  </select>
+
   <select id="selectById" resultType="org.sonar.core.metric.db.MetricDto">
     SELECT
     <include refid="metricColumns"/>