@@ -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) { |
@@ -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()); | |||
} | |||
} |
@@ -27,6 +27,7 @@ public class CustomMeasuresWsModule extends Module { | |||
protected void configureModule() { | |||
add( | |||
CustomMeasuresWs.class, | |||
DeleteAction.class); | |||
DeleteAction.class, | |||
CreateAction.class); | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -0,0 +1,7 @@ | |||
{ | |||
"projectId": "project-uuid", | |||
"projectKey": "project-key", | |||
"metricKey": "metric-key", | |||
"value": "custom-measure-free-text", | |||
"description": "custom-measure-description" | |||
} |
@@ -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); | |||
} |
@@ -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> |
@@ -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. | |||
#------------------------------------------------------------------------------ | |||
# |
@@ -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 | |||
} |
@@ -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 |
@@ -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()); |