From 1eea485b14b3af360250a2302e9557dd615ef492 Mon Sep 17 00:00:00 2001 From: Teryk Bellahsene Date: Tue, 16 Jun 2015 10:20:29 +0200 Subject: [PATCH] SONAR-6610 WS custom_measures/create create a custom measure --- .../custom/persistence/CustomMeasureDao.java | 12 +- .../measure/custom/ws/CreateAction.java | 307 +++++++++++++ .../custom/ws/CustomMeasuresWsModule.java | 3 +- .../server/metric/persistence/MetricDao.java | 10 +- .../util/MetricLevelTypeValidation.java | 43 ++ .../MetricWorkDurationTypeValidation.java | 49 ++ .../measure/custom/ws/CreateActionTest.java | 417 ++++++++++++++++++ .../custom/ws/CustomMeasuresWsModuleTest.java | 2 +- .../custom/ws/CustomMeasuresWsTest.java | 14 +- .../ws/CreateActionTest/custom-measure.json | 7 + .../custom/db/CustomMeasureMapper.java | 2 + .../measure/custom/db/CustomMeasureMapper.xml | 6 + .../resources/org/sonar/l10n/core.properties | 2 + .../main/java/org/sonar/api/PropertyType.java | 16 +- .../java/org/sonar/api/measures/Metric.java | 15 + .../java/org/sonar/api/utils/Durations.java | 3 +- 16 files changed, 895 insertions(+), 13 deletions(-) create mode 100644 server/sonar-server/src/main/java/org/sonar/server/measure/custom/ws/CreateAction.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/util/MetricLevelTypeValidation.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/util/MetricWorkDurationTypeValidation.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/CreateActionTest.java create mode 100644 server/sonar-server/src/test/resources/org/sonar/server/measure/custom/ws/CreateActionTest/custom-measure.json 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 94c26bca199..0c609c98615 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 @@ -65,12 +65,16 @@ public class CustomMeasureDao implements DaoComponent { return customMeasure; } - public List selectByMetricId(DbSession session, int id) { - return mapper(session).selectByMetricId(id); + public List selectByMetricId(DbSession session, int metricId) { + return mapper(session).selectByMetricId(metricId); } - public List selectByComponentId(DbSession session, long id) { - return mapper(session).selectByComponentId(id); + public List selectByComponentId(DbSession session, long componentId) { + return mapper(session).selectByComponentId(componentId); + } + + public int countByComponentIdAndMetricId(DbSession session, String componentUuid, int metricId) { + return mapper(session).countByComponentIdAndMetricId(componentUuid, metricId); } private CustomMeasureMapper mapper(DbSession session) { 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 new file mode 100644 index 00000000000..39b9428308d --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/custom/ws/CreateAction.java @@ -0,0 +1,307 @@ +/* + * 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.Joiner; +import java.net.HttpURLConnection; +import org.sonar.api.PropertyType; +import org.sonar.api.measures.Metric; +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.utils.Durations; +import org.sonar.api.utils.System2; +import org.sonar.api.utils.text.JsonWriter; +import org.sonar.api.web.UserRole; +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.permission.GlobalPermissions; +import org.sonar.core.persistence.DbSession; +import org.sonar.core.persistence.MyBatis; +import org.sonar.server.db.DbClient; +import org.sonar.server.exceptions.NotFoundException; +import org.sonar.server.exceptions.ServerException; +import org.sonar.server.user.UserSession; +import org.sonar.server.util.TypeValidations; + +import static com.google.common.base.Preconditions.checkArgument; + +public class CreateAction implements CustomMeasuresWsAction { + public static final String ACTION = "create"; + public static final String PARAM_PROJECT_ID = "projectId"; + public static final String PARAM_PROJECT_KEY = "projectKey"; + public static final String PARAM_METRIC_ID = "metricId"; + public static final String PARAM_METRIC_KEY = "metricKey"; + public static final String PARAM_VALUE = "value"; + public static final String PARAM_DESCRIPTION = "description"; + + 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_METRIC_ID = PARAM_METRIC_ID; + private static final String FIELD_VALUE = PARAM_VALUE; + private static final String FIELD_DESCRIPTION = PARAM_DESCRIPTION; + private static final String FIELD_METRIC_KEY = PARAM_METRIC_KEY; + + private final DbClient dbClient; + private final UserSession userSession; + private final System2 system; + private final TypeValidations typeValidations; + private final Durations durations; + + public CreateAction(DbClient dbClient, UserSession userSession, System2 system, TypeValidations typeValidations, Durations durations) { + this.dbClient = dbClient; + this.userSession = userSession; + this.system = system; + this.typeValidations = typeValidations; + this.durations = durations; + } + + @Override + public void define(WebService.NewController context) { + WebService.NewAction action = context.createAction(ACTION) + .setDescription("Create a custom measure.
" + + "The project id or the project key must be provided. The metric id or the metric key must be provided.
" + + "Requires 'Administer System' permission or 'Administer' permission on the project.") + .setSince("5.2") + .setPost(true) + .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"); + + action.createParam(PARAM_METRIC_ID) + .setDescription("Metric id") + .setExampleValue("16"); + + action.createParam(PARAM_METRIC_KEY) + .setDescription("Metric key") + .setExampleValue("ncloc"); + + action.createParam(PARAM_VALUE) + .setRequired(true) + .setDescription(measureValueDescription()) + .setExampleValue("47"); + + action.createParam(PARAM_DESCRIPTION) + .setDescription("Description") + .setExampleValue("Team size growing."); + } + + @Override + public void handle(Request request, Response response) throws Exception { + DbSession dbSession = dbClient.openSession(false); + String description = request.param(PARAM_DESCRIPTION); + long now = system.now(); + + try { + ComponentDto component = searchProject(dbSession, request); + MetricDto metric = searchMetric(dbSession, request); + checkPermissions(component); + checkMeasureDoesNotExistAlready(dbSession, component, metric); + CustomMeasureDto measure = new CustomMeasureDto() + .setComponentUuid(component.uuid()) + .setComponentId(component.getId()) + .setMetricId(metric.getId()) + .setDescription(description) + .setCreatedAt(now); + setMeasureValue(measure, request, metric); + dbClient.customMeasureDao().insert(dbSession, measure); + dbSession.commit(); + + JsonWriter json = response.newJsonWriter(); + writeMeasure(json, measure, component, metric, request.mandatoryParam(PARAM_VALUE)); + json.close(); + } finally { + MyBatis.closeQuietly(dbSession); + } + } + + private void checkPermissions(ComponentDto component) { + if (userSession.hasGlobalPermission(GlobalPermissions.SYSTEM_ADMIN)) { + return; + } + + userSession.checkLoggedIn().checkProjectUuidPermission(UserRole.ADMIN, component.projectUuid()); + } + + private void checkMeasureDoesNotExistAlready(DbSession dbSession, ComponentDto component, MetricDto metric) { + int nbMeasuresOnSameMetricAndMeasure = dbClient.customMeasureDao().countByComponentIdAndMetricId(dbSession, component.uuid(), metric.getId()); + if (nbMeasuresOnSameMetricAndMeasure > 0) { + throw new ServerException(HttpURLConnection.HTTP_CONFLICT, String.format("A measure already exists for project id '%s' and metric id '%d'", + component.uuid(), metric.getId())); + } + } + + private static void writeMeasure(JsonWriter json, CustomMeasureDto measure, ComponentDto component, MetricDto metric, String measureWithoutInternalFormatting) { + json.beginObject(); + json.prop(FIELD_ID, String.valueOf(measure.getId())); + json.prop(FIELD_METRIC_ID, String.valueOf(metric.getId())); + json.prop(FIELD_METRIC_KEY, metric.getKey()); + json.prop(FIELD_PROJECT_ID, component.uuid()); + json.prop(FIELD_PROJECT_KEY, component.key()); + json.prop(FIELD_DESCRIPTION, measure.getDescription()); + json.prop(FIELD_VALUE, measureWithoutInternalFormatting); + json.endObject(); + } + + private void setMeasureValue(CustomMeasureDto measure, Request request, MetricDto metric) { + String valueAsString = request.mandatoryParam(PARAM_VALUE); + Metric.ValueType metricType = Metric.ValueType.valueOf(metric.getValueType()); + try { + switch (metricType) { + case BOOL: + checkAndSetBooleanMeasureValue(measure, valueAsString); + break; + case INT: + case MILLISEC: + checkAndSetIntegerMeasureValue(measure, valueAsString); + break; + case WORK_DUR: + checkAndSetWorkDurationMeasureValue(measure, valueAsString); + break; + case FLOAT: + case PERCENT: + case RATING: + checkAndSetFloatMeasureValue(measure, valueAsString); + break; + case LEVEL: + checkAndSetLevelMeasureValue(measure, valueAsString); + break; + case STRING: + case DATA: + case DISTRIB: + measure.setTextValue(valueAsString); + break; + default: + throw new IllegalArgumentException("Unsupported metric type:" + metricType.description()); + } + } catch (Exception e) { + throw new IllegalArgumentException(String.format("Ill formatted value '%s' for metric type '%s'", valueAsString, metricType.description()), e); + } + } + + private void checkAndSetLevelMeasureValue(CustomMeasureDto measure, String valueAsString) { + typeValidations.validate(valueAsString, PropertyType.METRIC_LEVEL.name(), null); + measure.setTextValue(valueAsString); + } + + private void checkAndSetFloatMeasureValue(CustomMeasureDto measure, String valueAsString) { + typeValidations.validate(valueAsString, PropertyType.FLOAT.name(), null); + measure.setValue(Double.parseDouble(valueAsString)); + } + + private void checkAndSetWorkDurationMeasureValue(CustomMeasureDto measure, String valueAsString) { + typeValidations.validate(valueAsString, PropertyType.METRIC_WORK_DURATION.name(), null); + measure.setValue(durations.decode(valueAsString).toMinutes()); + } + + private void checkAndSetIntegerMeasureValue(CustomMeasureDto measure, String valueAsString) { + typeValidations.validate(valueAsString, PropertyType.INTEGER.name(), null); + measure.setValue(Integer.parseInt(valueAsString)); + } + + private void checkAndSetBooleanMeasureValue(CustomMeasureDto measure, String valueAsString) { + typeValidations.validate(valueAsString, PropertyType.BOOLEAN.name(), null); + measure.setValue(Boolean.parseBoolean(valueAsString) ? 1.0d : 0.0d); + } + + private MetricDto searchMetric(DbSession dbSession, Request request) { + Integer metricId = request.paramAsInt(PARAM_METRIC_ID); + String metricKey = request.param(PARAM_METRIC_KEY); + checkArgument(metricId != null ^ metricKey != null, "The metric id or the metric key must be provided, not both."); + + if (metricId != null) { + return dbClient.metricDao().selectById(dbSession, metricId); + } + + return dbClient.metricDao().selectByKey(dbSession, metricKey); + } + + private ComponentDto searchProject(DbSession dbSession, Request request) { + String projectUuid = request.param(PARAM_PROJECT_ID); + String projectKey = request.param(PARAM_PROJECT_KEY); + checkArgument(projectUuid != null ^ projectKey != null, "The project key or the project id must be provided, not both."); + + if (projectUuid != null) { + ComponentDto project = dbClient.componentDao().selectNullableByUuid(dbSession, projectUuid); + if (project == null) { + throw new NotFoundException(String.format("Project id '%s' not found", projectUuid)); + } + + return project; + } + + ComponentDto project = dbClient.componentDao().selectNullableByKey(dbSession, projectKey); + if (project == null) { + throw new NotFoundException(String.format("Project key '%s' not found", projectKey)); + } + + return project; + } + + private static String measureValueDescription() { + StringBuilder description = new StringBuilder("Measure value. Value type depends on metric type:"); + description.append("
    "); + for (Metric.ValueType metricType : Metric.ValueType.values()) { + description.append("
  • "); + description.append(String.format("%s - %s", metricType.description(), metricTypeWsDescription(metricType))); + description.append("
  • "); + } + description.append("
"); + + return description.toString(); + } + + private static String metricTypeWsDescription(Metric.ValueType metricType) { + switch (metricType) { + case BOOL: + return "the possible values are true or false"; + case INT: + case MILLISEC: + return "type: integer"; + case FLOAT: + case PERCENT: + case RATING: + return "type: double"; + case LEVEL: + return "the possible values are " + formattedMetricLevelNames(); + case STRING: + case DATA: + case DISTRIB: + return "type: string"; + case WORK_DUR: + return "duration format: 12d 5h 30min"; + default: + return "metric type not supported"; + } + } + + private static String formattedMetricLevelNames() { + return Joiner.on(", ").join(Metric.Level.names()); + } +} 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 f0c981e7e46..ef50f5e42b3 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 @@ -27,6 +27,7 @@ public class CustomMeasuresWsModule extends Module { protected void configureModule() { add( CustomMeasuresWs.class, - DeleteAction.class); + DeleteAction.class, + CreateAction.class); } } 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 50de63febca..8765806f735 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 @@ -63,7 +63,7 @@ public class MetricDao implements DaoComponent { public MetricDto selectByKey(DbSession session, String key) { MetricDto metric = selectNullableByKey(session, key); if (metric == null) { - throw new NotFoundException(String.format("Metric '%s' not found", key)); + throw new NotFoundException(String.format("Metric key '%s' not found", key)); } return metric; } @@ -129,4 +129,12 @@ public class MetricDao implements DaoComponent { public MetricDto selectNullableById(DbSession session, long id) { return mapper(session).selectById(id); } + + public MetricDto selectById(DbSession session, int id) { + MetricDto metric = mapper(session).selectById(id); + if (metric == null) { + throw new NotFoundException(String.format("Metric id '%d' not found", id)); + } + return metric; + } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/util/MetricLevelTypeValidation.java b/server/sonar-server/src/main/java/org/sonar/server/util/MetricLevelTypeValidation.java new file mode 100644 index 00000000000..4c964defc61 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/util/MetricLevelTypeValidation.java @@ -0,0 +1,43 @@ +/* + * 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.util; + +import java.util.List; +import javax.annotation.Nullable; +import org.sonar.api.PropertyType; +import org.sonar.api.measures.Metric; +import org.sonar.server.exceptions.BadRequestException; + +public class MetricLevelTypeValidation implements TypeValidation { + @Override + public String key() { + return PropertyType.METRIC_LEVEL.name(); + } + + @Override + public void validate(String value, @Nullable List options) { + try { + Metric.Level.valueOf(value); + } catch (IllegalArgumentException e) { + throw new BadRequestException("errors.type.notMetricLevel", value); + } + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/util/MetricWorkDurationTypeValidation.java b/server/sonar-server/src/main/java/org/sonar/server/util/MetricWorkDurationTypeValidation.java new file mode 100644 index 00000000000..c2bbf3c201c --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/util/MetricWorkDurationTypeValidation.java @@ -0,0 +1,49 @@ +/* + * 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.util; + +import java.util.List; +import javax.annotation.Nullable; +import org.sonar.api.PropertyType; +import org.sonar.api.utils.Durations; +import org.sonar.server.exceptions.BadRequestException; + +public class MetricWorkDurationTypeValidation implements TypeValidation { + private final Durations durations; + + public MetricWorkDurationTypeValidation(Durations durations) { + this.durations = durations; + } + + @Override + public String key() { + return PropertyType.METRIC_WORK_DURATION.name(); + } + + @Override + public void validate(String value, @Nullable List options) { + try { + durations.decode(value); + } catch (IllegalArgumentException e) { + throw new BadRequestException("errors.type.notMetricWorkDuration", value); + } + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/CreateActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/CreateActionTest.java new file mode 100644 index 00000000000..532d73e78b9 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/CreateActionTest.java @@ -0,0 +1,417 @@ +/* + * 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 java.util.Arrays; +import java.util.List; +import org.assertj.core.data.Offset; +import org.junit.After; +import org.junit.Before; +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.i18n.I18n; +import org.sonar.api.measures.Metric; +import org.sonar.api.measures.Metric.ValueType; +import org.sonar.api.utils.Durations; +import org.sonar.api.utils.System2; +import org.sonar.api.web.UserRole; +import org.sonar.core.measure.custom.db.CustomMeasureDto; +import org.sonar.core.metric.db.MetricDto; +import org.sonar.core.permission.GlobalPermissions; +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.exceptions.ForbiddenException; +import org.sonar.server.exceptions.NotFoundException; +import org.sonar.server.exceptions.ServerException; +import org.sonar.server.measure.custom.persistence.CustomMeasureDao; +import org.sonar.server.metric.persistence.MetricDao; +import org.sonar.server.metric.ws.MetricTesting; +import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.util.BooleanTypeValidation; +import org.sonar.server.util.FloatTypeValidation; +import org.sonar.server.util.IntegerTypeValidation; +import org.sonar.server.util.MetricLevelTypeValidation; +import org.sonar.server.util.MetricWorkDurationTypeValidation; +import org.sonar.server.util.TypeValidations; +import org.sonar.server.ws.WsTester; +import org.sonar.test.DbTests; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.data.Offset.offset; +import static org.mockito.Mockito.mock; + +@Category(DbTests.class) +public class CreateActionTest { + + private static final String DEFAULT_PROJECT_UUID = "project-uuid"; + private static final String DEFAULT_PROJECT_KEY = "project-key"; + @Rule + public UserSessionRule userSession = UserSessionRule.standalone(); + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @ClassRule + public static DbTester db = new DbTester(); + DbClient dbClient; + DbSession dbSession; + + WsTester ws; + + @Before + public void setUp() { + dbClient = new DbClient(db.database(), db.myBatis(), new CustomMeasureDao(), new MetricDao(), new ComponentDao()); + dbSession = dbClient.openSession(false); + Durations durations = new Durations(mock(Settings.class), mock(I18n.class)); + TypeValidations typeValidations = new TypeValidations(Arrays.asList(new BooleanTypeValidation(), new IntegerTypeValidation(), new FloatTypeValidation(), + new MetricLevelTypeValidation(), new MetricWorkDurationTypeValidation(durations))); + ws = new WsTester(new CustomMeasuresWs(new CreateAction(dbClient, userSession, System2.INSTANCE, typeValidations, durations))); + db.truncateTables(); + userSession.setGlobalPermissions(GlobalPermissions.SYSTEM_ADMIN); + } + + @After + public void tearDown() { + dbSession.close(); + } + + @Test + public void create_boolean_custom_measure_in_db() throws Exception { + MetricDto metric = insertMetricAndProject(ValueType.BOOL, DEFAULT_PROJECT_UUID); + + newRequest() + .setParam(CreateAction.PARAM_PROJECT_ID, DEFAULT_PROJECT_UUID) + .setParam(CreateAction.PARAM_METRIC_ID, metric.getId().toString()) + .setParam(CreateAction.PARAM_DESCRIPTION, "custom-measure-description") + .setParam(CreateAction.PARAM_VALUE, "true") + .execute(); + + List customMeasures = dbClient.customMeasureDao().selectByMetricId(dbSession, metric.getId()); + CustomMeasureDto customMeasure = customMeasures.get(0); + assertThat(customMeasures).hasSize(1); + assertThat(customMeasure.getDescription()).isEqualTo("custom-measure-description"); + assertThat(customMeasure.getTextValue()).isNullOrEmpty(); + assertThat(customMeasure.getValue()).isCloseTo(1.0d, offset(0.01d)); + assertThat(customMeasure.getComponentUuid()).isEqualTo(DEFAULT_PROJECT_UUID); + } + + @Test + public void create_int_custom_measure_in_db() throws Exception { + MetricDto metric = insertMetricAndProject(ValueType.INT, DEFAULT_PROJECT_UUID); + + newRequest() + .setParam(CreateAction.PARAM_PROJECT_ID, DEFAULT_PROJECT_UUID) + .setParam(CreateAction.PARAM_METRIC_ID, metric.getId().toString()) + .setParam(CreateAction.PARAM_VALUE, "42") + .execute(); + + CustomMeasureDto customMeasure = dbClient.customMeasureDao().selectByMetricId(dbSession, metric.getId()).get(0); + assertThat(customMeasure.getTextValue()).isNullOrEmpty(); + assertThat(customMeasure.getValue()).isCloseTo(42.0d, offset(0.01d)); + } + + @Test + public void create_text_custom_measure_in_db() throws Exception { + MetricDto metric = insertMetricAndProject(ValueType.STRING, DEFAULT_PROJECT_UUID); + + newRequest() + .setParam(CreateAction.PARAM_PROJECT_ID, DEFAULT_PROJECT_UUID) + .setParam(CreateAction.PARAM_METRIC_ID, metric.getId().toString()) + .setParam(CreateAction.PARAM_VALUE, "custom-measure-free-text") + .execute(); + + CustomMeasureDto customMeasure = dbClient.customMeasureDao().selectByMetricId(dbSession, metric.getId()).get(0); + assertThat(customMeasure.getTextValue()).isEqualTo("custom-measure-free-text"); + } + + @Test + public void create_text_custom_measure_as_project_admin() throws Exception { + MetricDto metric = insertMetricAndProject(ValueType.STRING, DEFAULT_PROJECT_UUID); + userSession.login("login").addProjectUuidPermissions(UserRole.ADMIN, DEFAULT_PROJECT_UUID); + + newRequest() + .setParam(CreateAction.PARAM_PROJECT_ID, DEFAULT_PROJECT_UUID) + .setParam(CreateAction.PARAM_METRIC_ID, metric.getId().toString()) + .setParam(CreateAction.PARAM_VALUE, "custom-measure-free-text") + .execute(); + + CustomMeasureDto customMeasure = dbClient.customMeasureDao().selectByMetricId(dbSession, metric.getId()).get(0); + assertThat(customMeasure).isNotNull(); + } + + @Test + public void create_text_custom_measure_with_metric_key() throws Exception { + MetricDto metric = insertMetricAndProject(ValueType.STRING, DEFAULT_PROJECT_UUID); + + newRequest() + .setParam(CreateAction.PARAM_PROJECT_ID, DEFAULT_PROJECT_UUID) + .setParam(CreateAction.PARAM_METRIC_KEY, metric.getKey()) + .setParam(CreateAction.PARAM_VALUE, "whatever-value") + .execute(); + + CustomMeasureDto customMeasure = dbClient.customMeasureDao().selectByMetricId(dbSession, metric.getId()).get(0); + assertThat(customMeasure).isNotNull(); + } + + @Test + public void create_text_custom_measure_with_project_key() throws Exception { + MetricDto metric = insertMetricAndProject(ValueType.STRING, DEFAULT_PROJECT_UUID); + + newRequest() + .setParam(CreateAction.PARAM_PROJECT_KEY, DEFAULT_PROJECT_KEY) + .setParam(CreateAction.PARAM_METRIC_ID, metric.getId().toString()) + .setParam(CreateAction.PARAM_VALUE, "whatever-value") + .execute(); + + CustomMeasureDto customMeasure = dbClient.customMeasureDao().selectByMetricId(dbSession, metric.getId()).get(0); + assertThat(customMeasure).isNotNull(); + } + + @Test + public void create_float_custom_measure_indb() throws Exception { + MetricDto metric = insertMetricAndProject(ValueType.FLOAT, DEFAULT_PROJECT_UUID); + + newRequest() + .setParam(CreateAction.PARAM_PROJECT_ID, DEFAULT_PROJECT_UUID) + .setParam(CreateAction.PARAM_METRIC_ID, metric.getId().toString()) + .setParam(CreateAction.PARAM_VALUE, "4.2") + .execute(); + + CustomMeasureDto customMeasure = dbClient.customMeasureDao().selectByMetricId(dbSession, metric.getId()).get(0); + assertThat(customMeasure.getValue()).isCloseTo(4.2d, Offset.offset(0.01d)); + assertThat(customMeasure.getTextValue()).isNullOrEmpty(); + } + + @Test + public void create_work_duration_custom_measure_in_db() throws Exception { + MetricDto metric = insertMetricAndProject(ValueType.WORK_DUR, DEFAULT_PROJECT_UUID); + + newRequest() + .setParam(CreateAction.PARAM_PROJECT_ID, DEFAULT_PROJECT_UUID) + .setParam(CreateAction.PARAM_METRIC_ID, metric.getId().toString()) + .setParam(CreateAction.PARAM_VALUE, "4h 12min") + .execute(); + + CustomMeasureDto customMeasure = dbClient.customMeasureDao().selectByMetricId(dbSession, metric.getId()).get(0); + assertThat(customMeasure.getTextValue()).isNullOrEmpty(); + assertThat(customMeasure.getValue()).isCloseTo(4 * 60 + 12, offset(0.01d)); + } + + @Test + public void create_level_type_custom_measure_in_db() throws Exception { + MetricDto metric = insertMetricAndProject(ValueType.LEVEL, DEFAULT_PROJECT_UUID); + + newRequest() + .setParam(CreateAction.PARAM_PROJECT_ID, DEFAULT_PROJECT_UUID) + .setParam(CreateAction.PARAM_METRIC_ID, metric.getId().toString()) + .setParam(CreateAction.PARAM_VALUE, Metric.Level.WARN.name()) + .execute(); + + CustomMeasureDto customMeasure = dbClient.customMeasureDao().selectByMetricId(dbSession, metric.getId()).get(0); + assertThat(customMeasure.getTextValue()).isEqualTo(Metric.Level.WARN.name()); + } + + @Test + public void response_with_object_and_id() throws Exception { + MetricDto metric = insertMetricAndProject(ValueType.STRING, DEFAULT_PROJECT_UUID); + + WsTester.Result response = newRequest() + .setParam(CreateAction.PARAM_PROJECT_ID, DEFAULT_PROJECT_UUID) + .setParam(CreateAction.PARAM_METRIC_ID, metric.getId().toString()) + .setParam(CreateAction.PARAM_DESCRIPTION, "custom-measure-description") + .setParam(CreateAction.PARAM_VALUE, "custom-measure-free-text") + .execute(); + + CustomMeasureDto customMeasure = dbClient.customMeasureDao().selectByMetricId(dbSession, metric.getId()).get(0); + response.assertJson(getClass(), "custom-measure.json"); + assertThat(response.outputAsString()).matches(String.format(".*\"id\"\\s*:\\s*\"%d\".*", customMeasure.getId())); + assertThat(response.outputAsString()).matches(String.format(".*\"metricId\"\\s*:\\s*\"%d\".*", metric.getId())); + } + + @Test + public void fail_when_get_request() throws Exception { + expectedException.expect(ServerException.class); + + ws.newGetRequest(CustomMeasuresWs.ENDPOINT, CreateAction.ACTION) + .setParam(CreateAction.PARAM_PROJECT_ID, DEFAULT_PROJECT_UUID) + .setParam(CreateAction.PARAM_METRIC_ID, "whatever-id") + .setParam(CreateAction.PARAM_VALUE, "custom-measure-free-text") + .execute(); + } + + @Test + public void fail_when_project_id_nor_project_key_provided() throws Exception { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("The project key or the project id must be provided, not both."); + MetricDto metric = insertMetricAndProject(ValueType.STRING, DEFAULT_PROJECT_UUID); + + newRequest() + .setParam(CreateAction.PARAM_METRIC_ID, "whatever-id") + .setParam(CreateAction.PARAM_VALUE, metric.getId().toString()) + .execute(); + } + + @Test + public void fail_when_project_id_and_project_key_are_provided() throws Exception { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("The project key or the project id must be provided, not both."); + MetricDto metric = insertMetricAndProject(ValueType.STRING, DEFAULT_PROJECT_UUID); + + newRequest() + .setParam(CreateAction.PARAM_PROJECT_ID, DEFAULT_PROJECT_UUID) + .setParam(CreateAction.PARAM_PROJECT_KEY, DEFAULT_PROJECT_KEY) + .setParam(CreateAction.PARAM_METRIC_ID, metric.getId().toString()) + .setParam(CreateAction.PARAM_VALUE, "whatever-value") + .execute(); + } + + @Test + public void fail_when_project_key_does_not_exist_in_db() throws Exception { + expectedException.expect(NotFoundException.class); + expectedException.expectMessage("Project key 'another-project-key' not found"); + insertMetricAndProject(ValueType.STRING, DEFAULT_PROJECT_UUID); + + newRequest() + .setParam(CreateAction.PARAM_PROJECT_KEY, "another-project-key") + .setParam(CreateAction.PARAM_METRIC_ID, "whatever-id") + .setParam(CreateAction.PARAM_VALUE, "whatever-value") + .execute(); + } + + @Test + public void fail_when_project_id_does_not_exist_in_db() throws Exception { + expectedException.expect(NotFoundException.class); + expectedException.expectMessage("Project id 'another-project-uuid' not found"); + MetricDto metric = insertMetricAndProject(ValueType.STRING, DEFAULT_PROJECT_UUID); + + newRequest() + .setParam(CreateAction.PARAM_PROJECT_ID, "another-project-uuid") + .setParam(CreateAction.PARAM_METRIC_ID, metric.getId().toString()) + .setParam(CreateAction.PARAM_VALUE, "whatever-value") + .execute(); + } + + @Test + public void fail_when_metric_id_nor_metric_key_is_provided() throws Exception { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("The metric id or the metric key must be provided, not both."); + insertMetricAndProject(ValueType.STRING, DEFAULT_PROJECT_UUID); + + newRequest() + .setParam(CreateAction.PARAM_PROJECT_ID, DEFAULT_PROJECT_UUID) + .setParam(CreateAction.PARAM_VALUE, "whatever-value") + .execute(); + } + + @Test + public void fail_when_metric_id_and_metric_key_are_provided() throws Exception { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("The metric id or the metric key must be provided, not both."); + MetricDto metric = insertMetricAndProject(ValueType.STRING, DEFAULT_PROJECT_UUID); + + newRequest() + .setParam(CreateAction.PARAM_PROJECT_ID, DEFAULT_PROJECT_UUID) + .setParam(CreateAction.PARAM_METRIC_ID, metric.getId().toString()) + .setParam(CreateAction.PARAM_METRIC_KEY, metric.getKey()) + .setParam(CreateAction.PARAM_VALUE, "whatever-value") + .execute(); + + } + + @Test + public void fail_when_metric_is_not_found_in_db() throws Exception { + dbClient.componentDao().insert(dbSession, ComponentTesting.newProjectDto(DEFAULT_PROJECT_UUID)); + dbSession.commit(); + + expectedException.expect(ServerException.class); + expectedException.expectMessage("Metric id '42' not found"); + + newRequest() + .setParam(CreateAction.PARAM_PROJECT_ID, DEFAULT_PROJECT_UUID) + .setParam(CreateAction.PARAM_METRIC_ID, "42") + .setParam(CreateAction.PARAM_VALUE, "whatever-value") + .execute(); + } + + @Test + public void fail_when_measure_already_exists_on_same_project_and_same_metric() throws Exception { + MetricDto metric = insertMetricAndProject(ValueType.STRING, DEFAULT_PROJECT_UUID); + + expectedException.expect(ServerException.class); + expectedException.expectMessage(String.format("A measure already exists for project id 'project-uuid' and metric id '%d'", metric.getId())); + + newRequest() + .setParam(CreateAction.PARAM_PROJECT_ID, DEFAULT_PROJECT_UUID) + .setParam(CreateAction.PARAM_METRIC_ID, metric.getId().toString()) + .setParam(CreateAction.PARAM_VALUE, "whatever-value") + .execute(); + newRequest() + .setParam(CreateAction.PARAM_PROJECT_ID, DEFAULT_PROJECT_UUID) + .setParam(CreateAction.PARAM_METRIC_ID, metric.getId().toString()) + .setParam(CreateAction.PARAM_VALUE, "whatever-value") + .execute(); + } + + @Test + public void fail_when_value_is_not_well_formatted() throws Exception { + MetricDto metric = insertMetricAndProject(ValueType.BOOL, DEFAULT_PROJECT_UUID); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Ill formatted value 'non-correct-boolean-value' for metric type 'Yes/No'"); + + newRequest() + .setParam(CreateAction.PARAM_PROJECT_ID, DEFAULT_PROJECT_UUID) + .setParam(CreateAction.PARAM_METRIC_ID, metric.getId().toString()) + .setParam(CreateAction.PARAM_VALUE, "non-correct-boolean-value") + .execute(); + } + + @Test + public void fail_when_not_enough_permission() throws Exception { + expectedException.expect(ForbiddenException.class); + userSession.login("login"); + MetricDto metric = insertMetricAndProject(ValueType.STRING, DEFAULT_PROJECT_UUID); + + newRequest() + .setParam(CreateAction.PARAM_PROJECT_ID, DEFAULT_PROJECT_UUID) + .setParam(CreateAction.PARAM_METRIC_ID, metric.getId().toString()) + .setParam(CreateAction.PARAM_VALUE, "whatever-value") + .execute(); + } + + private WsTester.TestRequest newRequest() { + return ws.newPostRequest(CustomMeasuresWs.ENDPOINT, CreateAction.ACTION); + } + + private MetricDto insertMetricAndProject(ValueType metricType, String projectUuid) { + MetricDto metric = MetricTesting.newMetricDto().setEnabled(true).setValueType(metricType.name()).setKey("metric-key"); + dbClient.metricDao().insert(dbSession, metric); + dbClient.componentDao().insert(dbSession, ComponentTesting.newProjectDto(projectUuid).setKey(DEFAULT_PROJECT_KEY)); + dbSession.commit(); + + return metric; + } +} 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 c0ed75c4b9b..098819b9cb0 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(4); + assertThat(container.size()).isEqualTo(5); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/CustomMeasuresWsTest.java b/server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/CustomMeasuresWsTest.java index d0a50285eba..4a7a34fed55 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/CustomMeasuresWsTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/CustomMeasuresWsTest.java @@ -23,8 +23,11 @@ package org.sonar.server.measure.custom.ws; import org.junit.Before; import org.junit.Test; import org.sonar.api.server.ws.WebService; +import org.sonar.api.utils.Durations; +import org.sonar.api.utils.System2; import org.sonar.server.db.DbClient; import org.sonar.server.user.UserSession; +import org.sonar.server.util.TypeValidations; import org.sonar.server.ws.WsTester; import static org.assertj.core.api.Assertions.assertThat; @@ -38,7 +41,8 @@ public class CustomMeasuresWsTest { DbClient dbClient = mock(DbClient.class); UserSession userSession = mock(UserSession.class); ws = new WsTester(new CustomMeasuresWs( - new DeleteAction(dbClient, userSession) + new DeleteAction(dbClient, userSession), + new CreateAction(dbClient, userSession, System2.INSTANCE, mock(TypeValidations.class), mock(Durations.class)) )); } @@ -47,7 +51,7 @@ public class CustomMeasuresWsTest { WebService.Controller controller = ws.controller("api/custom_measures"); assertThat(controller).isNotNull(); assertThat(controller.description()).isNotEmpty(); - assertThat(controller.actions()).hasSize(1); + assertThat(controller.actions()).hasSize(2); } @Test @@ -55,4 +59,10 @@ public class CustomMeasuresWsTest { WebService.Action deleteAction = ws.controller("api/custom_measures").action("delete"); assertThat(deleteAction.isPost()).isTrue(); } + + @Test + public void create_action_properties() { + WebService.Action action = ws.controller("api/custom_measures").action("create"); + assertThat(action.isPost()).isTrue(); + } } diff --git a/server/sonar-server/src/test/resources/org/sonar/server/measure/custom/ws/CreateActionTest/custom-measure.json b/server/sonar-server/src/test/resources/org/sonar/server/measure/custom/ws/CreateActionTest/custom-measure.json new file mode 100644 index 00000000000..80e9f1469e2 --- /dev/null +++ b/server/sonar-server/src/test/resources/org/sonar/server/measure/custom/ws/CreateActionTest/custom-measure.json @@ -0,0 +1,7 @@ +{ + "projectId": "project-uuid", + "projectKey": "project-key", + "metricKey": "metric-key", + "value": "custom-measure-free-text", + "description": "custom-measure-description" +} 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 ab0c89beb60..4b609b7fee2 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 @@ -35,4 +35,6 @@ public interface CustomMeasureMapper { List selectByComponentId(long id); void delete(long id); + + int countByComponentIdAndMetricId(@Param("componentUuid") String componentUuid, @Param("metricId") int metricId); } 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 a3c935db3dc..a3465ed2b90 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 @@ -59,4 +59,10 @@ delete from manual_measures where id=#{id} + + diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 3c36eb56776..ea25e50f369 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -2825,6 +2825,8 @@ errors.type.notBoolean=Value '{0}' must be one of "true" or "false". errors.type.notInteger=Value '{0}' must be an integer. errors.type.notFloat=Value '{0}' must be an floating point number. errors.type.notInOptions=Value '{0}' must be one of : {1}. +errors.type.notMetricLevel=Value '{0}' must be one of "OK", "WARN", "ERROR". +errors.type.notMetricWorkDuration=Value '{0}' is not well formatted. #------------------------------------------------------------------------------ # diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/PropertyType.java b/sonar-plugin-api/src/main/java/org/sonar/api/PropertyType.java index 292a298c7e2..06c2e867c3a 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/PropertyType.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/PropertyType.java @@ -83,8 +83,18 @@ public enum PropertyType { PROPERTY_SET, /** - * User login - * @since 5.1 + * User login + * @since 5.1 + */ + USER_LOGIN, + + /** + * Level metric type + */ + METRIC_LEVEL, + + /** + * Work duration metric type */ - USER_LOGIN + METRIC_WORK_DURATION } diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/measures/Metric.java b/sonar-plugin-api/src/main/java/org/sonar/api/measures/Metric.java index 987be1d33e4..b02bc030b8e 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/measures/Metric.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/measures/Metric.java @@ -19,8 +19,13 @@ */ package org.sonar.api.measures; +import com.google.common.base.Function; +import com.google.common.collect.Lists; import java.io.Serializable; +import java.util.Arrays; +import java.util.List; import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.persistence.Column; import javax.persistence.Entity; @@ -124,6 +129,16 @@ public class Metric implements Serializable, org.sonar.a public String getColorName() { return colorName; } + + public static List names() { + return Lists.transform(Arrays.asList(values()), new Function() { + @Nonnull + @Override + public String apply(@Nonnull Level level) { + return level.name(); + } + }); + } } @Id diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/utils/Durations.java b/sonar-plugin-api/src/main/java/org/sonar/api/utils/Durations.java index 4f88279c6d5..bfaa830b94a 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/utils/Durations.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/utils/Durations.java @@ -63,7 +63,8 @@ public class Durations { /** * Convert the text to a Duration *
- * Example : decode("9d 10 h") -> Duration.encode("10d2h") (if sonar.technicalDebt.hoursInDay property is set to 8) + * Example : decode("9d 10 h") -> Duration.encode("10d2h") (if sonar.technicalDebt.hoursInDay property is set to 8)
+ * Throws {@code IllegalArgumentException} */ public Duration decode(String duration) { return Duration.decode(duration, hoursInDay()); -- 2.39.5