From f4d9bd4f139e9300dc08adceb370b660b1ec220b Mon Sep 17 00:00:00 2001 From: Julien Lancelot Date: Mon, 22 Aug 2016 18:45:37 +0200 Subject: [PATCH] SONAR-7975 Return inherited values in /api/settings/values for modules --- .../org/sonar/server/settings/ws/Setting.java | 112 +++++++++ .../server/settings/ws/SettingsFinder.java | 107 ++++++++ .../server/settings/ws/SettingsWsModule.java | 3 +- .../server/settings/ws/ValuesAction.java | 237 +++++------------- .../settings/ws/SettingsFinderTest.java | 208 +++++++++++++++ .../settings/ws/SettingsWsModuleTest.java | 2 +- .../server/settings/ws/ValuesActionTest.java | 83 ++++-- .../org/sonar/db/property/PropertiesDao.java | 9 +- .../sonar/db/property/PropertiesMapper.java | 2 + .../sonar/db/property/PropertiesMapper.xml | 16 ++ .../sonar/db/property/PropertiesDaoTest.java | 47 +++- 11 files changed, 624 insertions(+), 202 deletions(-) create mode 100644 server/sonar-server/src/main/java/org/sonar/server/settings/ws/Setting.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/settings/ws/SettingsFinder.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/settings/ws/SettingsFinderTest.java diff --git a/server/sonar-server/src/main/java/org/sonar/server/settings/ws/Setting.java b/server/sonar-server/src/main/java/org/sonar/server/settings/ws/Setting.java new file mode 100644 index 00000000000..668ed30a2c2 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/settings/ws/Setting.java @@ -0,0 +1,112 @@ +/* + * 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 com.google.common.collect.ImmutableTable; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import org.sonar.api.config.PropertyDefinition; +import org.sonar.db.property.PropertyDto; + +public class Setting { + + private static final Splitter DOT_SPLITTER = Splitter.on(".").omitEmptyStrings(); + + private final String key; + private final Long componentId; + private final String value; + private final PropertyDefinition definition; + private final List> propertySets; + private final boolean isDefault; + + /** + * Use this constructor to create setting from a property dto, and that can have a definition or not + */ + Setting(PropertyDto propertyDto, List propertyDtoSetValues, @Nullable PropertyDefinition definition) { + this.key = propertyDto.getKey(); + this.value = propertyDto.getValue(); + this.componentId = propertyDto.getResourceId(); + this.definition = definition; + this.propertySets = buildPropertySetValuesAsMap(key, propertyDtoSetValues); + this.isDefault = false; + } + + /** + * Use this constructor to create setting for default value + */ + Setting(PropertyDefinition definition) { + this.key = definition.key(); + this.value = definition.defaultValue(); + this.componentId = null; + this.definition = definition; + this.propertySets = Collections.emptyList(); + this.isDefault = true; + } + + public String getKey() { + return key; + } + + @CheckForNull + public Long getComponentId() { + return componentId; + } + + @CheckForNull + public PropertyDefinition getDefinition() { + return definition; + } + + public String getValue() { + return value; + } + + public boolean isDefault() { + return isDefault; + } + + public List> getPropertySets() { + return propertySets; + } + + private static List> buildPropertySetValuesAsMap(String propertyKey, List propertySets) { + if (propertySets.isEmpty()) { + return Collections.emptyList(); + } + ImmutableTable.Builder tableBuilder = new ImmutableTable.Builder<>(); + propertySets.forEach(property -> { + List setIdWithFieldKey = DOT_SPLITTER.splitToList(property.getKey().replace(propertyKey + ".", "")); + String setId = setIdWithFieldKey.get(0); + String fieldKey = setIdWithFieldKey.get(1); + tableBuilder.put(setId, fieldKey, property.getValue()); + }); + ImmutableTable table = tableBuilder.build(); + return table.rowKeySet().stream() + .map(table::row) + .collect(Collectors.toList()); + } + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/settings/ws/SettingsFinder.java b/server/sonar-server/src/main/java/org/sonar/server/settings/ws/SettingsFinder.java new file mode 100644 index 00000000000..e1501d8a951 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/settings/ws/SettingsFinder.java @@ -0,0 +1,107 @@ +/* + * 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 com.google.common.collect.Multimap; +import com.google.common.collect.Ordering; +import com.google.common.collect.TreeMultimap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import org.sonar.api.config.PropertyDefinitions; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.property.PropertyDto; + +import static org.sonar.api.PropertyType.PROPERTY_SET; + +public class SettingsFinder { + + private static final Splitter DOT_SPLITTER = Splitter.on(".").omitEmptyStrings(); + private static final Splitter COMMA_SPLITTER = Splitter.on(","); + + private final DbClient dbClient; + private final PropertyDefinitions definitions; + + public SettingsFinder(DbClient dbClient, PropertyDefinitions definitions) { + this.dbClient = dbClient; + this.definitions = definitions; + } + + public List loadGlobalSettings(DbSession dbSession, Set keys) { + List properties = dbClient.propertiesDao().selectGlobalPropertiesByKeys(dbSession, keys); + List propertySets = dbClient.propertiesDao().selectGlobalPropertiesByKeys(dbSession, getPropertySetKeys(properties)); + return properties.stream() + .map(property -> new Setting(property, getPropertySets(property.getKey(), propertySets, null), definitions.get(property.getKey()))) + .collect(Collectors.toList()); + } + + /** + * Return list of settings by component uuid, sorted from project to lowest module + */ + public Multimap loadComponentSettings(DbSession dbSession, Set keys, ComponentDto component) { + List componentUuids = DOT_SPLITTER.splitToList(component.moduleUuidPath()); + List componentDtos = dbClient.componentDao().selectByUuids(dbSession, componentUuids); + Set componentIds = componentDtos.stream().map(ComponentDto::getId).collect(Collectors.toSet()); + Map uuidsById = componentDtos.stream().collect(Collectors.toMap(ComponentDto::getId, ComponentDto::uuid)); + List properties = dbClient.propertiesDao().selectPropertiesByKeysAndComponentIds(dbSession, keys, componentIds); + List propertySets = dbClient.propertiesDao().selectPropertiesByKeysAndComponentIds(dbSession, getPropertySetKeys(properties), componentIds); + + Multimap settingsByUuid = TreeMultimap.create(Ordering.explicit(componentUuids), Ordering.arbitrary()); + for (PropertyDto propertyDto : properties) { + Long componentId = propertyDto.getResourceId(); + String componentUuid = uuidsById.get(componentId); + String propertyKey = propertyDto.getKey(); + settingsByUuid.put(componentUuid, + new Setting(propertyDto, getPropertySets(propertyKey, propertySets, componentId), definitions.get(propertyKey))); + } + return settingsByUuid; + } + + private Set getPropertySetKeys(List properties) { + Set propertySetKeys = new HashSet<>(); + properties.stream() + .filter(propertyDto -> definitions.get(propertyDto.getKey()) != null) + .filter(propertyDto -> definitions.get(propertyDto.getKey()).type().equals(PROPERTY_SET)) + .forEach(propertyDto -> definitions.get(propertyDto.getKey()).fields() + .forEach(field -> COMMA_SPLITTER.splitToList(propertyDto.getValue()) + .forEach(setId -> propertySetKeys.add(generatePropertySetKey(propertyDto.getKey(), setId, field.key()))))); + return propertySetKeys; + } + + private static String generatePropertySetKey(String propertyBaseKey, String id, String fieldKey) { + return propertyBaseKey + "." + id + "." + fieldKey; + } + + private static List getPropertySets(String propertyKey, List propertySets, @Nullable Long componentId) { + return propertySets.stream() + .filter(propertyDto -> Objects.equals(propertyDto.getResourceId(), componentId)) + .filter(propertyDto -> propertyDto.getKey().startsWith(propertyKey + ".")) + .collect(Collectors.toList()); + } + +} 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 c5eb79e52e4..2d067ebcc7f 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 @@ -29,6 +29,7 @@ public class SettingsWsModule extends Module { SetAction.class, SettingsWsComponentParameters.class, ListDefinitionsAction.class, - ValuesAction.class); + ValuesAction.class, + SettingsFinder.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 index 98f982c1fd9..3ca00245a23 100644 --- 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 @@ -20,24 +20,24 @@ package org.sonar.server.settings.ws; import com.google.common.base.Splitter; -import com.google.common.collect.ImmutableTable; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Optional; 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.core.util.stream.Collectors; 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; @@ -52,16 +52,18 @@ public class ValuesAction implements SettingsWsAction { private static final Splitter COMMA_SPLITTER = Splitter.on(","); - static final String PARAM_KEYS = "keys"; + private static final String PARAM_KEYS = "keys"; private final DbClient dbClient; private final SettingsWsComponentParameters settingsWsComponentParameters; private final PropertyDefinitions propertyDefinitions; + private final SettingsFinder settingsFinder; - public ValuesAction(DbClient dbClient, SettingsWsComponentParameters settingsWsComponentParameters, PropertyDefinitions propertyDefinitions) { + public ValuesAction(DbClient dbClient, SettingsWsComponentParameters settingsWsComponentParameters, PropertyDefinitions propertyDefinitions, SettingsFinder settingsFinder) { this.dbClient = dbClient; this.settingsWsComponentParameters = settingsWsComponentParameters; this.propertyDefinitions = propertyDefinitions; + this.settingsFinder = settingsFinder; } @Override @@ -93,89 +95,63 @@ public class ValuesAction implements SettingsWsAction { private ValuesWsResponse doHandle(Request request) { DbSession dbSession = dbClient.openSession(true); try { - ComponentDto component = settingsWsComponentParameters.getComponent(dbSession, request); - settingsWsComponentParameters.checkAdminPermission(component); + ComponentDto componentDto = settingsWsComponentParameters.getComponent(dbSession, request); + settingsWsComponentParameters.checkAdminPermission(componentDto); Set keys = new HashSet<>(request.mandatoryParamAsStrings(PARAM_KEYS)); - - ValuesWsResponse.Builder valuesBuilder = ValuesWsResponse.newBuilder(); - new SettingsBuilder(dbSession, valuesBuilder, getDefinitions(keys), keys, component).build(); - ValuesWsResponse response = valuesBuilder.build(); - return response; + Optional component = Optional.ofNullable(componentDto); + return new ValuesResponseBuilder(loadSettings(dbSession, component, keys), component).build(); } finally { dbClient.closeSession(dbSession); } } - private class SettingsBuilder { - private final DbSession dbSession; - private final ComponentDto component; + private List loadSettings(DbSession dbSession, Optional component, Set keys) { + if (keys.isEmpty()) { + return Collections.emptyList(); + } + + // List of settings must be kept in the following orders : default -> global -> component + List settings = new ArrayList<>(); + settings.addAll(loadDefaultSettings(keys)); + settings.addAll(settingsFinder.loadGlobalSettings(dbSession, keys)); + if (component.isPresent()) { + settings.addAll(settingsFinder.loadComponentSettings(dbSession, keys, component.get()).values()); + } + return settings; + } + + private List loadDefaultSettings(Set keys) { + return propertyDefinitions.getAll().stream() + .filter(definition -> keys.contains(definition.key())) + .filter(defaultProperty -> !isNullOrEmpty(defaultProperty.defaultValue())) + .map(Setting::new) + .collect(Collectors.toList()); + } - private final ValuesWsResponse.Builder valuesWsBuilder; - private final Map definitionsByKey; - private final Set keys; - private final Set propertySetKeys; + private class ValuesResponseBuilder { + private final List settings; + private final Optional component; + private final ValuesWsResponse.Builder valuesWsBuilder = ValuesWsResponse.newBuilder(); private final Map settingsBuilderByKey = new HashMap<>(); - SettingsBuilder(DbSession dbSession, ValuesWsResponse.Builder valuesWsBuilder, List definitions, - Set keys, @Nullable ComponentDto component) { - this.dbSession = dbSession; - this.valuesWsBuilder = valuesWsBuilder; - this.definitionsByKey = definitions.stream() - .collect(Collectors.toMap(PropertyDefinition::key, Function.identity())); - this.keys = keys; + ValuesResponseBuilder(List settings, Optional component) { + this.settings = settings; this.component = component; - - this.propertySetKeys = definitions.stream() - .filter(definition -> definition.type().equals(PROPERTY_SET)) - .map(PropertyDefinition::key) - .collect(Collectors.toSet()); } - void build() { - processDefinitions(); - processPropertyDtos(true); - if (component != null) { - processPropertyDtos(false); - } + ValuesWsResponse build() { + processSettings(); settingsBuilderByKey.values().forEach(Settings.Setting.Builder::build); + return valuesWsBuilder.build(); } - private void processDefinitions() { - definitionsByKey.values().stream() - .filter(defaultProperty -> !isNullOrEmpty(defaultProperty.defaultValue())) - .forEach(this::processDefaultValue); - } - - private void processDefaultValue(PropertyDefinition definition) { - Settings.Setting.Builder settingBuilder = valuesWsBuilder.addSettingsBuilder() - .setKey(definition.key()) - .setInherited(false) - .setDefault(true); - setValue(settingBuilder, definition.defaultValue()); - settingsBuilderByKey.put(definition.key(), settingBuilder); - } - - private void processPropertyDtos(boolean loadGlobal) { - List properties = loadProperties(dbSession, loadGlobal, keys); - PropertySetValues propertySetValues = new PropertySetValues(properties, loadGlobal); - - properties.forEach(property -> { - String key = property.getKey(); - Settings.Setting.Builder valueBuilder = getOrCreateValueBuilder(key); - valueBuilder.setInherited(component != null && property.getResourceId() == null); - valueBuilder.setDefault(false); - - PropertyDefinition definition = definitionsByKey.get(key); - if (definition != null && definition.type().equals(PROPERTY_SET)) { - Settings.FieldsValues.Builder builder = Settings.FieldsValues.newBuilder(); - for (Map propertySetMap : propertySetValues.get(key)) { - builder.addFieldsValuesBuilder().putAllValue(propertySetMap); - } - valueBuilder.setFieldsValues(builder); - } else { - setValue(valueBuilder, property.getValue()); - } + private void processSettings() { + settings.forEach(setting -> { + Settings.Setting.Builder valueBuilder = getOrCreateValueBuilder(setting.getKey()); + valueBuilder.setDefault(setting.isDefault()); + setInherited(setting, valueBuilder); + setValue(setting, valueBuilder); }); } @@ -188,108 +164,33 @@ public class ValuesAction implements SettingsWsAction { return valueBuilder; } - private void setValue(Settings.Setting.Builder valueBuilder, String value) { - PropertyDefinition definition = definitionsByKey.get(valueBuilder.getKey()); - if (definition != null && definition.multiValues()) { + private void setValue(Setting setting, Settings.Setting.Builder valueBuilder) { + PropertyDefinition definition = setting.getDefinition(); + String value = setting.getValue(); + if (definition == null) { + valueBuilder.setValue(value); + return; + } + if (definition.type().equals(PROPERTY_SET)) { + Settings.FieldsValues.Builder builder = Settings.FieldsValues.newBuilder(); + for (Map propertySetMap : setting.getPropertySets()) { + builder.addFieldsValuesBuilder().putAllValue(propertySetMap); + } + valueBuilder.setFieldsValues(builder); + } else if (definition.multiValues()) { valueBuilder.setValues(Settings.Values.newBuilder().addAllValues(COMMA_SPLITTER.split(value))); } else { valueBuilder.setValue(value); } } - private List loadProperties(DbSession dbSession, boolean loadGlobal, Set keys) { - if (loadGlobal) { - return dbClient.propertiesDao().selectGlobalPropertiesByKeys(dbSession, keys); - } - return dbClient.propertiesDao().selectComponentPropertiesByKeys(dbSession, keys, component.getId()); - } - - private class PropertySetValues { - private final Map propertyKeyWithFieldAndSetIds = new HashMap<>(); - private final Map propertySetValuesByPropertyKey = new HashMap<>(); - - PropertySetValues(List properties, boolean loadGlobal) { - properties.stream() - .filter(propertyDto -> propertySetKeys.contains(propertyDto.getKey())) - .forEach(propertyDto -> definitionsByKey.get(propertyDto.getKey()).fields() - .forEach(field -> COMMA_SPLITTER.splitToList(propertyDto.getValue()) - .forEach(value -> add(propertyDto.getKey(), field.key(), value)))); - - loadProperties(dbSession, loadGlobal, propertyKeyWithFieldAndSetIds.keySet()) - .forEach(propertySetDto -> { - PropertyKeyWithFieldAndSetId propertyKeyWithFieldAndSetIdKey = propertyKeyWithFieldAndSetIds.get(propertySetDto.getKey()); - PropertySetValue propertySetValue = getOrCreatePropertySetValue(propertyKeyWithFieldAndSetIdKey.getPropertyKey()); - propertySetValue.add(propertyKeyWithFieldAndSetIdKey.getSetId(), propertyKeyWithFieldAndSetIdKey.getFieldKey(), propertySetDto.getValue()); - }); - } - - private void add(String propertyKey, String fieldKey, String value) { - String propertySetKey = generatePropertySetKey(propertyKey, value, fieldKey); - propertyKeyWithFieldAndSetIds.put(propertySetKey, new PropertyKeyWithFieldAndSetId(propertyKey, fieldKey, value)); - } - - private PropertySetValue getOrCreatePropertySetValue(String propertyKey) { - PropertySetValue propertySetValue = propertySetValuesByPropertyKey.get(propertyKey); - if (propertySetValue == null) { - propertySetValue = new PropertySetValue(); - propertySetValuesByPropertyKey.put(propertyKey, propertySetValue); - } - return propertySetValue; - } - - List> get(String propertyKey) { - return propertySetValuesByPropertyKey.get(propertyKey).get(); - } - - private String generatePropertySetKey(String propertyKey, String id, String fieldKey) { - return propertyKey + "." + id + "." + fieldKey; + private void setInherited(Setting setting, Settings.Setting.Builder valueBuilder) { + if (setting.isDefault()) { + valueBuilder.setInherited(false); + } else { + valueBuilder.setInherited(component.isPresent() && !Objects.equals(setting.getComponentId(), component.get().getId())); } } } - private List getDefinitions(Set keys) { - return propertyDefinitions.getAll().stream() - .filter(def -> keys.contains(def.key())) - .collect(Collectors.toList()); - } - - private static class PropertyKeyWithFieldAndSetId { - private final String propertyKey; - private final String fieldKey; - private final String setId; - - PropertyKeyWithFieldAndSetId(String propertyKey, String fieldKey, String setId) { - this.propertyKey = propertyKey; - this.fieldKey = fieldKey; - this.setId = setId; - } - - public String getPropertyKey() { - return propertyKey; - } - - public String getFieldKey() { - return fieldKey; - } - - public String getSetId() { - return setId; - } - } - - private static class PropertySetValue { - ImmutableTable.Builder tableBuilder = new ImmutableTable.Builder<>(); - - public void add(String setId, String fieldKey, String value) { - tableBuilder.put(setId, fieldKey, value); - } - - public List> get() { - ImmutableTable table = tableBuilder.build(); - return table.rowKeySet().stream() - .map(table::row) - .collect(Collectors.toList()); - } - } - } diff --git a/server/sonar-server/src/test/java/org/sonar/server/settings/ws/SettingsFinderTest.java b/server/sonar-server/src/test/java/org/sonar/server/settings/ws/SettingsFinderTest.java new file mode 100644 index 00000000000..b11d4a79864 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/settings/ws/SettingsFinderTest.java @@ -0,0 +1,208 @@ +/* + * 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.collect.ImmutableMap; +import com.google.common.collect.Multimap; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.config.PropertyDefinition; +import org.sonar.api.config.PropertyDefinitions; +import org.sonar.api.config.PropertyFieldDefinition; +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 static com.google.common.collect.Sets.newHashSet; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.guava.api.Assertions.assertThat; +import static org.sonar.api.PropertyType.PROPERTY_SET; +import static org.sonar.db.component.ComponentTesting.newModuleDto; +import static org.sonar.db.component.ComponentTesting.newProjectDto; +import static org.sonar.db.property.PropertyTesting.newComponentPropertyDto; +import static org.sonar.db.property.PropertyTesting.newGlobalPropertyDto; + +public class SettingsFinderTest { + + @Rule + public DbTester db = DbTester.create(System2.INSTANCE); + + DbClient dbClient = db.getDbClient(); + DbSession dbSession = db.getSession(); + ComponentDbTester componentDb = new ComponentDbTester(db); + PropertyDefinitions propertyDefinitions = new PropertyDefinitions(); + + SettingsFinder underTest = new SettingsFinder(dbClient, propertyDefinitions); + + @Test + public void return_global_settings() throws Exception { + PropertyDefinition definition = PropertyDefinition.builder("foo").build(); + addDefinitions(definition); + insertProperties(newGlobalPropertyDto().setKey("foo").setValue("one")); + + List settings = underTest.loadGlobalSettings(dbSession, newHashSet("foo")); + assertThat(settings).hasSize(1); + assertSetting(settings.get(0), "foo", "one", null, true); + + assertThat(underTest.loadGlobalSettings(dbSession, newHashSet("unknown"))).isEmpty(); + } + + @Test + public void return_global_setting_even_if_no_definition() throws Exception { + insertProperties(newGlobalPropertyDto().setKey("foo").setValue("one")); + + List settings = underTest.loadGlobalSettings(dbSession, newHashSet("foo")); + assertThat(settings).hasSize(1); + assertSetting(settings.get(0), "foo", "one", null, false); + } + + @Test + public void return_global_settings_with_property_set() throws Exception { + addDefinitions(PropertyDefinition.builder("set1") + .type(PROPERTY_SET) + .fields(asList( + PropertyFieldDefinition.build("key").name("Key").build(), + PropertyFieldDefinition.build("size").name("Size").build())) + .build(), + PropertyDefinition.builder("another") + .type(PROPERTY_SET) + .fields(singletonList(PropertyFieldDefinition.build("key").name("Key").build())) + .build()); + insertProperties( + newGlobalPropertyDto().setKey("set1").setValue("1,2"), + newGlobalPropertyDto().setKey("set1.1.key").setValue("key1"), + newGlobalPropertyDto().setKey("set1.1.size").setValue("size1"), + newGlobalPropertyDto().setKey("set1.2.key").setValue("key2"), + newGlobalPropertyDto().setKey("set2").setValue("1"), + newGlobalPropertyDto().setKey("another.1.key").setValue("key1")); + + List settings = underTest.loadGlobalSettings(dbSession, newHashSet("set1")); + assertThat(settings).hasSize(1); + assertSetting(settings.get(0), "set1", "1,2", null, true, ImmutableMap.of("key", "key1", "size", "size1"), ImmutableMap.of("key", "key2")); + } + + @Test + public void return_component_settings() throws Exception { + ComponentDto project = componentDb.insertComponent(newProjectDto()); + addDefinitions(PropertyDefinition.builder("property").defaultValue("default").build()); + insertProperties(newComponentPropertyDto(project).setKey("property").setValue("one")); + + Multimap result = underTest.loadComponentSettings(dbSession, newHashSet("property"), project); + assertThat(result.values()).hasSize(1); + List settings = new ArrayList<>(result.get(project.uuid())); + assertThat(settings).hasSize(1); + assertSetting(settings.get(0), "property", "one", project.getId(), true); + + assertThat(underTest.loadComponentSettings(dbSession, newHashSet("unknown"), project)).isEmpty(); + } + + @Test + public void return_component_setting_even_if_no_definition() throws Exception { + ComponentDto project = componentDb.insertComponent(newProjectDto()); + insertProperties(newComponentPropertyDto(project).setKey("property").setValue("one")); + + Multimap settings = underTest.loadComponentSettings(dbSession, newHashSet("property"), project); + assertThat(settings.values()).hasSize(1); + assertSetting(settings.get(project.uuid()).iterator().next(), "property", "one", project.getId(), false); + } + + @Test + public void return_component_settings_with_property_set() throws Exception { + ComponentDto project = componentDb.insertComponent(newProjectDto()); + addDefinitions(PropertyDefinition.builder("set1") + .type(PROPERTY_SET) + .fields(asList( + PropertyFieldDefinition.build("key").name("Key").build(), + PropertyFieldDefinition.build("size").name("Size").build())) + .build(), + PropertyDefinition.builder("another") + .type(PROPERTY_SET) + .fields(singletonList(PropertyFieldDefinition.build("key").name("Key").build())) + .build()); + insertProperties( + newComponentPropertyDto(project).setKey("set1").setValue("1,2"), + newComponentPropertyDto(project).setKey("set1.1.key").setValue("key1"), + newComponentPropertyDto(project).setKey("set1.1.size").setValue("size1"), + newComponentPropertyDto(project).setKey("set1.2.key").setValue("key2"), + newComponentPropertyDto(project).setKey("set2").setValue("1"), + newComponentPropertyDto(project).setKey("another.1.key").setValue("key1")); + + Multimap settings = underTest.loadComponentSettings(dbSession, newHashSet("set1"), project); + assertThat(settings).hasSize(1); + assertSetting(settings.get(project.uuid()).iterator().next(), "set1", "1,2", project.getId(), true, ImmutableMap.of("key", "key1", "size", "size1"), ImmutableMap.of("key", "key2")); + } + + @Test + public void return_module_settings() throws Exception { + ComponentDto project = componentDb.insertComponent(newProjectDto()); + ComponentDto module = componentDb.insertComponent(newModuleDto(project)); + ComponentDto subModule = componentDb.insertComponent(newModuleDto(module)); + ComponentDto anotherProject = componentDb.insertComponent(newProjectDto()); + + insertProperties( + newComponentPropertyDto(project).setKey("property").setValue("one"), + newComponentPropertyDto(module).setKey("property").setValue("two"), + newComponentPropertyDto(subModule).setKey("property2").setValue("three"), + newComponentPropertyDto(anotherProject).setKey("property").setValue("another one")); + + Multimap result = underTest.loadComponentSettings(dbSession, newHashSet("property", "property2"), subModule); + assertThat(result).hasSize(3); + assertThat(result.keySet()).containsExactly(project.uuid(), module.uuid(), subModule.uuid()); + + assertSetting(result.get(subModule.uuid()).iterator().next(), "property2", "three", subModule.getId(), false); + assertSetting(result.get(module.uuid()).iterator().next(), "property", "two", module.getId(), false); + assertSetting(result.get(project.uuid()).iterator().next(), "property", "one", project.getId(), false); + } + + private void assertSetting(Setting setting, String expectedKey, String expectedValue, @Nullable Long expectedComponentId, boolean hasPropertyDefinition, + Map... propertySets) { + assertThat(setting.getKey()).isEqualTo(expectedKey); + assertThat(setting.getValue()).isEqualTo(expectedValue); + assertThat(setting.getComponentId()).isEqualTo(expectedComponentId); + if (hasPropertyDefinition) { + assertThat(setting.getDefinition()).isEqualTo(propertyDefinitions.get(expectedKey)); + } else { + assertThat(setting.getDefinition()).isNull(); + } + assertThat(setting.getPropertySets()).containsOnly(propertySets); + } + + private void insertProperties(PropertyDto... properties) { + for (PropertyDto propertyDto : properties) { + dbClient.propertiesDao().insertProperty(dbSession, propertyDto); + } + dbSession.commit(); + } + + private void addDefinitions(PropertyDefinition... definitions) { + propertyDefinitions.addComponents(asList(definitions)); + } +} 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 3b9185d8b55..ff8ee47d2f3 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(5 + 2); + assertThat(container.size()).isEqualTo(6 + 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 index 65b22087737..ee060b1d138 100644 --- 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 @@ -56,6 +56,7 @@ 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.newModuleDto; import static org.sonar.db.component.ComponentTesting.newProjectDto; import static org.sonar.db.property.PropertyTesting.newComponentPropertyDto; import static org.sonar.db.property.PropertyTesting.newGlobalPropertyDto; @@ -79,10 +80,11 @@ public class ValuesActionTest { ComponentDbTester componentDb = new ComponentDbTester(db); SettingsWsComponentParameters settingsWsComponentParameters = new SettingsWsComponentParameters(new ComponentFinder(dbClient), userSession); PropertyDefinitions propertyDefinitions = new PropertyDefinitions(); + SettingsFinder settingsFinder = new SettingsFinder(dbClient, propertyDefinitions); ComponentDto project; - WsActionTester ws = new WsActionTester(new ValuesAction(dbClient, settingsWsComponentParameters, propertyDefinitions)); + WsActionTester ws = new WsActionTester(new ValuesAction(dbClient, settingsWsComponentParameters, propertyDefinitions, settingsFinder)); @Before public void setUp() throws Exception { @@ -208,12 +210,7 @@ public class ValuesActionTest { ValuesWsResponse result = newRequestForGlobalProperties("foo"); assertThat(result.getSettingsList()).hasSize(1); - - Settings.Setting value = result.getSettings(0); - assertThat(value.getKey()).isEqualTo("foo"); - assertThat(value.getValue()).isEqualTo("default"); - assertThat(value.getDefault()).isTrue(); - assertThat(value.getInherited()).isFalse(); + assertSetting(result.getSettings(0), "foo", "default", true, false); } @Test @@ -226,12 +223,7 @@ public class ValuesActionTest { ValuesWsResponse result = newRequestForGlobalProperties("property"); assertThat(result.getSettingsList()).hasSize(1); - - Settings.Setting globalPropertyValue = result.getSettings(0); - assertThat(globalPropertyValue.getKey()).isEqualTo("property"); - assertThat(globalPropertyValue.getValue()).isEqualTo("one"); - assertThat(globalPropertyValue.getDefault()).isFalse(); - assertThat(globalPropertyValue.getInherited()).isFalse(); + assertSetting(result.getSettings(0), "property", "one", false, false); } @Test @@ -245,12 +237,7 @@ public class ValuesActionTest { ValuesWsResponse result = newRequestForProjectProperties("property"); assertThat(result.getSettingsList()).hasSize(1); - - Settings.Setting globalPropertyValue = result.getSettings(0); - assertThat(globalPropertyValue.getKey()).isEqualTo("property"); - assertThat(globalPropertyValue.getValue()).isEqualTo("two"); - assertThat(globalPropertyValue.getDefault()).isFalse(); - assertThat(globalPropertyValue.getInherited()).isFalse(); + assertSetting(result.getSettings(0), "property", "two", false, false); } @Test @@ -262,12 +249,7 @@ public class ValuesActionTest { ValuesWsResponse result = newRequestForProjectProperties("property"); assertThat(result.getSettingsList()).hasSize(1); - - Settings.Setting globalPropertyValue = result.getSettings(0); - assertThat(globalPropertyValue.getKey()).isEqualTo("property"); - assertThat(globalPropertyValue.getValue()).isEqualTo("one"); - assertThat(globalPropertyValue.getDefault()).isFalse(); - assertThat(globalPropertyValue.getInherited()).isTrue(); + assertSetting(result.getSettings(0), "property", "one", false, true); } @Test @@ -306,6 +288,46 @@ public class ValuesActionTest { assertThat(result.getSettingsList()).isEmpty(); } + @Test + public void return_module_values() throws Exception { + setUserAsSystemAdmin(); + ComponentDto module = componentDb.insertComponent(newModuleDto(project)); + + propertyDefinitions.addComponent(PropertyDefinition.builder("property").defaultValue("default").build()); + insertProperties( + newGlobalPropertyDto().setKey("property").setValue("one"), + // The property is overriding global value + newComponentPropertyDto(module).setKey("property").setValue("two")); + + ValuesWsResponse result = newRequestForComponentProperties(module, "property"); + assertThat(result.getSettingsList()).hasSize(1); + assertSetting(result.getSettings(0), "property", "two", false, false); + } + + @Test + public void return_inherited_values_on_sub_module() throws Exception { + setUserAsSystemAdmin(); + ComponentDto module = componentDb.insertComponent(newModuleDto(project)); + ComponentDto subModule = componentDb.insertComponent(newModuleDto(module)); + + propertyDefinitions.addComponents(asList( + PropertyDefinition.builder("defaultProperty").defaultValue("default").build(), + PropertyDefinition.builder("globalProperty").build(), + PropertyDefinition.builder("projectProperty").build(), + PropertyDefinition.builder("moduleProperty").build())); + insertProperties( + newGlobalPropertyDto().setKey("globalProperty").setValue("global"), + newComponentPropertyDto(project).setKey("projectProperty").setValue("project"), + newComponentPropertyDto(module).setKey("moduleProperty").setValue("module")); + + ValuesWsResponse result = newRequestForComponentProperties(subModule, "defaultProperty", "globalProperty", "projectProperty", "moduleProperty"); + assertThat(result.getSettingsList()).hasSize(4); + assertSetting(result.getSettings(0), "defaultProperty", "default", true, false); + assertSetting(result.getSettings(1), "globalProperty", "global", false, true); + assertSetting(result.getSettings(2), "projectProperty", "project", false, true); + assertSetting(result.getSettings(3), "moduleProperty", "module", false, true); + } + @Test public void test_example_json_response() { setUserAsSystemAdmin(); @@ -376,6 +398,10 @@ public class ValuesActionTest { assertThat(action.params()).hasSize(3); } + private ValuesWsResponse newRequestForComponentProperties(ComponentDto componentDto, String... keys) { + return newRequest(componentDto.uuid(), null, keys); + } + private ValuesWsResponse newRequestForProjectProperties(String... keys) { return newRequest(project.uuid(), null, keys); } @@ -416,4 +442,11 @@ public class ValuesActionTest { dbSession.commit(); } + private void assertSetting(Settings.Setting setting, String expectedKey, String expectedValue, boolean expectedDefault, boolean expectedInherited) { + assertThat(setting.getKey()).isEqualTo(expectedKey); + assertThat(setting.getValue()).isEqualTo(expectedValue); + assertThat(setting.getDefault()).isEqualTo(expectedDefault); + assertThat(setting.getInherited()).isEqualTo(expectedInherited); + } + } 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 140d7edf142..38683018c91 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 @@ -168,12 +168,17 @@ public class PropertiesDao implements Dao { return selectByKeys(session, keys, null); } - public List selectComponentPropertiesByKeys(DbSession session, Set keys, long componentId) { + public List selectPropertiesByKeysAndComponentId(DbSession session, Set keys, long componentId) { return selectByKeys(session, keys, componentId); } + public List selectPropertiesByKeysAndComponentIds(DbSession session, Set keys, Set componentIds) { + return executeLargeInputs(keys, partitionKeys -> executeLargeInputs(componentIds, + partitionComponentIds -> session.getMapper(PropertiesMapper.class).selectByKeysAndComponentIds(partitionKeys, partitionComponentIds))); + } + private List selectByKeys(DbSession session, Set keys, @Nullable Long componentId) { - return executeLargeInputs(keys, propertyKeys -> session.getMapper(PropertiesMapper.class).selectByKeys(propertyKeys, componentId)); + return executeLargeInputs(keys, partitionKeys -> session.getMapper(PropertiesMapper.class).selectByKeys(partitionKeys, componentId)); } public void insertProperty(DbSession session, PropertyDto 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 e8ef34a84da..781c1ecb79e 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 @@ -41,6 +41,8 @@ public interface PropertiesMapper { List selectByKeys(@Param("keys") List keys, @Nullable @Param("componentId") Long componentId); + List selectByKeysAndComponentIds(@Param("keys") List keys, @Param("componentIds") List componentIds); + List selectByQuery(@Param("query") PropertyQuery query); List selectDescendantModuleProperties(@Param("moduleUuid") String moduleUuid, @Param(value = "scope") String scope, 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 a64c3756236..6c8f3d3336a 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 @@ -99,6 +99,22 @@ + +