diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/custom/persistence/CustomMeasureDao.java b/server/sonar-server/src/main/java/org/sonar/server/measure/custom/persistence/CustomMeasureDao.java index 6a454ee27203..7f660ea5e5d1 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/measure/custom/persistence/CustomMeasureDao.java +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/custom/persistence/CustomMeasureDao.java @@ -24,12 +24,14 @@ 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 int countByComponentIdAndMetricId(DbSession session, String componentUuid return mapper(session).countByComponentIdAndMetricId(componentUuid, metricId); } + public List selectByComponentUuid(DbSession session, String componentUuid, SearchOptions searchOptions) { + return mapper(session).selectByComponentUuid(componentUuid, new RowBounds(searchOptions.getOffset(), searchOptions.getLimit())); + } + public List selectByComponentUuid(DbSession session, String componentUuid) { return mapper(session).selectByComponentUuid(componentUuid); } @@ -84,4 +90,8 @@ public List selectByComponentUuid(DbSession session, String co private CustomMeasureMapper mapper(DbSession session) { return session.getMapper(CustomMeasureMapper.class); } + + public int countByComponentUuid(DbSession dbSession, String uuid) { + return mapper(dbSession).countByComponentUuid(uuid); + } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/custom/ws/CreateAction.java b/server/sonar-server/src/main/java/org/sonar/server/measure/custom/ws/CreateAction.java index 7618cd5d41a3..a809a6230283 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/measure/custom/ws/CreateAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/custom/ws/CreateAction.java @@ -132,7 +132,7 @@ public void handle(Request request, Response response) throws Exception { 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); diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/custom/ws/CustomMeasureJsonWriter.java b/server/sonar-server/src/main/java/org/sonar/server/measure/custom/ws/CustomMeasureJsonWriter.java index f2b9f2618bc7..026d0cb9fa0d 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/measure/custom/ws/CustomMeasureJsonWriter.java +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/custom/ws/CustomMeasureJsonWriter.java @@ -20,55 +20,63 @@ 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 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 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 @@ private String measureValue(CustomMeasureDto measure, MetricDto metric) { } } - 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 customMeasures, ComponentDto project, Map metricsById, Map usersByLogin, + Collection 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(); } - } diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/custom/ws/CustomMeasuresWsModule.java b/server/sonar-server/src/main/java/org/sonar/server/measure/custom/ws/CustomMeasuresWsModule.java index 136f332dcfe6..1b40340d5a52 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/measure/custom/ws/CustomMeasuresWsModule.java +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/custom/ws/CustomMeasuresWsModule.java @@ -30,6 +30,7 @@ protected void configureModule() { 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 index 000000000000..02f33429d724 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/custom/ws/SearchAction.java @@ -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 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 customMeasures = searchCustomMeasures(dbSession, project, searchOptions); + int nbCustomMeasures = countTotalOfCustomMeasures(dbSession, project); + Map usersByLogin = usersByLogin(customMeasures); + Map 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 searchCustomMeasures(DbSession dbSession, ComponentDto project, SearchOptions searchOptions) { + return dbClient.customMeasureDao().selectByComponentUuid(dbSession, project.uuid(), searchOptions); + } + + private void writeResponse(Response response, List customMeasures, int nbCustomMeasures, ComponentDto project, Map metricsById, + Map usersByLogin, SearchOptions searchOptions, List 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 metricsById(DbSession dbSession, List customMeasures) { + List metrics = dbClient.metricDao().selectByIds(dbSession, newHashSet(Lists.transform(customMeasures, CustomMeasureToMetricIdFunction.INSTANCE))); + return Maps.uniqueIndex(metrics, MetricToIdFunction.INSTANCE); + } + + private Map usersByLogin(List 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 { + INSTANCE; + + @Override + public String apply(@Nonnull CustomMeasureDto customMeasure) { + return customMeasure.getUserLogin(); + } + } + + private final class UserLoginToUserFunction implements Function { + @Override + public User apply(@Nonnull String userLogin) { + return userIndex.getByLogin(userLogin); + } + } + + private enum CustomMeasureToMetricIdFunction implements Function { + INSTANCE; + + @Override + public Integer apply(@Nonnull CustomMeasureDto customMeasure) { + return customMeasure.getMetricId(); + } + } + + private enum MetricToIdFunction implements Function { + INSTANCE; + + @Override + public Integer apply(@Nonnull MetricDto metric) { + return metric.getId(); + } + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/custom/ws/UpdateAction.java b/server/sonar-server/src/main/java/org/sonar/server/measure/custom/ws/UpdateAction.java index a5edb38b2f06..60ef4a642cd2 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/measure/custom/ws/UpdateAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/custom/ws/UpdateAction.java @@ -100,7 +100,6 @@ public void handle(Request request, Response response) throws Exception { checkPermissions(component); User user = userIndex.getByLogin(userSession.getLogin()); - setValue(customMeasure, value, metric); setDescription(customMeasure, description); customMeasure.setUserLogin(user.login()); @@ -109,7 +108,7 @@ public void handle(Request request, Response response) throws Exception { 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); diff --git a/server/sonar-server/src/main/java/org/sonar/server/metric/persistence/MetricDao.java b/server/sonar-server/src/main/java/org/sonar/server/metric/persistence/MetricDao.java index 3a72aecf2e42..1b0b5a4a9e66 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/metric/persistence/MetricDao.java +++ b/server/sonar-server/src/main/java/org/sonar/server/metric/persistence/MetricDao.java @@ -25,9 +25,11 @@ 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 List selectAvailableCustomMetricsByComponentKey(DbSession sess return mapper(session).selectAvailableCustomMetricsByComponentUuid(projectKey); } + public List selectByIds(final DbSession session, Set idsSet) { + List ids = new ArrayList<>(idsSet); + return DaoUtils.executeLargeInputs(ids, new Function, List>() { + @Override + public List apply(@Nonnull List ids) { + return mapper(session).selectByIds(ids); + } + }); + } + private static class NotEmptyPredicate implements Predicate { @Override diff --git a/server/sonar-server/src/main/java/org/sonar/server/metric/ws/MetricJsonWriter.java b/server/sonar-server/src/main/java/org/sonar/server/metric/ws/MetricJsonWriter.java index 0e752b2276f6..474a518c3b46 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/metric/ws/MetricJsonWriter.java +++ b/server/sonar-server/src/main/java/org/sonar/server/metric/ws/MetricJsonWriter.java @@ -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 OPTIONAL_FIELDS = ImmutableSet.of(FIELD_NAME, FIELD_DESCRIPTION, FIELD_DOMAIN, FIELD_DIRECTION, FIELD_QUALITATIVE, FIELD_HIDDEN, FIELD_CUSTOM); - private static final Set MANDATORY_FIELDS = ImmutableSet.of(FIELD_ID, FIELD_KEY, FIELD_TYPE); + public static final Set MANDATORY_FIELDS = ImmutableSet.of(FIELD_ID, FIELD_KEY, FIELD_TYPE); public static final Set ALL_FIELDS = Sets.union(MANDATORY_FIELDS, OPTIONAL_FIELDS); private MetricJsonWriter() { @@ -52,19 +52,23 @@ public static void write(JsonWriter json, List metrics, Set f 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 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(); + } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/ws/JsonWriterUtils.java b/server/sonar-server/src/main/java/org/sonar/server/ws/JsonWriterUtils.java index 16cc16e99068..bd6548d82ac1 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/ws/JsonWriterUtils.java +++ b/server/sonar-server/src/main/java/org/sonar/server/ws/JsonWriterUtils.java @@ -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 static void writeIfNeeded(JsonWriter json, @Nullable Long value, String f } } + public static void writeIfNeeded(JsonWriter json, @Nullable Date value, String field, Collection fields) { + if (isFieldNeeded(field, fields)) { + json.propDateTime(field, value); + } + } + public static boolean isFieldNeeded(String field, @Nullable Collection 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 index 000000000000..bbdb201c18e2 --- /dev/null +++ b/server/sonar-server/src/main/resources/org/sonar/server/measure/custom/ws/example-search.json @@ -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 +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/CustomMeasuresWsModuleTest.java b/server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/CustomMeasuresWsModuleTest.java index b8c4f3fd306c..fdbed1017bec 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/CustomMeasuresWsModuleTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/CustomMeasuresWsModuleTest.java @@ -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); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/MetricsActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/MetricsActionTest.java index 0225dcb57080..5dfafb50df04 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/MetricsActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/MetricsActionTest.java @@ -22,11 +22,13 @@ 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.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 index 000000000000..be3a25e4c0b2 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/SearchActionTest.java @@ -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 index 000000000000..07b3a2950c20 --- /dev/null +++ b/server/sonar-server/src/test/resources/org/sonar/server/measure/custom/ws/SearchActionTest/empty.json @@ -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 index 000000000000..bf3bd9f80836 --- /dev/null +++ b/server/sonar-server/src/test/resources/org/sonar/server/measure/custom/ws/SearchActionTest/metrics.json @@ -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 +} diff --git a/sonar-core/src/main/java/org/sonar/core/measure/custom/db/CustomMeasureMapper.java b/sonar-core/src/main/java/org/sonar/core/measure/custom/db/CustomMeasureMapper.java index 444753996da7..58bbb04b966b 100644 --- a/sonar-core/src/main/java/org/sonar/core/measure/custom/db/CustomMeasureMapper.java +++ b/sonar-core/src/main/java/org/sonar/core/measure/custom/db/CustomMeasureMapper.java @@ -22,6 +22,7 @@ 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 selectByComponentUuid(String s); + List selectByComponentUuid(String s, RowBounds rowBounds); + + int countByComponentUuid(String componentUuid); + int countByComponentIdAndMetricId(@Param("componentUuid") String componentUuid, @Param("metricId") int metricId); } diff --git a/sonar-core/src/main/java/org/sonar/core/metric/db/MetricMapper.java b/sonar-core/src/main/java/org/sonar/core/metric/db/MetricMapper.java index 7644057f92e5..aea1761b8282 100644 --- a/sonar-core/src/main/java/org/sonar/core/metric/db/MetricMapper.java +++ b/sonar-core/src/main/java/org/sonar/core/metric/db/MetricMapper.java @@ -28,8 +28,14 @@ public interface MetricMapper { + MetricDto selectById(long id); + + List selectByIds(@Param("ids") List ids); + MetricDto selectByKey(@Param("key") String key); + List selectByKeys(@Param("keys") List keys); + List selectAllEnabled(); List selectAllEnabled(Map properties, RowBounds rowBounds); @@ -38,8 +44,6 @@ public interface MetricMapper { List selectDomains(); - List selectByKeys(@Param("keys") List keys); - void disableByIds(@Param("ids") List ids); void disableByKey(@Param("key") String key); @@ -48,8 +52,6 @@ public interface MetricMapper { void update(MetricDto metric); - MetricDto selectById(long id); - List selectAvailableCustomMetricsByComponentUuid(String projectUuid); List selectAvailableCustomMetricsByComponentKey(String projectKey); diff --git a/sonar-core/src/main/resources/org/sonar/core/measure/custom/db/CustomMeasureMapper.xml b/sonar-core/src/main/resources/org/sonar/core/measure/custom/db/CustomMeasureMapper.xml index e19060dbc1a8..8d39aac0143b 100644 --- a/sonar-core/src/main/resources/org/sonar/core/measure/custom/db/CustomMeasureMapper.xml +++ b/sonar-core/src/main/resources/org/sonar/core/measure/custom/db/CustomMeasureMapper.xml @@ -69,6 +69,12 @@ where id=#{id} + + + +