From 1e9159e3b8dad9afa097718259866122a866b101 Mon Sep 17 00:00:00 2001 From: Julien Lancelot Date: Wed, 17 Aug 2016 18:00:26 +0200 Subject: [PATCH] SONAR-7969 Create /api/settings/values WS --- .../settings/ws/ListDefinitionsAction.java | 39 +- .../ws/SettingsWsComponentParameters.java | 76 ++++ .../server/settings/ws/SettingsWsModule.java | 4 +- .../server/settings/ws/ValuesAction.java | 184 ++++++++++ .../server/settings/ws/values-example.json | 36 ++ .../ws/ListDefinitionsActionTest.java | 12 +- .../server/settings/ws/SetActionTest.java | 16 +- .../settings/ws/SettingsWsModuleTest.java | 2 +- .../server/settings/ws/ValuesActionTest.java | 342 ++++++++++++++++++ .../org/sonar/db/property/PropertiesDao.java | 15 + .../sonar/db/property/PropertiesMapper.java | 2 + .../sonar/db/property/PropertyTesting.java | 98 +++++ .../sonar/db/property/PropertiesMapper.xml | 18 + .../sonar/db/property/PropertiesDaoTest.java | 80 +++- .../sonar/db/property/PropertyTesting.java | 32 -- sonar-ws/src/main/protobuf/ws-settings.proto | 18 + 16 files changed, 888 insertions(+), 86 deletions(-) create mode 100644 server/sonar-server/src/main/java/org/sonar/server/settings/ws/SettingsWsComponentParameters.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/settings/ws/ValuesAction.java create mode 100644 server/sonar-server/src/main/resources/org/sonar/server/settings/ws/values-example.json create mode 100644 server/sonar-server/src/test/java/org/sonar/server/settings/ws/ValuesActionTest.java create mode 100644 sonar-db/src/main/java/org/sonar/db/property/PropertyTesting.java delete mode 100644 sonar-db/src/test/java/org/sonar/db/property/PropertyTesting.java diff --git a/server/sonar-server/src/main/java/org/sonar/server/settings/ws/ListDefinitionsAction.java b/server/sonar-server/src/main/java/org/sonar/server/settings/ws/ListDefinitionsAction.java index 3baef74a380..b098a1c9c59 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/settings/ws/ListDefinitionsAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/settings/ws/ListDefinitionsAction.java @@ -28,34 +28,27 @@ import org.sonar.api.config.PropertyFieldDefinition; 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.web.UserRole; -import org.sonar.core.permission.GlobalPermissions; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.component.ComponentDto; -import org.sonar.server.component.ComponentFinder; -import org.sonar.server.user.UserSession; import org.sonarqube.ws.Settings; import org.sonarqube.ws.Settings.ListDefinitionsWsResponse; import static org.elasticsearch.common.Strings.isNullOrEmpty; -import static org.sonar.core.util.Uuids.UUID_EXAMPLE_01; +import static org.sonar.server.settings.ws.SettingsWsComponentParameters.PARAM_COMPONENT_ID; +import static org.sonar.server.settings.ws.SettingsWsComponentParameters.PARAM_COMPONENT_KEY; +import static org.sonar.server.settings.ws.SettingsWsComponentParameters.addComponentParameters; import static org.sonar.server.ws.WsUtils.writeProtobuf; public class ListDefinitionsAction implements SettingsWsAction { - private static final String PARAM_COMPONENT_ID = "componentId"; - private static final String PARAM_COMPONENT_KEY = "componentKey"; - private final DbClient dbClient; - private final ComponentFinder componentFinder; - private final UserSession userSession; + private final SettingsWsComponentParameters settingsWsComponentParameters; private final PropertyDefinitions propertyDefinitions; - public ListDefinitionsAction(DbClient dbClient, ComponentFinder componentFinder, UserSession userSession, PropertyDefinitions propertyDefinitions) { + public ListDefinitionsAction(DbClient dbClient, SettingsWsComponentParameters settingsWsComponentParameters, PropertyDefinitions propertyDefinitions) { this.dbClient = dbClient; - this.componentFinder = componentFinder; - this.userSession = userSession; + this.settingsWsComponentParameters = settingsWsComponentParameters; this.propertyDefinitions = propertyDefinitions; } @@ -73,13 +66,7 @@ public class ListDefinitionsAction implements SettingsWsAction { .setSince("6.1") .setHandler(this); - action.createParam(PARAM_COMPONENT_ID) - .setDescription("Component id") - .setExampleValue(UUID_EXAMPLE_01); - - action.createParam(PARAM_COMPONENT_KEY) - .setDescription("Component key") - .setExampleValue("my_component_key"); + addComponentParameters(action); } @Override @@ -150,15 +137,9 @@ public class ListDefinitionsAction implements SettingsWsAction { private String getQualifier(Request request) { DbSession dbSession = dbClient.openSession(false); try { - if (request.hasParam(PARAM_COMPONENT_ID) || request.hasParam(PARAM_COMPONENT_KEY)) { - ComponentDto component = componentFinder.getByUuidOrKey(dbSession, request.param(PARAM_COMPONENT_ID), request.param(PARAM_COMPONENT_KEY), - ComponentFinder.ParamNames.ID_AND_KEY); - userSession.checkComponentUuidPermission(UserRole.ADMIN, component.uuid()); - return component.qualifier(); - } else { - userSession.checkPermission(GlobalPermissions.SYSTEM_ADMIN); - return null; - } + ComponentDto component = settingsWsComponentParameters.getComponent(dbSession, request); + settingsWsComponentParameters.checkAdminPermission(component); + return component == null ? null : component.qualifier(); } finally { dbClient.closeSession(dbSession); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/settings/ws/SettingsWsComponentParameters.java b/server/sonar-server/src/main/java/org/sonar/server/settings/ws/SettingsWsComponentParameters.java new file mode 100644 index 00000000000..40a3e5fb9e1 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/settings/ws/SettingsWsComponentParameters.java @@ -0,0 +1,76 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program 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. + * + * This program 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.settings.ws; + +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.WebService; +import org.sonar.api.web.UserRole; +import org.sonar.core.permission.GlobalPermissions; +import org.sonar.db.DbSession; +import org.sonar.db.component.ComponentDto; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.user.UserSession; + +import static org.sonar.core.util.Uuids.UUID_EXAMPLE_01; +import static org.sonar.server.component.ComponentFinder.ParamNames.ID_AND_KEY; +import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001; + +public class SettingsWsComponentParameters { + + static final String PARAM_COMPONENT_ID = "componentId"; + static final String PARAM_COMPONENT_KEY = "componentKey"; + + private final ComponentFinder componentFinder; + private final UserSession userSession; + + public SettingsWsComponentParameters(ComponentFinder componentFinder, UserSession userSession) { + this.componentFinder = componentFinder; + this.userSession = userSession; + } + + static void addComponentParameters(WebService.NewAction action) { + action.createParam(PARAM_COMPONENT_ID) + .setDescription("Component id") + .setExampleValue(UUID_EXAMPLE_01); + + action.createParam(PARAM_COMPONENT_KEY) + .setDescription("Component key") + .setExampleValue(KEY_PROJECT_EXAMPLE_001); + } + + @CheckForNull + ComponentDto getComponent(DbSession dbSession, Request request) { + if (request.hasParam(PARAM_COMPONENT_ID) || request.hasParam(PARAM_COMPONENT_KEY)) { + return componentFinder.getByUuidOrKey(dbSession, request.param(PARAM_COMPONENT_ID), request.param(PARAM_COMPONENT_KEY), ID_AND_KEY); + } + return null; + } + + void checkAdminPermission(@Nullable ComponentDto component) { + if (component == null) { + userSession.checkPermission(GlobalPermissions.SYSTEM_ADMIN); + } else { + userSession.checkComponentUuidPermission(UserRole.ADMIN, component.uuid()); + } + } + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/settings/ws/SettingsWsModule.java b/server/sonar-server/src/main/java/org/sonar/server/settings/ws/SettingsWsModule.java index 006dc678b46..c5eb79e52e4 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/settings/ws/SettingsWsModule.java +++ b/server/sonar-server/src/main/java/org/sonar/server/settings/ws/SettingsWsModule.java @@ -27,6 +27,8 @@ public class SettingsWsModule extends Module { add( SettingsWs.class, SetAction.class, - ListDefinitionsAction.class); + SettingsWsComponentParameters.class, + ListDefinitionsAction.class, + ValuesAction.class); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/settings/ws/ValuesAction.java b/server/sonar-server/src/main/java/org/sonar/server/settings/ws/ValuesAction.java new file mode 100644 index 00000000000..83dec424f60 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/settings/ws/ValuesAction.java @@ -0,0 +1,184 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program 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. + * + * This program 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.settings.ws; + +import com.google.common.base.Splitter; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import org.sonar.api.config.PropertyDefinition; +import org.sonar.api.config.PropertyDefinitions; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.property.PropertyDto; +import org.sonarqube.ws.Settings; +import org.sonarqube.ws.Settings.ValuesWsResponse; + +import static org.elasticsearch.common.Strings.isNullOrEmpty; +import static org.sonar.server.settings.ws.SettingsWsComponentParameters.PARAM_COMPONENT_ID; +import static org.sonar.server.settings.ws.SettingsWsComponentParameters.PARAM_COMPONENT_KEY; +import static org.sonar.server.settings.ws.SettingsWsComponentParameters.addComponentParameters; +import static org.sonar.server.ws.WsUtils.writeProtobuf; + +public class ValuesAction implements SettingsWsAction { + + private static final Splitter MULTI_VALUE_SPLITTER = Splitter.on(","); + + static final String PARAM_KEYS = "keys"; + + private final DbClient dbClient; + private final SettingsWsComponentParameters settingsWsComponentParameters; + private final PropertyDefinitions propertyDefinitions; + + public ValuesAction(DbClient dbClient, SettingsWsComponentParameters settingsWsComponentParameters, PropertyDefinitions propertyDefinitions) { + this.dbClient = dbClient; + this.settingsWsComponentParameters = settingsWsComponentParameters; + this.propertyDefinitions = propertyDefinitions; + } + + @Override + public void define(WebService.NewController context) { + WebService.NewAction action = context.createAction("values") + .setDescription(String.format("Returns values of given properties.
" + + "If no value have been set for a property, then the default value is returned.
" + + "Either '%s' or '%s' could be provided, not both.
" + + "Requires one of the following permissions: " + + "", PARAM_COMPONENT_ID, PARAM_COMPONENT_KEY)) + .setResponseExample(getClass().getResource("values-example.json")) + .setSince("6.1") + .setHandler(this); + addComponentParameters(action); + action.createParam(PARAM_KEYS) + .setDescription("List of property keys") + .setRequired(true) + .setExampleValue("sonar.technicalDebt.hoursInDay,sonar.dbcleaner.cleanDirectory"); + } + + @Override + public void handle(Request request, Response response) throws Exception { + writeProtobuf(doHandle(request), request, response); + } + + private ValuesWsResponse doHandle(Request request) { + DbSession dbSession = dbClient.openSession(true); + try { + ComponentDto component = settingsWsComponentParameters.getComponent(dbSession, request); + settingsWsComponentParameters.checkAdminPermission(component); + Set keys = new HashSet<>(request.mandatoryParamAsStrings(PARAM_KEYS)); + + List definitions = getDefinitions(keys); + Map definitionsByKey = definitions.stream() + .collect(Collectors.toMap(PropertyDefinition::key, Function.identity())); + + ValuesWsResponse.Builder valuesBuilder = ValuesWsResponse.newBuilder(); + new ValuesBuilder(dbSession, valuesBuilder, definitionsByKey, keys, component).build(); + return valuesBuilder.build(); + } finally { + dbClient.closeSession(dbSession); + } + } + + private class ValuesBuilder { + private final DbSession dbSession; + private final ValuesWsResponse.Builder valuesWsBuilder; + private final Map definitionsByKey; + private final Set keys; + private final ComponentDto component; + + private final Map valueBuilderByKey = new HashMap<>(); + + ValuesBuilder(DbSession dbSession, ValuesWsResponse.Builder valuesWsBuilder, Map definitionsByKey, + Set keys, @Nullable ComponentDto component) { + this.dbSession = dbSession; + this.valuesWsBuilder = valuesWsBuilder; + this.definitionsByKey = definitionsByKey; + this.keys = keys; + this.component = component; + } + + void build() { + processDefinitions(); + processPropertyDtos(dbClient.propertiesDao().selectGlobalPropertiesByKeys(dbSession, keys)); + if (component != null) { + processPropertyDtos(dbClient.propertiesDao().selectComponentPropertiesByKeys(dbSession, keys, component.getId())); + } + valueBuilderByKey.values().forEach(Settings.Value.Builder::build); + } + + private void processDefinitions() { + definitionsByKey.values().stream() + .filter(defaultProperty -> !isNullOrEmpty(defaultProperty.defaultValue())) + .forEach(this::processDefaultValue); + } + + private void processDefaultValue(PropertyDefinition definition) { + Settings.Value.Builder valueBuilder = valuesWsBuilder.addValuesBuilder() + .setKey(definition.key()) + .setIsDefault(true); + setValue(valueBuilder, definition.defaultValue()); + valueBuilderByKey.put(definition.key(), valueBuilder); + } + + private void processPropertyDtos(List properties) { + properties.stream() + .filter(propertyDto -> !isNullOrEmpty(propertyDto.getValue())) + .forEach(this::processDtoValue); + } + + private void processDtoValue(PropertyDto property) { + Settings.Value.Builder valueBuilder = valueBuilderByKey.get(property.getKey()); + if (valueBuilder == null) { + valueBuilder = valuesWsBuilder.addValuesBuilder().setKey(property.getKey()); + valueBuilderByKey.put(property.getKey(), valueBuilder); + } + valueBuilder.setIsInherited(component != null && property.getResourceId() == null); + valueBuilder.setIsDefault(false); + setValue(valueBuilder, property.getValue()); + } + + private void setValue(Settings.Value.Builder valueBuilder, String value) { + PropertyDefinition definition = definitionsByKey.get(valueBuilder.getKey()); + if (definition != null && definition.multiValues()) { + valueBuilder.addAllValues(MULTI_VALUE_SPLITTER.split(value)); + } else { + valueBuilder.setValue(value); + } + } + } + + private List getDefinitions(Set keys) { + return propertyDefinitions.getAll().stream() + .filter(def -> keys.contains(def.key())) + .collect(Collectors.toList()); + } + +} diff --git a/server/sonar-server/src/main/resources/org/sonar/server/settings/ws/values-example.json b/server/sonar-server/src/main/resources/org/sonar/server/settings/ws/values-example.json new file mode 100644 index 00000000000..11d7e50851d --- /dev/null +++ b/server/sonar-server/src/main/resources/org/sonar/server/settings/ws/values-example.json @@ -0,0 +1,36 @@ +{ + "values": [ + { + "key": "sonar.test.jira", + "value": "abc", + "isDefault": true + }, + { + "key": "sonar.autogenerated", + "values": [ + "val1", + "val2", + "val3" + ], + "isDefault": false + }, + { + "key": "sonar.demo", + "setValues": { + "text": { + "values": [ + "foo", + "bar" + ] + }, + "boolean": { + "values": [ + "true", + "false" + ] + } + }, + "isDefault": false + } + ] +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/settings/ws/ListDefinitionsActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/settings/ws/ListDefinitionsActionTest.java index 47ebd860da5..c670f788ad5 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/settings/ws/ListDefinitionsActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/settings/ws/ListDefinitionsActionTest.java @@ -55,6 +55,7 @@ import static org.sonar.api.web.UserRole.USER; import static org.sonar.core.permission.GlobalPermissions.DASHBOARD_SHARING; import static org.sonar.core.permission.GlobalPermissions.SYSTEM_ADMIN; import static org.sonar.db.component.ComponentTesting.newProjectDto; +import static org.sonarqube.ws.MediaTypes.JSON; import static org.sonarqube.ws.Settings.Type.BOOLEAN; import static org.sonarqube.ws.Settings.Type.PROPERTY_SET; import static org.sonarqube.ws.Settings.Type.SINGLE_SELECT_LIST; @@ -78,12 +79,13 @@ public class ListDefinitionsActionTest { ComponentDto project; PropertyDefinitions propertyDefinitions = new PropertyDefinitions(); + SettingsWsComponentParameters settingsWsComponentParameters = new SettingsWsComponentParameters(new ComponentFinder(dbClient), userSession); - WsActionTester ws = new WsActionTester(new ListDefinitionsAction(dbClient, new ComponentFinder(dbClient), userSession, propertyDefinitions)); + WsActionTester ws = new WsActionTester(new ListDefinitionsAction(dbClient, settingsWsComponentParameters, propertyDefinitions)); @Before public void setUp() throws Exception { - project = insertProject(); + project = componentDb.insertComponent(newProjectDto()); } @Test @@ -369,14 +371,10 @@ public class ListDefinitionsActionTest { .build()) .build())); - String result = ws.newRequest().setMediaType(MediaTypes.JSON).execute().getInput(); + String result = ws.newRequest().setMediaType(JSON).execute().getInput(); JsonAssert.assertJson(ws.getDef().responseExampleAsString()).isSimilarTo(result); } - private ComponentDto insertProject() { - return componentDb.insertComponent(newProjectDto()); - } - private ListDefinitionsWsResponse newRequest() { return newRequest(null, null); } diff --git a/server/sonar-server/src/test/java/org/sonar/server/settings/ws/SetActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/settings/ws/SetActionTest.java index 5a5b8eae291..aa8cb63e548 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/settings/ws/SetActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/settings/ws/SetActionTest.java @@ -48,8 +48,8 @@ import org.sonar.server.ws.WsActionTester; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.groups.Tuple.tuple; -import static org.sonar.db.property.PropertyTesting.newGlobalProperty; -import static org.sonar.db.property.PropertyTesting.newProjectProperty; +import static org.sonar.db.property.PropertyTesting.newComponentPropertyDto; +import static org.sonar.db.property.PropertyTesting.newGlobalPropertyDto; public class SetActionTest { @@ -90,7 +90,7 @@ public class SetActionTest { @Test public void update_existing_global_property() { - propertyDb.insertProperty(newGlobalProperty("my.key", "my value")); + propertyDb.insertProperty(newGlobalPropertyDto("my.key", "my value")); assertGlobalProperty("my.key", "my value"); callForGlobalProperty("my.key", "my new value"); @@ -100,7 +100,7 @@ public class SetActionTest { @Test public void persist_new_project_property() { - propertyDb.insertProperty(newGlobalProperty("my.key", "my global value")); + propertyDb.insertProperty(newGlobalPropertyDto("my.key", "my global value")); ComponentDto project = componentDb.insertProject(); callForProjectPropertyByUuid("my.key", "my project value", project.uuid()); @@ -121,9 +121,9 @@ public class SetActionTest { @Test public void update_existing_project_property() { - propertyDb.insertProperty(newGlobalProperty("my.key", "my global value")); + propertyDb.insertProperty(newGlobalPropertyDto("my.key", "my global value")); ComponentDto project = componentDb.insertProject(); - propertyDb.insertProperty(newProjectProperty("my.key", "my project value", project.getId())); + propertyDb.insertProperty(newComponentPropertyDto("my.key", "my project value", project)); assertProjectProperty("my.key", "my project value", project.getId()); callForProjectPropertyByKey("my.key", "my new project value", project.key()); @@ -133,8 +133,8 @@ public class SetActionTest { @Test public void user_property_is_not_updated() { - propertyDb.insertProperty(newGlobalProperty("my.key", "my user value").setUserId(42L)); - propertyDb.insertProperty(newGlobalProperty("my.key", "my global value")); + propertyDb.insertProperty(newGlobalPropertyDto("my.key", "my user value").setUserId(42L)); + propertyDb.insertProperty(newGlobalPropertyDto("my.key", "my global value")); callForGlobalProperty("my.key", "my new global value"); diff --git a/server/sonar-server/src/test/java/org/sonar/server/settings/ws/SettingsWsModuleTest.java b/server/sonar-server/src/test/java/org/sonar/server/settings/ws/SettingsWsModuleTest.java index d2f1b858c0b..3b9185d8b55 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/settings/ws/SettingsWsModuleTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/settings/ws/SettingsWsModuleTest.java @@ -29,6 +29,6 @@ public class SettingsWsModuleTest { public void verify_count_of_added_components() { ComponentContainer container = new ComponentContainer(); new SettingsWsModule().configure(container); - assertThat(container.size()).isEqualTo(3 + 2); + assertThat(container.size()).isEqualTo(5 + 2); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/settings/ws/ValuesActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/settings/ws/ValuesActionTest.java new file mode 100644 index 00000000000..792005321e7 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/settings/ws/ValuesActionTest.java @@ -0,0 +1,342 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program 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. + * + * This program 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.settings.ws; + +import com.google.common.base.Joiner; +import java.io.IOException; +import javax.annotation.Nullable; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.config.PropertyDefinition; +import org.sonar.api.config.PropertyDefinitions; +import org.sonar.api.server.ws.WebService; +import org.sonar.api.utils.System2; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.DbTester; +import org.sonar.db.component.ComponentDbTester; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.property.PropertyDto; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.exceptions.ForbiddenException; +import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.ws.TestRequest; +import org.sonar.server.ws.WsActionTester; +import org.sonar.test.JsonAssert; +import org.sonarqube.ws.MediaTypes; +import org.sonarqube.ws.Settings; +import org.sonarqube.ws.Settings.ValuesWsResponse; + +import static org.assertj.core.api.Java6Assertions.assertThat; +import static org.sonar.api.web.UserRole.ADMIN; +import static org.sonar.api.web.UserRole.USER; +import static org.sonar.core.permission.GlobalPermissions.DASHBOARD_SHARING; +import static org.sonar.core.permission.GlobalPermissions.SYSTEM_ADMIN; +import static org.sonar.db.component.ComponentTesting.newProjectDto; +import static org.sonar.db.property.PropertyTesting.newComponentPropertyDto; +import static org.sonar.db.property.PropertyTesting.newGlobalPropertyDto; +import static org.sonarqube.ws.MediaTypes.JSON; + +public class ValuesActionTest { + + static Joiner COMMA_JOINER = Joiner.on(","); + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Rule + public UserSessionRule userSession = UserSessionRule.standalone(); + + @Rule + public DbTester db = DbTester.create(System2.INSTANCE); + + DbClient dbClient = db.getDbClient(); + DbSession dbSession = db.getSession(); + ComponentDbTester componentDb = new ComponentDbTester(db); + SettingsWsComponentParameters settingsWsComponentParameters = new SettingsWsComponentParameters(new ComponentFinder(dbClient), userSession); + PropertyDefinitions propertyDefinitions = new PropertyDefinitions(); + + ComponentDto project; + + WsActionTester ws = new WsActionTester(new ValuesAction(dbClient, settingsWsComponentParameters, propertyDefinitions)); + + @Before + public void setUp() throws Exception { + project = componentDb.insertComponent(newProjectDto()); + } + + @Test + public void return_simple_value() throws Exception { + setUserAsSystemAdmin(); + propertyDefinitions.addComponent(PropertyDefinition + .builder("foo") + .build()); + insertProperties(newGlobalPropertyDto().setKey("foo").setValue("one")); + + ValuesWsResponse result = newRequestForGlobalProperties("foo"); + assertThat(result.getValuesList()).hasSize(1); + + Settings.Value value = result.getValues(0); + assertThat(value.getKey()).isEqualTo("foo"); + assertThat(value.getValue()).isEqualTo("one"); + assertThat(value.getValuesCount()).isZero(); + assertThat(value.getSetValues()).isEmpty(); + assertThat(value.getIsDefault()).isFalse(); + } + + @Test + public void return_multi_values() throws Exception { + setUserAsSystemAdmin(); + + // Property never defined, default value is returned + propertyDefinitions.addComponent(PropertyDefinition.builder("default") + .multiValues(true) + .defaultValue("one,two") + .build()); + + // Property defined at global level + propertyDefinitions.addComponent(PropertyDefinition.builder("global") + .multiValues(true) + .build()); + insertProperties(newGlobalPropertyDto().setKey("global").setValue("three,four")); + + ValuesWsResponse result = newRequestForGlobalProperties("default", "global"); + assertThat(result.getValuesList()).hasSize(2); + + Settings.Value foo = result.getValues(0); + assertThat(foo.getKey()).isEqualTo("default"); + assertThat(foo.hasValue()).isFalse(); + assertThat(foo.getValuesList()).containsOnly("one", "two"); + assertThat(foo.getSetValues()).isEmpty(); + + Settings.Value bar = result.getValues(1); + assertThat(bar.getKey()).isEqualTo("global"); + assertThat(bar.hasValue()).isFalse(); + assertThat(bar.getValuesList()).containsOnly("three", "four"); + assertThat(bar.getSetValues()).isEmpty(); + } + + @Test + public void return_default_values() throws Exception { + setUserAsSystemAdmin(); + propertyDefinitions.addComponent(PropertyDefinition + .builder("foo") + .defaultValue("default") + .build()); + + ValuesWsResponse result = newRequestForGlobalProperties("foo"); + assertThat(result.getValuesList()).hasSize(1); + + Settings.Value value = result.getValues(0); + assertThat(value.getKey()).isEqualTo("foo"); + assertThat(value.getValue()).isEqualTo("default"); + assertThat(value.getIsDefault()).isTrue(); + assertThat(value.getIsInherited()).isFalse(); + } + + @Test + public void return_global_values() throws Exception { + setUserAsSystemAdmin(); + propertyDefinitions.addComponent(PropertyDefinition.builder("property").defaultValue("default").build()); + insertProperties( + // The property is overriding default value + newGlobalPropertyDto().setKey("property").setValue("one")); + + ValuesWsResponse result = newRequestForGlobalProperties("property"); + assertThat(result.getValuesList()).hasSize(1); + + Settings.Value globalPropertyValue = result.getValues(0); + assertThat(globalPropertyValue.getKey()).isEqualTo("property"); + assertThat(globalPropertyValue.getValue()).isEqualTo("one"); + assertThat(globalPropertyValue.getIsDefault()).isFalse(); + assertThat(globalPropertyValue.getIsInherited()).isFalse(); + } + + @Test + public void return_component_values() throws Exception { + setUserAsSystemAdmin(); + propertyDefinitions.addComponent(PropertyDefinition.builder("property").defaultValue("default").build()); + insertProperties( + newGlobalPropertyDto().setKey("property").setValue("one"), + // The property is overriding global value + newComponentPropertyDto(project).setKey("property").setValue("two")); + + ValuesWsResponse result = newRequestForProjectProperties("property"); + assertThat(result.getValuesList()).hasSize(1); + + Settings.Value globalPropertyValue = result.getValues(0); + assertThat(globalPropertyValue.getKey()).isEqualTo("property"); + assertThat(globalPropertyValue.getValue()).isEqualTo("two"); + assertThat(globalPropertyValue.getIsDefault()).isFalse(); + assertThat(globalPropertyValue.getIsInherited()).isFalse(); + } + + @Test + public void return_is_inherited_to_true_when_property_is_defined_only_at_global_level() throws Exception { + setUserAsSystemAdmin(); + propertyDefinitions.addComponent(PropertyDefinition.builder("property").defaultValue("default").build()); + // The property is not defined on project + insertProperties(newGlobalPropertyDto().setKey("property").setValue("one")); + + ValuesWsResponse result = newRequestForProjectProperties("property"); + assertThat(result.getValuesList()).hasSize(1); + + Settings.Value globalPropertyValue = result.getValues(0); + assertThat(globalPropertyValue.getKey()).isEqualTo("property"); + assertThat(globalPropertyValue.getValue()).isEqualTo("one"); + assertThat(globalPropertyValue.getIsDefault()).isFalse(); + assertThat(globalPropertyValue.getIsInherited()).isTrue(); + } + + @Test + public void return_values_even_if_no_property_definition() throws Exception { + setUserAsSystemAdmin(); + insertProperties(newGlobalPropertyDto().setKey("globalPropertyWithoutDefinition").setValue("value")); + + ValuesWsResponse result = newRequestForGlobalProperties("globalPropertyWithoutDefinition"); + Settings.Value globalPropertyWithoutDefinitionValue = result.getValues(0); + assertThat(globalPropertyWithoutDefinitionValue.getKey()).isEqualTo("globalPropertyWithoutDefinition"); + assertThat(globalPropertyWithoutDefinitionValue.getValue()).isEqualTo("value"); + assertThat(globalPropertyWithoutDefinitionValue.getIsDefault()).isFalse(); + } + + @Test + public void return_empty_when_property_def_exists_but_no_value() throws Exception { + setUserAsSystemAdmin(); + propertyDefinitions.addComponent(PropertyDefinition + .builder("foo") + .build()); + insertProperties(newGlobalPropertyDto().setKey("bar").setValue("")); + + ValuesWsResponse result = newRequestForGlobalProperties("foo", "bar"); + assertThat(result.getValuesList()).isEmpty(); + } + + @Test + public void does_return_nothing_when_unknown_keys() throws Exception { + setUserAsSystemAdmin(); + propertyDefinitions.addComponent(PropertyDefinition + .builder("foo") + .defaultValue("default") + .build()); + insertProperties(newGlobalPropertyDto().setKey("bar").setValue("")); + + ValuesWsResponse result = newRequestForGlobalProperties("unknown"); + assertThat(result.getValuesList()).isEmpty(); + } + + @Test + @Ignore + public void test_example_json_response() { + setUserAsSystemAdmin(); + propertyDefinitions.addComponent(PropertyDefinition + .builder("sonar.test.jira") + .defaultValue("abc") + .build()); + + String result = ws.newRequest() + .setParam("keys", "sonar.test.jira,sonar.autogenerated,sonar.demo") + .setMediaType(JSON) + .execute() + .getInput(); + JsonAssert.assertJson(ws.getDef().responseExampleAsString()).isSimilarTo(result); + } + + @Test + public void fail_when_id_and_key_are_set() throws Exception { + setUserAsProjectAdmin(); + + expectedException.expect(IllegalArgumentException.class); + newRequest(project.uuid(), project.key()); + } + + @Test + public void fail_when_not_system_admin() throws Exception { + userSession.login("not-admin").setGlobalPermissions(DASHBOARD_SHARING); + propertyDefinitions.addComponent(PropertyDefinition.builder("foo").build()); + + expectedException.expect(ForbiddenException.class); + newRequestForGlobalProperties(); + } + + @Test + public void fail_when_not_project_admin() throws Exception { + userSession.login("project-admin").addProjectUuidPermissions(USER, project.uuid()); + propertyDefinitions.addComponent(PropertyDefinition.builder("foo").build()); + + expectedException.expect(ForbiddenException.class); + newRequest(project.uuid(), null); + } + + @Test + public void test_ws_definition() { + WebService.Action action = ws.getDef(); + assertThat(action).isNotNull(); + assertThat(action.isInternal()).isFalse(); + assertThat(action.isPost()).isFalse(); + assertThat(action.responseExampleAsString()).isNotEmpty(); + assertThat(action.params()).hasSize(3); + } + + private ValuesWsResponse newRequestForProjectProperties(String... keys) { + return newRequest(project.uuid(), null, keys); + } + + private ValuesWsResponse newRequestForGlobalProperties(String... keys) { + return newRequest(null, null, keys); + } + + private ValuesWsResponse newRequest(@Nullable String id, @Nullable String key, String... keys) { + TestRequest request = ws.newRequest() + .setMediaType(MediaTypes.PROTOBUF) + .setParam("keys", COMMA_JOINER.join(keys)); + if (id != null) { + request.setParam("componentId", id); + } + if (key != null) { + request.setParam("componentKey", key); + } + try { + return ValuesWsResponse.parseFrom(request.execute().getInputStream()); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + private void setUserAsSystemAdmin() { + userSession.login("admin").setGlobalPermissions(SYSTEM_ADMIN); + } + + private void setUserAsProjectAdmin() { + userSession.login("project-admin").addProjectUuidPermissions(ADMIN, project.uuid()); + } + + private void insertProperties(PropertyDto... properties) { + for (PropertyDto propertyDto : properties) { + dbClient.propertiesDao().insertProperty(dbSession, propertyDto); + } + dbSession.commit(); + } + +} diff --git a/sonar-db/src/main/java/org/sonar/db/property/PropertiesDao.java b/sonar-db/src/main/java/org/sonar/db/property/PropertiesDao.java index b7325cee8a3..140d7edf142 100644 --- a/sonar-db/src/main/java/org/sonar/db/property/PropertiesDao.java +++ b/sonar-db/src/main/java/org/sonar/db/property/PropertiesDao.java @@ -28,6 +28,7 @@ import java.sql.SQLException; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Set; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.apache.commons.dbutils.DbUtils; @@ -38,6 +39,8 @@ import org.sonar.db.DatabaseUtils; import org.sonar.db.DbSession; import org.sonar.db.MyBatis; +import static org.sonar.db.DatabaseUtils.executeLargeInputs; + public class PropertiesDao implements Dao { private static final String NOTIFICATION_PREFIX = "notification."; @@ -161,6 +164,18 @@ public class PropertiesDao implements Dao { return session.getMapper(PropertiesMapper.class).selectByQuery(query); } + public List selectGlobalPropertiesByKeys(DbSession session, Set keys) { + return selectByKeys(session, keys, null); + } + + public List selectComponentPropertiesByKeys(DbSession session, Set keys, long componentId) { + return selectByKeys(session, keys, componentId); + } + + private List selectByKeys(DbSession session, Set keys, @Nullable Long componentId) { + return executeLargeInputs(keys, propertyKeys -> session.getMapper(PropertiesMapper.class).selectByKeys(propertyKeys, componentId)); + } + public void insertProperty(DbSession session, PropertyDto property) { PropertiesMapper mapper = session.getMapper(PropertiesMapper.class); PropertyDto persistedProperty = mapper.selectByKey(property); diff --git a/sonar-db/src/main/java/org/sonar/db/property/PropertiesMapper.java b/sonar-db/src/main/java/org/sonar/db/property/PropertiesMapper.java index a90fed439af..e8ef34a84da 100644 --- a/sonar-db/src/main/java/org/sonar/db/property/PropertiesMapper.java +++ b/sonar-db/src/main/java/org/sonar/db/property/PropertiesMapper.java @@ -39,6 +39,8 @@ public interface PropertiesMapper { PropertyDto selectByKey(PropertyDto key); + List selectByKeys(@Param("keys") List keys, @Nullable @Param("componentId") Long componentId); + List selectByQuery(@Param("query") PropertyQuery query); List selectDescendantModuleProperties(@Param("moduleUuid") String moduleUuid, @Param(value = "scope") String scope, diff --git a/sonar-db/src/main/java/org/sonar/db/property/PropertyTesting.java b/sonar-db/src/main/java/org/sonar/db/property/PropertyTesting.java new file mode 100644 index 00000000000..88ab57acd12 --- /dev/null +++ b/sonar-db/src/main/java/org/sonar/db/property/PropertyTesting.java @@ -0,0 +1,98 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program 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. + * + * This program 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.db.property; + +import javax.annotation.Nullable; +import org.apache.commons.lang.math.RandomUtils; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.user.UserDto; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class PropertyTesting { + + private static int cursor = RandomUtils.nextInt(100); + + private PropertyTesting() { + // static methods only + } + + public static PropertyDto newGlobalPropertyDto(String key, String value) { + return newPropertyDto(key, value, (Long) null, null); + } + + public static PropertyDto newGlobalPropertyDto() { + return newPropertyDto((Long) null, null); + } + + public static PropertyDto newComponentPropertyDto(String key, String value, ComponentDto component) { + checkNotNull(component.getId()); + return newPropertyDto(key, value, component.getId(), null); + } + + public static PropertyDto newComponentPropertyDto(ComponentDto component) { + checkNotNull(component.getId()); + return newPropertyDto(component.getId(), null); + } + + public static PropertyDto newUserPropertyDto(String key, String value, UserDto user) { + checkNotNull(user.getId()); + return newPropertyDto(key, value, null, user.getId()); + } + + public static PropertyDto newUserPropertyDto(UserDto user) { + checkNotNull(user.getId()); + return newPropertyDto(null, user.getId()); + } + + public static PropertyDto newPropertyDto(String key, String value, ComponentDto component, UserDto user) { + checkNotNull(component.getId()); + checkNotNull(user.getId()); + return newPropertyDto(key, value, component.getId(), user.getId()); + } + + public static PropertyDto newPropertyDto(ComponentDto component, UserDto user) { + checkNotNull(component.getId()); + checkNotNull(user.getId()); + return newPropertyDto(component.getId(), user.getId()); + } + + private static PropertyDto newPropertyDto(@Nullable Long componentId, @Nullable Long userId) { + String key = String.valueOf(cursor); + cursor++; + String value = String.valueOf(cursor); + cursor++; + return newPropertyDto(key, value, componentId, userId); + } + + private static PropertyDto newPropertyDto(String key, String value, @Nullable Long componentId, @Nullable Long userId) { + PropertyDto propertyDto = new PropertyDto() + .setKey(key) + .setValue(value); + if (componentId != null) { + propertyDto.setResourceId(componentId); + } + if (userId != null) { + propertyDto.setUserId(userId); + } + return propertyDto; + } + +} diff --git a/sonar-db/src/main/resources/org/sonar/db/property/PropertiesMapper.xml b/sonar-db/src/main/resources/org/sonar/db/property/PropertiesMapper.xml index afb6f9d5c1d..a64c3756236 100644 --- a/sonar-db/src/main/resources/org/sonar/db/property/PropertiesMapper.xml +++ b/sonar-db/src/main/resources/org/sonar/db/property/PropertiesMapper.xml @@ -81,6 +81,24 @@ + +