return customMeasure;
}
- public List<CustomMeasureDto> selectByMetricId(DbSession session, int id) {
- return mapper(session).selectByMetricId(id);
+ public List<CustomMeasureDto> selectByMetricId(DbSession session, int metricId) {
+ return mapper(session).selectByMetricId(metricId);
}
- public List<CustomMeasureDto> selectByComponentId(DbSession session, long id) {
- return mapper(session).selectByComponentId(id);
+ public List<CustomMeasureDto> 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) {
--- /dev/null
+/*
+ * 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.<br /> " +
+ "The project id or the project key must be provided. The metric id or the metric key must be provided.<br/>" +
+ "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("<ul>");
+ for (Metric.ValueType metricType : Metric.ValueType.values()) {
+ description.append("<li>");
+ description.append(String.format("%s - %s", metricType.description(), metricTypeWsDescription(metricType)));
+ description.append("</li>");
+ }
+ description.append("</ul>");
+
+ 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());
+ }
+}
protected void configureModule() {
add(
CustomMeasuresWs.class,
- DeleteAction.class);
+ DeleteAction.class,
+ CreateAction.class);
}
}
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;
}
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;
+ }
}
--- /dev/null
+/*
+ * 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<String> options) {
+ try {
+ Metric.Level.valueOf(value);
+ } catch (IllegalArgumentException e) {
+ throw new BadRequestException("errors.type.notMetricLevel", value);
+ }
+ }
+}
--- /dev/null
+/*
+ * 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<String> options) {
+ try {
+ durations.decode(value);
+ } catch (IllegalArgumentException e) {
+ throw new BadRequestException("errors.type.notMetricWorkDuration", value);
+ }
+ }
+}
--- /dev/null
+/*
+ * 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<CustomMeasureDto> 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;
+ }
+}
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);
}
}
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;
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))
));
}
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
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();
+ }
}
--- /dev/null
+{
+ "projectId": "project-uuid",
+ "projectKey": "project-key",
+ "metricKey": "metric-key",
+ "value": "custom-measure-free-text",
+ "description": "custom-measure-description"
+}
List<CustomMeasureDto> selectByComponentId(long id);
void delete(long id);
+
+ int countByComponentIdAndMetricId(@Param("componentUuid") String componentUuid, @Param("metricId") int metricId);
}
delete from manual_measures
where id=#{id}
</delete>
+
+ <select id="countByComponentIdAndMetricId" resultType="Integer">
+ select count(*)
+ from manual_measures m
+ where m.metric_id=#{metricId} and m.component_uuid=#{componentUuid}
+ </select>
</mapper>
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.
#------------------------------------------------------------------------------
#
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
}
*/
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;
public String getColorName() {
return colorName;
}
+
+ public static List<String> names() {
+ return Lists.transform(Arrays.asList(values()), new Function<Level, String>() {
+ @Nonnull
+ @Override
+ public String apply(@Nonnull Level level) {
+ return level.name();
+ }
+ });
+ }
}
@Id
/**
* Convert the text to a Duration
* <br>
- * 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)<br />
+ * Throws {@code IllegalArgumentException}
*/
public Duration decode(String duration) {
return Duration.decode(duration, hoursInDay());