Browse Source

SONAR-6610 WS custom_measures/create create a custom measure

tags/5.2-RC1
Teryk Bellahsene 9 years ago
parent
commit
1eea485b14
16 changed files with 895 additions and 13 deletions
  1. 8
    4
      server/sonar-server/src/main/java/org/sonar/server/measure/custom/persistence/CustomMeasureDao.java
  2. 307
    0
      server/sonar-server/src/main/java/org/sonar/server/measure/custom/ws/CreateAction.java
  3. 2
    1
      server/sonar-server/src/main/java/org/sonar/server/measure/custom/ws/CustomMeasuresWsModule.java
  4. 9
    1
      server/sonar-server/src/main/java/org/sonar/server/metric/persistence/MetricDao.java
  5. 43
    0
      server/sonar-server/src/main/java/org/sonar/server/util/MetricLevelTypeValidation.java
  6. 49
    0
      server/sonar-server/src/main/java/org/sonar/server/util/MetricWorkDurationTypeValidation.java
  7. 417
    0
      server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/CreateActionTest.java
  8. 1
    1
      server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/CustomMeasuresWsModuleTest.java
  9. 12
    2
      server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/CustomMeasuresWsTest.java
  10. 7
    0
      server/sonar-server/src/test/resources/org/sonar/server/measure/custom/ws/CreateActionTest/custom-measure.json
  11. 2
    0
      sonar-core/src/main/java/org/sonar/core/measure/custom/db/CustomMeasureMapper.java
  12. 6
    0
      sonar-core/src/main/resources/org/sonar/core/measure/custom/db/CustomMeasureMapper.xml
  13. 2
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties
  14. 13
    3
      sonar-plugin-api/src/main/java/org/sonar/api/PropertyType.java
  15. 15
    0
      sonar-plugin-api/src/main/java/org/sonar/api/measures/Metric.java
  16. 2
    1
      sonar-plugin-api/src/main/java/org/sonar/api/utils/Durations.java

+ 8
- 4
server/sonar-server/src/main/java/org/sonar/server/measure/custom/persistence/CustomMeasureDao.java View File

@@ -65,12 +65,16 @@ public class CustomMeasureDao implements DaoComponent {
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) {

+ 307
- 0
server/sonar-server/src/main/java/org/sonar/server/measure/custom/ws/CreateAction.java View File

@@ -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.<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());
}
}

+ 2
- 1
server/sonar-server/src/main/java/org/sonar/server/measure/custom/ws/CustomMeasuresWsModule.java View File

@@ -27,6 +27,7 @@ public class CustomMeasuresWsModule extends Module {
protected void configureModule() {
add(
CustomMeasuresWs.class,
DeleteAction.class);
DeleteAction.class,
CreateAction.class);
}
}

+ 9
- 1
server/sonar-server/src/main/java/org/sonar/server/metric/persistence/MetricDao.java View File

@@ -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;
}
}

+ 43
- 0
server/sonar-server/src/main/java/org/sonar/server/util/MetricLevelTypeValidation.java View File

@@ -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<String> options) {
try {
Metric.Level.valueOf(value);
} catch (IllegalArgumentException e) {
throw new BadRequestException("errors.type.notMetricLevel", value);
}
}
}

+ 49
- 0
server/sonar-server/src/main/java/org/sonar/server/util/MetricWorkDurationTypeValidation.java View File

@@ -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<String> options) {
try {
durations.decode(value);
} catch (IllegalArgumentException e) {
throw new BadRequestException("errors.type.notMetricWorkDuration", value);
}
}
}

+ 417
- 0
server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/CreateActionTest.java View File

@@ -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<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;
}
}

+ 1
- 1
server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/CustomMeasuresWsModuleTest.java View File

@@ -30,6 +30,6 @@ public class CustomMeasuresWsModuleTest {
public void verify_count_of_added_components() {
ComponentContainer container = new ComponentContainer();
new CustomMeasuresWsModule().configure(container);
assertThat(container.size()).isEqualTo(4);
assertThat(container.size()).isEqualTo(5);
}
}

+ 12
- 2
server/sonar-server/src/test/java/org/sonar/server/measure/custom/ws/CustomMeasuresWsTest.java View File

@@ -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();
}
}

+ 7
- 0
server/sonar-server/src/test/resources/org/sonar/server/measure/custom/ws/CreateActionTest/custom-measure.json View File

@@ -0,0 +1,7 @@
{
"projectId": "project-uuid",
"projectKey": "project-key",
"metricKey": "metric-key",
"value": "custom-measure-free-text",
"description": "custom-measure-description"
}

+ 2
- 0
sonar-core/src/main/java/org/sonar/core/measure/custom/db/CustomMeasureMapper.java View File

@@ -35,4 +35,6 @@ public interface CustomMeasureMapper {
List<CustomMeasureDto> selectByComponentId(long id);

void delete(long id);

int countByComponentIdAndMetricId(@Param("componentUuid") String componentUuid, @Param("metricId") int metricId);
}

+ 6
- 0
sonar-core/src/main/resources/org/sonar/core/measure/custom/db/CustomMeasureMapper.xml View File

@@ -59,4 +59,10 @@
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>

+ 2
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -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.

#------------------------------------------------------------------------------
#

+ 13
- 3
sonar-plugin-api/src/main/java/org/sonar/api/PropertyType.java View File

@@ -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
}

+ 15
- 0
sonar-plugin-api/src/main/java/org/sonar/api/measures/Metric.java View File

@@ -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<G extends Serializable> implements Serializable, org.sonar.a
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

+ 2
- 1
sonar-plugin-api/src/main/java/org/sonar/api/utils/Durations.java View File

@@ -63,7 +63,8 @@ public class Durations {
/**
* 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());

Loading…
Cancel
Save