]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-7300 Deprecate and rewrite api/properties/index in Java
authorJulien Lancelot <julien.lancelot@sonarsource.com>
Wed, 4 Jan 2017 15:18:14 +0000 (16:18 +0100)
committerJulien Lancelot <julien.lancelot@sonarsource.com>
Tue, 10 Jan 2017 08:56:54 +0000 (09:56 +0100)
35 files changed:
server/sonar-server/src/main/java/org/sonar/server/config/ws/IndexAction.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/config/ws/PropertiesWs.java
server/sonar-server/src/main/java/org/sonar/server/setting/ws/SettingsPermissionPredicates.java
server/sonar-server/src/main/java/org/sonar/server/ws/WebServiceFilter.java
server/sonar-server/src/main/resources/org/sonar/server/config/ws/index-example.json [new file with mode: 0644]
server/sonar-server/src/main/resources/org/sonar/server/config/ws/index-example.xml [deleted file]
server/sonar-server/src/test/java/org/sonar/server/config/ws/IndexActionTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/config/ws/PropertiesWsTest.java [deleted file]
server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/does_not_fail_when_user_has_not_project_browse_permission.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/does_not_returned_secured_and_license_settings_in_property_set_when_not_authenticated.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/does_not_returned_secured_and_license_settings_when_not_authenticated.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/empty.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_all_project_settings_when_component_and_no_key.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_all_settings_when_no_component_and_no_key.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_default_values.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_global_values.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_inherited_values_on_global_setting.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_inherited_values_on_module.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_license_with_hash_settings_when_authenticated_but_not_admin.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_module_values.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_multi_value_with_coma.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_multi_values.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_only_one_setting_when_key_is_provided.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_project_values.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_property_set.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_secured_and_license_settings_in_property_set_when_system_admin.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_secured_and_license_settings_when_project_admin.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_secured_and_license_settings_when_system_admin.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_simple_value.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_values_even_if_no_property_definition.json [new file with mode: 0644]
sonar-db/src/main/java/org/sonar/db/property/PropertiesDao.java
sonar-db/src/main/java/org/sonar/db/property/PropertiesMapper.java
sonar-db/src/main/resources/org/sonar/db/property/PropertiesMapper.xml
sonar-db/src/test/java/org/sonar/db/property/PropertiesDaoTest.java
sonar-plugin-api/src/main/java/org/sonar/api/server/ws/RailsHandler.java

diff --git a/server/sonar-server/src/main/java/org/sonar/server/config/ws/IndexAction.java b/server/sonar-server/src/main/java/org/sonar/server/config/ws/IndexAction.java
new file mode 100644 (file)
index 0000000..fef3bad
--- /dev/null
@@ -0,0 +1,257 @@
+/*
+ * 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.config.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.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+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.api.utils.text.JsonWriter;
+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.db.property.PropertyDto;
+import org.sonar.server.component.ComponentFinder;
+import org.sonar.server.user.UserSession;
+import org.sonar.server.ws.WsAction;
+
+import static org.apache.commons.lang.StringUtils.isEmpty;
+import static org.sonar.api.PropertyType.PROPERTY_SET;
+import static org.sonar.api.server.ws.RailsHandler.PARAM_FORMAT;
+import static org.sonar.api.server.ws.RailsHandler.addJsonOnlyFormatParam;
+import static org.sonar.api.web.UserRole.ADMIN;
+import static org.sonar.server.setting.ws.SettingsPermissionPredicates.DOT_LICENSE;
+import static org.sonar.server.setting.ws.SettingsPermissionPredicates.DOT_SECURED;
+import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
+
+public class IndexAction implements WsAction {
+
+  private static final Splitter DOT_SPLITTER = Splitter.on(".").omitEmptyStrings();
+  private static final Splitter COMMA_SPLITTER = Splitter.on(",");
+  private static final String COMMA_ENCODED_VALUE = "%2C";
+
+  private static final String PARAM_KEY = "key";
+  private static final String PARAM_COMPONENT = "resource";
+
+  private final DbClient dbClient;
+  private final ComponentFinder componentFinder;
+  private final UserSession userSession;
+  private final PropertyDefinitions propertyDefinitions;
+
+  public IndexAction(DbClient dbClient, ComponentFinder componentFinder, UserSession userSession, PropertyDefinitions propertyDefinitions) {
+    this.dbClient = dbClient;
+    this.componentFinder = componentFinder;
+    this.userSession = userSession;
+    this.propertyDefinitions = propertyDefinitions;
+  }
+
+  @Override
+  public void define(WebService.NewController context) {
+    WebService.NewAction action = context.createAction("index")
+      .setDescription("This web service is deprecated, please use api/settings/values instead.")
+      .setDeprecatedSince("6.3")
+      .setResponseExample(getClass().getResource("index-example.json"))
+      .setSince("2.6")
+      .setHandler(this);
+    action.createParam(PARAM_KEY)
+      .setDescription("Setting key")
+      .setExampleValue("sonar.technicalDebt.hoursInDay");
+    action.createParam(PARAM_COMPONENT)
+      .setDescription("Component key or database id")
+      .setExampleValue(KEY_PROJECT_EXAMPLE_001);
+    addJsonOnlyFormatParam(action);
+  }
+
+  @Override
+  public void handle(Request request, Response response) throws Exception {
+    request.param(PARAM_FORMAT);
+    JsonWriter json = response.newJsonWriter();
+    json.beginArray();
+    doHandle(json, request);
+    json.endArray();
+    json.close();
+  }
+
+  private void doHandle(JsonWriter json, Request request) {
+    DbSession dbSession = dbClient.openSession(true);
+    try {
+      Optional<ComponentDto> component = loadComponent(dbSession, request);
+      String key = request.param(PARAM_KEY);
+      List<PropertyDto> propertyDtos = loadProperties(dbSession, component, Optional.ofNullable(key));
+      new ResponseBuilder(propertyDtos).build(json);
+    } finally {
+      dbClient.closeSession(dbSession);
+    }
+  }
+
+  private Optional<ComponentDto> loadComponent(DbSession dbSession, Request request) {
+    String component = request.param(PARAM_COMPONENT);
+    if (component == null) {
+      return Optional.empty();
+    }
+    return Optional.of(loadComponent(dbSession, component));
+  }
+
+  private ComponentDto loadComponent(DbSession dbSession, String component) {
+    try {
+      Long componentId = Long.parseLong(component);
+      return componentFinder.getById(dbSession, componentId);
+    } catch (NumberFormatException e) {
+      return componentFinder.getByKey(dbSession, component);
+    }
+  }
+
+  private List<PropertyDto> loadProperties(DbSession dbSession, Optional<ComponentDto> component, Optional<String> key) {
+    // List of settings must be kept in the following orders : default -> global -> component
+    List<PropertyDto> propertyDtos = new ArrayList<>();
+    propertyDtos.addAll(loadDefaultSettings(key));
+    propertyDtos.addAll(loadGlobalSettings(dbSession, key));
+    component.ifPresent(componentDto -> propertyDtos.addAll(loadComponentSettings(dbSession, key, componentDto).values()));
+    return propertyDtos.stream().filter(isVisible(component)).collect(Collectors.toList());
+  }
+
+  Predicate<PropertyDto> isVisible(Optional<ComponentDto> component) {
+    return propertyDto -> !propertyDto.getKey().endsWith(DOT_SECURED)
+      || hasAdminPermission(component)
+      || (propertyDto.getKey().contains(DOT_LICENSE) && userSession.isLoggedIn());
+  }
+
+  private boolean hasAdminPermission(Optional<ComponentDto> component) {
+    return component.isPresent() ? userSession.hasComponentUuidPermission(ADMIN, component.get().uuid()) : userSession.hasPermission(GlobalPermissions.SYSTEM_ADMIN);
+  }
+
+  private List<PropertyDto> loadGlobalSettings(DbSession dbSession, Optional<String> key) {
+    if (key.isPresent()) {
+      return dbClient.propertiesDao().selectGlobalPropertiesByKeys(dbSession, Collections.singleton(key.get()));
+    }
+    return dbClient.propertiesDao().selectGlobalProperties(dbSession);
+  }
+
+  /**
+   * Return list of propertyDto by component uuid, sorted from project to lowest module
+   */
+  private Multimap<String, PropertyDto> loadComponentSettings(DbSession dbSession, Optional<String> key, ComponentDto component) {
+    List<String> componentUuids = DOT_SPLITTER.splitToList(component.moduleUuidPath());
+    List<ComponentDto> componentDtos = dbClient.componentDao().selectByUuids(dbSession, componentUuids);
+    Set<Long> componentIds = componentDtos.stream().map(ComponentDto::getId).collect(Collectors.toSet());
+    Map<Long, String> uuidsById = componentDtos.stream().collect(Collectors.toMap(ComponentDto::getId, ComponentDto::uuid));
+    List<PropertyDto> properties = key.isPresent() ? dbClient.propertiesDao().selectPropertiesByKeysAndComponentIds(dbSession, Collections.singleton(key.get()), componentIds)
+      : dbClient.propertiesDao().selectPropertiesByComponentIds(dbSession, componentIds);
+
+    Multimap<String, PropertyDto> propertyDtosByUuid = TreeMultimap.create(Ordering.explicit(componentUuids), Ordering.arbitrary());
+    for (PropertyDto propertyDto : properties) {
+      Long componentId = propertyDto.getResourceId();
+      String componentUuid = uuidsById.get(componentId);
+      propertyDtosByUuid.put(componentUuid, propertyDto);
+    }
+    return propertyDtosByUuid;
+  }
+
+  private List<PropertyDto> loadDefaultSettings(Optional<String> key) {
+    return propertyDefinitions.getAll().stream()
+      .filter(definition -> !key.isPresent() || key.get().equals(definition.key()))
+      .filter(defaultProperty -> !isEmpty(defaultProperty.defaultValue()))
+      .map(definition -> new PropertyDto().setKey(definition.key()).setValue(definition.defaultValue()))
+      .collect(Collectors.toList());
+  }
+
+  private class ResponseBuilder {
+    private final List<PropertyDto> propertyDtos;
+    private final Map<String, Property> propertiesByKey = new HashMap<>();
+
+    ResponseBuilder(List<PropertyDto> propertyDtos) {
+      this.propertyDtos = propertyDtos;
+    }
+
+    void build(JsonWriter json) {
+      processSettings();
+      propertiesByKey.values().forEach(property -> {
+        json.beginObject()
+          .prop("key", property.key)
+          .prop("value", property.value);
+        if (!property.values.isEmpty()) {
+          json.name("values").beginArray().values(property.values).endArray();
+        }
+        json.endObject();
+      });
+    }
+
+    private void processSettings() {
+      propertyDtos.forEach(setting -> {
+        Property property = createProperty(setting.getKey());
+        setValue(setting, property);
+      });
+    }
+
+    private Property createProperty(String key) {
+      return propertiesByKey.computeIfAbsent(key, k -> new Property(key));
+    }
+
+    private void setValue(PropertyDto propertyDto, Property property) {
+      String value = propertyDto.getValue();
+      property.setValue(value);
+      PropertyDefinition definition = propertyDefinitions.get(propertyDto.getKey());
+      if (definition != null && (definition.multiValues() || definition.type().equals(PROPERTY_SET))) {
+        property.setValues(createValues(value));
+      }
+    }
+
+    private List<String> createValues(String value) {
+      return COMMA_SPLITTER.splitToList(value).stream().map(v -> v.replace(COMMA_ENCODED_VALUE, ",")).collect(Collectors.toList());
+    }
+  }
+
+  private static class Property {
+    private final String key;
+    private String value;
+    private List<String> values = new ArrayList<>();
+
+    public Property(String key) {
+      this.key = key;
+    }
+
+    public Property setValue(String value) {
+      this.value = value;
+      return this;
+    }
+
+    public Property setValues(List<String> values) {
+      this.values = values;
+      return this;
+    }
+  }
+
+}
index 3aa3f58d8a0c5e74194fb19eed6273b78a175bed..3870bff9e622f000f15c4cfb266be2bbea187ac1 100644 (file)
  */
 package org.sonar.server.config.ws;
 
-import org.sonar.api.server.ws.RailsHandler;
 import org.sonar.api.server.ws.WebService;
 
-import static org.sonar.api.server.ws.RailsHandler.addFormatParam;
-
 public class PropertiesWs implements WebService {
 
-  @Override
-  public void define(Context context) {
-    NewController controller = context.createController("api/properties");
-    controller.setDescription("Manage global and project properties.");
-    controller.setSince("2.6");
+  public static final String CONTROLLER_PROPERTIES = "api/properties";
 
-    defineIndexAction(controller);
+  private final IndexAction indexAction;
 
-    controller.done();
+  public PropertiesWs(IndexAction indexAction) {
+    this.indexAction = indexAction;
   }
 
-  private void defineIndexAction(NewController controller) {
-    NewAction action = controller.createAction("index")
-      .setDescription("Documentation of this web service is available <a href=\"http://redirect.sonarsource.com/doc/old-web-service-api.html\">here</a>")
-      .setResponseExample(getClass().getResource("index-example.xml"))
-      .setSince("2.6")
-      .setHandler(RailsHandler.INSTANCE);
-    addFormatParam(action);
+  @Override
+  public void define(Context context) {
+    NewController controller = context.createController(CONTROLLER_PROPERTIES)
+      .setDescription("This web service is deprecated, please use api/settings instead.")
+      .setSince("2.6");
+    indexAction.define(controller);
+    controller.done();
   }
 }
index 53eb10147b7ab91f345ef68f01a45480f0f74581..ef4482efb6b93dd220cc1062ab2af9f2cfb16d21 100644 (file)
@@ -32,9 +32,10 @@ import static org.sonar.api.web.UserRole.ADMIN;
 
 public class SettingsPermissionPredicates {
 
-  private static final String SECURED_SUFFIX = ".secured";
-  static final String LICENSE_SUFFIX = ".license.secured";
-  static final String LICENSE_HASH_SUFFIX = ".licenseHash.secured";
+  public static final String DOT_SECURED = ".secured";
+  public static final String DOT_LICENSE = ".license";
+  static final String LICENSE_SUFFIX = DOT_LICENSE + DOT_SECURED;
+  static final String LICENSE_HASH_SUFFIX = ".licenseHash" + DOT_SECURED;
 
   private final UserSession userSession;
 
@@ -55,7 +56,7 @@ public class SettingsPermissionPredicates {
   }
 
   private boolean verifySecuredSetting(String key, @Nullable PropertyDefinition definition, Optional<ComponentDto> component) {
-    return isLicense(key, definition) || (!key.endsWith(SECURED_SUFFIX) || hasAdminPermission(component));
+    return isLicense(key, definition) || (!key.endsWith(DOT_SECURED) || hasAdminPermission(component));
   }
 
   private boolean hasAdminPermission(Optional<ComponentDto> component) {
index 1c5e10b44d24e0de265ca5f5d9db1432d5eaca20..028c5c1760f1d519b06284a2249213e9f570561f 100644 (file)
@@ -29,8 +29,11 @@ import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import org.sonar.api.server.ws.RailsHandler;
+import org.sonar.api.server.ws.WebService;
 import org.sonar.api.web.ServletFilter;
 
+import static org.sonar.server.config.ws.PropertiesWs.CONTROLLER_PROPERTIES;
+
 /**
  * This filter is used to execute Java WS.
  *
@@ -45,11 +48,11 @@ public class WebServiceFilter extends ServletFilter {
 
   public WebServiceFilter(WebServiceEngine webServiceEngine) {
     this.webServiceEngine = webServiceEngine;
-    webServiceEngine.controllers().stream()
-      .forEach(controller -> controller.actions().stream()
+    webServiceEngine.controllers()
+      .forEach(controller -> controller.actions()
         .forEach(action -> {
-          // Rails and servlet filter WS should not be executed by the web service engine
-          if (!(action.handler() instanceof RailsHandler) && !(action.handler() instanceof ServletFilterHandler)) {
+          // Rails, Rest and servlet filter WS should not be executed by the web service engine
+          if (isJavaWs(controller, action)) {
             includeUrls.add("/" + controller.path() + "/*");
           } else {
             excludeUrls.add("/" + action.path() + "*");
@@ -84,21 +87,10 @@ public class WebServiceFilter extends ServletFilter {
     // Nothing to do
   }
 
-  private static class WsUrl {
-    private final String controller;
-    private final String action;
-
-    WsUrl(String controller, String action) {
-      this.controller = controller;
-      this.action = action;
-    }
-
-    String getController() {
-      return controller;
-    }
-
-    String getAction() {
-      return action;
-    }
+  private static boolean isJavaWs(WebService.Controller controller, WebService.Action action) {
+    return !(action.handler() instanceof RailsHandler)
+      && !(action.handler() instanceof ServletFilterHandler)
+      && !controller.path().equals(CONTROLLER_PROPERTIES);
   }
+
 }
diff --git a/server/sonar-server/src/main/resources/org/sonar/server/config/ws/index-example.json b/server/sonar-server/src/main/resources/org/sonar/server/config/ws/index-example.json
new file mode 100644 (file)
index 0000000..9574238
--- /dev/null
@@ -0,0 +1,39 @@
+[
+  {
+    "key": "sonar.test.jira",
+    "value": "abc"
+  },
+  {
+    "key": "sonar.autogenerated",
+    "value": "val1,val2,val3",
+    "values": [
+      "val1",
+      "val2",
+      "val3"
+    ]
+  },
+  {
+    "key": "sonar.demo",
+    "value": "1,2",
+    "values": [
+      "1",
+      "2"
+    ]
+  },
+  {
+    "key": "sonar.demo.1.text",
+    "value": "foo"
+  },
+  {
+    "key": "sonar.demo.1.boolean",
+    "value": "true"
+  },
+  {
+    "key": "sonar.demo.2.text",
+    "value": "bar"
+  },
+  {
+    "key": "sonar.demo.2.boolean",
+    "value": "false"
+  }
+]
diff --git a/server/sonar-server/src/main/resources/org/sonar/server/config/ws/index-example.xml b/server/sonar-server/src/main/resources/org/sonar/server/config/ws/index-example.xml
deleted file mode 100644 (file)
index 33186f2..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<properties>
-  <property>
-    <key>sonar.core.treemap.colormetric</key>
-    <value><![CDATA[violations_density]]></value>
-  </property>
-  <property>
-    <key>sonar.core.treemap.sizemetric</key>
-    <value><![CDATA[ncloc]]></value>
-  </property>
-  <property>
-    <key>sonar.plsql.suffixes</key>
-    <value><![CDATA[sql,tab,pkb]]></value>
-  </property>
-  <property>
-    <key>sonar.build-stability.days</key>
-    <value><![CDATA[30]]></value>
-  </property>
-  <property>
-    <key>sonar.dbcleaner.monthsBeforeDeletingAllSnapshots</key>
-    <value><![CDATA[120]]></value>
-  </property>
-  <property>
-    <key>sonar.c.predefinedMacros</key>
-    <value><![CDATA[#define MY_MACRO(a) struct a {}]]></value>
-  </property>
-  <property>
-    <key>sonar.scm.enabled</key>
-    <value><![CDATA[true]]></value>
-  </property>
-  <property>
-    <key>sonar.cpd.cross_project</key>
-    <value><![CDATA[true]]></value>
-  </property>
-  <property>
-    <key>sonar.forceAuthentication</key>
-    <value><![CDATA[true]]></value>
-  </property>
-  <property>
-    <key>sonar.dbcleaner.weeksBeforeDeletingAllSnapshots</key>
-    <value><![CDATA[480]]></value>
-  </property>
-  <property>
-    <key>sonar.java.coveragePlugin</key>
-    <value><![CDATA[jacoco]]></value>
-  </property>
-</properties>
diff --git a/server/sonar-server/src/test/java/org/sonar/server/config/ws/IndexActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/config/ws/IndexActionTest.java
new file mode 100644 (file)
index 0000000..1aba735
--- /dev/null
@@ -0,0 +1,483 @@
+/*
+ * 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.config.ws;
+
+import com.google.common.collect.ImmutableMap;
+import java.net.URL;
+import javax.annotation.Nullable;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.PropertyType;
+import org.sonar.api.config.PropertyDefinition;
+import org.sonar.api.config.PropertyDefinitions;
+import org.sonar.api.config.PropertyFieldDefinition;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.api.utils.System2;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.ComponentDbTester;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.property.PropertyDbTester;
+import org.sonar.server.component.ComponentFinder;
+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 static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.api.PropertyType.LICENSE;
+import static org.sonar.api.web.UserRole.ADMIN;
+import static org.sonar.api.web.UserRole.CODEVIEWER;
+import static org.sonar.api.web.UserRole.USER;
+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;
+import static org.sonar.test.JsonAssert.assertJson;
+import static org.sonarqube.ws.MediaTypes.JSON;
+
+public class IndexActionTest {
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  @Rule
+  public UserSessionRule userSession = UserSessionRule.standalone();
+
+  @Rule
+  public DbTester db = DbTester.create(System2.INSTANCE);
+
+  DbClient dbClient = db.getDbClient();
+  PropertyDbTester propertyDb = new PropertyDbTester(db);
+  ComponentDbTester componentDb = new ComponentDbTester(db);
+  PropertyDefinitions definitions = new PropertyDefinitions();
+
+  ComponentDto project;
+
+  WsActionTester ws = new WsActionTester(new IndexAction(dbClient, new ComponentFinder(dbClient), userSession, definitions));
+
+  @Before
+  public void setUp() throws Exception {
+    project = componentDb.insertComponent(newProjectDto());
+  }
+
+  @Test
+  public void return_simple_value() throws Exception {
+    setAuthenticatedUser();
+    definitions.addComponent(PropertyDefinition.builder("foo").build());
+    propertyDb.insertProperties(newGlobalPropertyDto().setKey("foo").setValue("one"));
+
+    executeAndVerify(null, null, "return_simple_value.json");
+  }
+
+  @Test
+  public void return_multi_values() throws Exception {
+    setAuthenticatedUser();
+    // Property never defined, default value is returned
+    definitions.addComponent(PropertyDefinition.builder("default")
+      .multiValues(true)
+      .defaultValue("one,two")
+      .build());
+    // Property defined at global level
+    definitions.addComponent(PropertyDefinition.builder("global")
+      .multiValues(true)
+      .build());
+    propertyDb.insertProperties(newGlobalPropertyDto().setKey("global").setValue("three,four"));
+
+    executeAndVerify(null, null, "return_multi_values.json");
+  }
+
+  @Test
+  public void return_multi_value_with_coma() throws Exception {
+    setAuthenticatedUser();
+    definitions.addComponent(PropertyDefinition.builder("global").multiValues(true).build());
+    propertyDb.insertProperties(newGlobalPropertyDto().setKey("global").setValue("three,four%2Cfive"));
+
+    executeAndVerify(null, null, "return_multi_value_with_coma.json");
+  }
+
+  @Test
+  public void return_property_set() throws Exception {
+    setAuthenticatedUser();
+    definitions.addComponent(PropertyDefinition
+      .builder("foo")
+      .type(PropertyType.PROPERTY_SET)
+      .fields(asList(
+        PropertyFieldDefinition.build("key").name("Key").build(),
+        PropertyFieldDefinition.build("size").name("Size").build()))
+      .build());
+    propertyDb.insertPropertySet("foo", null, ImmutableMap.of("key", "key1", "size", "size1"), ImmutableMap.of("key", "key2"));
+
+    executeAndVerify(null, null, "return_property_set.json");
+  }
+
+  @Test
+  public void return_default_values() throws Exception {
+    setAuthenticatedUser();
+    definitions.addComponent(PropertyDefinition.builder("foo").defaultValue("default").build());
+
+    executeAndVerify(null, null, "return_default_values.json");
+  }
+
+  @Test
+  public void return_global_values() throws Exception {
+    setAuthenticatedUser();
+    definitions.addComponent(PropertyDefinition.builder("property").defaultValue("default").build());
+    propertyDb.insertProperties(
+      // The property is overriding default value
+      newGlobalPropertyDto().setKey("property").setValue("one"));
+
+    executeAndVerify(null, null, "return_global_values.json");
+  }
+
+  @Test
+  public void return_project_values() throws Exception {
+    setUserWithBrowsePermissionOnProject();
+    definitions.addComponent(PropertyDefinition.builder("property").defaultValue("default").build());
+    propertyDb.insertProperties(
+      newGlobalPropertyDto().setKey("property").setValue("one"),
+      // The property is overriding global value
+      newComponentPropertyDto(project).setKey("property").setValue("two"));
+
+    executeAndVerify(project.key(), null, "return_project_values.json");
+  }
+
+  @Test
+  public void return_values_even_if_no_property_definition() throws Exception {
+    setAuthenticatedUser();
+    propertyDb.insertProperties(newGlobalPropertyDto().setKey("globalPropertyWithoutDefinition").setValue("value"));
+
+    executeAndVerify(null, null, "return_values_even_if_no_property_definition.json");
+  }
+
+  @Test
+  public void return_empty_when_property_def_exists_but_no_value() throws Exception {
+    setAuthenticatedUser();
+    definitions.addComponent(PropertyDefinition.builder("foo").build());
+
+    executeAndVerify(null, null, "empty.json");
+  }
+
+  @Test
+  public void return_nothing_when_unknown_key() throws Exception {
+    setAuthenticatedUser();
+    definitions.addComponent(PropertyDefinition.builder("foo").defaultValue("default").build());
+    propertyDb.insertProperties(newGlobalPropertyDto().setKey("bar").setValue(""));
+
+    executeAndVerify(null, "unknown", "empty.json");
+  }
+
+  @Test
+  public void return_module_values() throws Exception {
+    setUserWithBrowsePermissionOnProject();
+    ComponentDto module = componentDb.insertComponent(newModuleDto(project));
+    definitions.addComponent(PropertyDefinition.builder("property").defaultValue("default").build());
+    propertyDb.insertProperties(
+      newGlobalPropertyDto().setKey("property").setValue("one"),
+      // The property is overriding global value
+      newComponentPropertyDto(module).setKey("property").setValue("two"));
+
+    executeAndVerify(module.key(), "property", "return_module_values.json");
+  }
+
+  @Test
+  public void return_inherited_values_on_module() throws Exception {
+    setUserWithBrowsePermissionOnProject();
+    ComponentDto module = componentDb.insertComponent(newModuleDto(project));
+    definitions.addComponents(asList(
+      PropertyDefinition.builder("defaultProperty").defaultValue("default").build(),
+      PropertyDefinition.builder("globalProperty").build(),
+      PropertyDefinition.builder("projectProperty").build(),
+      PropertyDefinition.builder("moduleProperty").build()));
+    propertyDb.insertProperties(
+      newGlobalPropertyDto().setKey("globalProperty").setValue("global"),
+      newComponentPropertyDto(project).setKey("projectProperty").setValue("project"),
+      newComponentPropertyDto(module).setKey("moduleProperty").setValue("module"));
+
+    executeAndVerify(module.key(), null, "return_inherited_values_on_module.json");
+  }
+
+  @Test
+  public void return_inherited_values_on_global_setting() throws Exception {
+    setAuthenticatedUser();
+    definitions.addComponents(asList(
+      PropertyDefinition.builder("defaultProperty").defaultValue("default").build(),
+      PropertyDefinition.builder("globalProperty").build()));
+    propertyDb.insertProperties(
+      newGlobalPropertyDto().setKey("globalProperty").setValue("global"));
+
+    executeAndVerify(null, null, "return_inherited_values_on_global_setting.json");
+  }
+
+  @Test
+  public void does_not_return_value_of_deprecated_key() throws Exception {
+    setAuthenticatedUser();
+    definitions.addComponent(PropertyDefinition.builder("foo").deprecatedKey("deprecated").build());
+    propertyDb.insertProperties(newGlobalPropertyDto().setKey("foo").setValue("one"));
+
+    executeAndVerify(null, "deprecated", "empty.json");
+  }
+
+  @Test
+  public void does_not_returned_secured_and_license_settings_when_not_authenticated() throws Exception {
+    definitions.addComponents(asList(
+      PropertyDefinition.builder("foo").build(),
+      PropertyDefinition.builder("secret.secured").build(),
+      PropertyDefinition.builder("plugin.license.secured").type(LICENSE).build()));
+    propertyDb.insertProperties(
+      newGlobalPropertyDto().setKey("foo").setValue("one"),
+      newGlobalPropertyDto().setKey("secret.secured").setValue("password"),
+      newGlobalPropertyDto().setKey("plugin.license.secured").setValue("ABCD"));
+
+    executeAndVerify(null, null, "does_not_returned_secured_and_license_settings_when_not_authenticated.json");
+  }
+
+  @Test
+  public void does_not_returned_secured_and_license_settings_in_property_set_when_not_authenticated() throws Exception {
+    definitions.addComponent(PropertyDefinition
+      .builder("foo")
+      .type(PropertyType.PROPERTY_SET)
+      .fields(asList(
+        PropertyFieldDefinition.build("key").name("Key").build(),
+        PropertyFieldDefinition.build("plugin.license.secured").name("License").type(LICENSE).build(),
+        PropertyFieldDefinition.build("secret.secured").name("Secured").build()))
+      .build());
+    propertyDb.insertPropertySet("foo", null,
+      ImmutableMap.of("key", "key1", "plugin.license.secured", "ABCD", "secret.secured", "123456"));
+
+    executeAndVerify(null, null, "does_not_returned_secured_and_license_settings_in_property_set_when_not_authenticated.json");
+  }
+
+  @Test
+  public void return_license_with_hash_settings_when_authenticated_but_not_admin() throws Exception {
+    setUserWithBrowsePermissionOnProject();
+    definitions.addComponents(asList(
+      PropertyDefinition.builder("foo").build(),
+      PropertyDefinition.builder("secret.secured").build(),
+      PropertyDefinition.builder("commercial.plugin").type(LICENSE).build(),
+      PropertyDefinition.builder("plugin.license.secured").type(LICENSE).build()));
+    propertyDb.insertProperties(
+      newGlobalPropertyDto().setKey("foo").setValue("one"),
+      newGlobalPropertyDto().setKey("secret.secured").setValue("password"),
+      newGlobalPropertyDto().setKey("commercial.plugin").setValue("ABCD"),
+      newGlobalPropertyDto().setKey("plugin.license.secured").setValue("ABCD"),
+      newGlobalPropertyDto().setKey("plugin.licenseHash.secured").setValue("987654321"));
+
+    executeAndVerify(null, null, "return_license_with_hash_settings_when_authenticated_but_not_admin.json");
+  }
+
+  @Test
+  public void return_secured_and_license_settings_when_system_admin() throws Exception {
+    setUserAsSystemAdmin();
+    definitions.addComponents(asList(
+      PropertyDefinition.builder("foo").build(),
+      PropertyDefinition.builder("secret.secured").build(),
+      PropertyDefinition.builder("plugin.license.secured").type(LICENSE).build()));
+    propertyDb.insertProperties(
+      newGlobalPropertyDto().setKey("foo").setValue("one"),
+      newGlobalPropertyDto().setKey("secret.secured").setValue("password"),
+      newGlobalPropertyDto().setKey("plugin.license.secured").setValue("ABCD"),
+      newGlobalPropertyDto().setKey("plugin.licenseHash.secured").setValue("987654321"));
+
+    executeAndVerify(null, null, "return_secured_and_license_settings_when_system_admin.json");
+  }
+
+  @Test
+  public void return_secured_and_license_settings_when_project_admin() throws Exception {
+    setUserAsProjectAdmin();
+    definitions.addComponents(asList(
+      PropertyDefinition.builder("foo").build(),
+      PropertyDefinition.builder("secret.secured").build(),
+      PropertyDefinition.builder("plugin.license.secured").type(LICENSE).build()));
+    propertyDb.insertProperties(
+      newComponentPropertyDto(project).setKey("foo").setValue("one"),
+      newComponentPropertyDto(project).setKey("secret.secured").setValue("password"),
+      newComponentPropertyDto(project).setKey("plugin.license.secured").setValue("ABCD"),
+      newComponentPropertyDto(project).setKey("plugin.licenseHash.secured").setValue("987654321"));
+
+    executeAndVerify(project.key(), null, "return_secured_and_license_settings_when_project_admin.json");
+  }
+
+  @Test
+  public void return_secured_and_license_settings_in_property_set_when_system_admin() throws Exception {
+    setUserAsSystemAdmin();
+    definitions.addComponent(PropertyDefinition
+      .builder("foo")
+      .type(PropertyType.PROPERTY_SET)
+      .fields(asList(
+        PropertyFieldDefinition.build("key").name("Key").build(),
+        PropertyFieldDefinition.build("plugin.license.secured").name("License").type(LICENSE).build(),
+        PropertyFieldDefinition.build("secret.secured").name("Secured").build()))
+      .build());
+    propertyDb.insertPropertySet("foo", null,
+      ImmutableMap.of("key", "key1", "plugin.license.secured", "ABCD", "secret.secured", "123456"));
+
+    executeAndVerify(null, null, "return_secured_and_license_settings_in_property_set_when_system_admin.json");
+  }
+
+  @Test
+  public void return_all_settings_when_no_component_and_no_key() throws Exception {
+    setUserAsSystemAdmin();
+    definitions.addComponents(asList(
+      PropertyDefinition.builder("foo").build(),
+      PropertyDefinition.builder("secret.secured").build(),
+      PropertyDefinition.builder("plugin.license.secured").type(LICENSE).build()));
+    propertyDb.insertProperties(
+      newGlobalPropertyDto().setKey("foo").setValue("one"),
+      newGlobalPropertyDto().setKey("secret.secured").setValue("password"),
+      newGlobalPropertyDto().setKey("plugin.license.secured").setValue("ABCD"),
+      newGlobalPropertyDto().setKey("not_defined").setValue("ABCD"));
+
+    executeAndVerify(null, null, "return_all_settings_when_no_component_and_no_key.json");
+  }
+
+  @Test
+  public void return_all_project_settings_when_component_and_no_key() throws Exception {
+    setUserAsProjectAdmin();
+    definitions.addComponents(asList(
+      PropertyDefinition.builder("foo").build(),
+      PropertyDefinition.builder("secret.secured").build(),
+      PropertyDefinition.builder("plugin.license.secured").type(LICENSE).build()));
+    propertyDb.insertProperties(
+      newComponentPropertyDto(project).setKey("foo").setValue("one"),
+      newComponentPropertyDto(project).setKey("secret.secured").setValue("password"),
+      newComponentPropertyDto(project).setKey("plugin.license.secured").setValue("ABCD"),
+      newComponentPropertyDto(project).setKey("not_defined").setValue("ABCD"),
+      newGlobalPropertyDto().setKey("global_not_defined").setValue("ABCD"));
+
+    executeAndVerify(project.key(), null, "return_all_project_settings_when_component_and_no_key.json");
+  }
+
+  @Test
+  public void return_only_one_setting_when_key_is_provided() throws Exception {
+    definitions.addComponents(asList(
+      PropertyDefinition.builder("foo").build(),
+      PropertyDefinition.builder("bar").build()));
+    propertyDb.insertProperties(
+      newGlobalPropertyDto().setKey("foo").setValue("one"),
+      newGlobalPropertyDto().setKey("bar").setValue("two"));
+
+    executeAndVerify(project.key(), "foo", "return_only_one_setting_when_key_is_provided.json");
+    executeAndVerify(project.key(), "unknown", "empty.json");
+  }
+
+  @Test
+  public void does_not_fail_when_user_has_not_project_browse_permission() throws Exception {
+    userSession.login("project-admin").addProjectUuidPermissions(CODEVIEWER, project.uuid());
+    definitions.addComponent(PropertyDefinition.builder("foo").build());
+    propertyDb.insertProperties(newComponentPropertyDto(project).setKey("foo").setValue("one"));
+
+    executeAndVerify(project.key(), null, "does_not_fail_when_user_has_not_project_browse_permission.json");
+  }
+
+  @Test
+  public void does_not_fail_when_format_is_set_to_json() throws Exception {
+    setAuthenticatedUser();
+    definitions.addComponent(PropertyDefinition.builder("foo").defaultValue("default").build());
+
+    ws.newRequest().setParam("format", "json").execute();
+  }
+
+  @Test
+  public void fail_when_format_is_set_to_xml() throws Exception {
+    setAuthenticatedUser();
+    definitions.addComponent(PropertyDefinition.builder("foo").defaultValue("default").build());
+
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Value of parameter 'format' (xml) must be one of: [json]");
+    ws.newRequest().setParam("format", "xml").execute();
+  }
+
+  @Test
+  public void test_example_json_response() {
+    setAuthenticatedUser();
+    definitions.addComponent(PropertyDefinition
+      .builder("sonar.test.jira")
+      .defaultValue("abc")
+      .build());
+    definitions.addComponent(PropertyDefinition
+      .builder("sonar.autogenerated")
+      .multiValues(true)
+      .build());
+    propertyDb.insertProperties(newGlobalPropertyDto().setKey("sonar.autogenerated").setValue("val1,val2,val3"));
+    definitions.addComponent(PropertyDefinition
+      .builder("sonar.demo")
+      .type(PropertyType.PROPERTY_SET)
+      .fields(PropertyFieldDefinition.build("text").name("Text").build(),
+        PropertyFieldDefinition.build("boolean").name("Boolean").build())
+      .build());
+    propertyDb.insertPropertySet("sonar.demo", null, ImmutableMap.of("text", "foo", "boolean", "true"), ImmutableMap.of("text", "bar", "boolean", "false"));
+
+    String result = ws.newRequest().setMediaType(JSON).execute().getInput();
+
+    JsonAssert.assertJson(ws.getDef().responseExampleAsString()).isSimilarTo(result);
+  }
+
+  @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 void executeAndVerify(@Nullable String componentKey, @Nullable String key, String expectedFile) {
+    TestRequest request = ws.newRequest().setMediaType(MediaTypes.JSON);
+    if (key != null) {
+      request.setParam("key", key);
+    }
+    if (componentKey != null) {
+      request.setParam("resource", componentKey);
+    }
+    String result = request.execute().getInput();
+    assertJson(result).isSimilarTo(resource(expectedFile));
+  }
+
+  private void setAuthenticatedUser() {
+    userSession.login("user");
+  }
+
+  private void setUserWithBrowsePermissionOnProject() {
+    userSession.login("user").addProjectUuidPermissions(USER, project.uuid());
+  }
+
+  private void setUserAsSystemAdmin() {
+    userSession.login("admin").setGlobalPermissions(SYSTEM_ADMIN);
+  }
+
+  private void setUserAsProjectAdmin() {
+    userSession.login("project-admin")
+      .addProjectUuidPermissions(ADMIN, project.uuid())
+      .addProjectUuidPermissions(USER, project.uuid());
+  }
+
+  protected static URL resource(String s) {
+    Class<IndexActionTest> clazz = IndexActionTest.class;
+    return clazz.getResource(clazz.getSimpleName() + "/" + s);
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/config/ws/PropertiesWsTest.java b/server/sonar-server/src/test/java/org/sonar/server/config/ws/PropertiesWsTest.java
deleted file mode 100644 (file)
index c9914b1..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * 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.config.ws;
-
-import org.junit.Test;
-import org.sonar.api.server.ws.WebService;
-import org.sonar.server.ws.WsTester;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-public class PropertiesWsTest {
-
-  WsTester tester = new WsTester(new PropertiesWs());
-
-  @Test
-  public void define_ws() {
-    WebService.Controller controller = tester.controller("api/properties");
-    assertThat(controller).isNotNull();
-    assertThat(controller.description()).isNotEmpty();
-    assertThat(controller.actions()).hasSize(1);
-  }
-
-}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/does_not_fail_when_user_has_not_project_browse_permission.json b/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/does_not_fail_when_user_has_not_project_browse_permission.json
new file mode 100644 (file)
index 0000000..d98b4c4
--- /dev/null
@@ -0,0 +1,6 @@
+[
+  {
+    "key": "foo",
+    "value": "one"
+  }
+]
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/does_not_returned_secured_and_license_settings_in_property_set_when_not_authenticated.json b/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/does_not_returned_secured_and_license_settings_in_property_set_when_not_authenticated.json
new file mode 100644 (file)
index 0000000..1475b5a
--- /dev/null
@@ -0,0 +1,13 @@
+[
+  {
+    "key": "foo",
+    "value": "1",
+    "values": [
+      "1"
+    ]
+  },
+  {
+    "key": "foo.1.key",
+    "value": "key1"
+  }
+]
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/does_not_returned_secured_and_license_settings_when_not_authenticated.json b/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/does_not_returned_secured_and_license_settings_when_not_authenticated.json
new file mode 100644 (file)
index 0000000..d98b4c4
--- /dev/null
@@ -0,0 +1,6 @@
+[
+  {
+    "key": "foo",
+    "value": "one"
+  }
+]
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/empty.json b/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/empty.json
new file mode 100644 (file)
index 0000000..0d4f101
--- /dev/null
@@ -0,0 +1,2 @@
+[
+]
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_all_project_settings_when_component_and_no_key.json b/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_all_project_settings_when_component_and_no_key.json
new file mode 100644 (file)
index 0000000..12d2fd7
--- /dev/null
@@ -0,0 +1,22 @@
+[
+  {
+    "key": "secret.secured",
+    "value": "password"
+  },
+  {
+    "key": "foo",
+    "value": "one"
+  },
+  {
+    "key": "not_defined",
+    "value": "ABCD"
+  },
+  {
+    "key": "plugin.license.secured",
+    "value": "ABCD"
+  },
+  {
+    "key": "global_not_defined",
+    "value": "ABCD"
+  }
+]
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_all_settings_when_no_component_and_no_key.json b/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_all_settings_when_no_component_and_no_key.json
new file mode 100644 (file)
index 0000000..96409e9
--- /dev/null
@@ -0,0 +1,18 @@
+[
+  {
+    "key": "secret.secured",
+    "value": "password"
+  },
+  {
+    "key": "foo",
+    "value": "one"
+  },
+  {
+    "key": "not_defined",
+    "value": "ABCD"
+  },
+  {
+    "key": "plugin.license.secured",
+    "value": "ABCD"
+  }
+]
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_default_values.json b/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_default_values.json
new file mode 100644 (file)
index 0000000..4647a4e
--- /dev/null
@@ -0,0 +1,6 @@
+[
+  {
+    "key": "foo",
+    "value": "default"
+  }
+]
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_global_values.json b/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_global_values.json
new file mode 100644 (file)
index 0000000..0869d17
--- /dev/null
@@ -0,0 +1,6 @@
+[
+  {
+    "key": "property",
+    "value": "one"
+  }
+]
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_inherited_values_on_global_setting.json b/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_inherited_values_on_global_setting.json
new file mode 100644 (file)
index 0000000..815c459
--- /dev/null
@@ -0,0 +1,10 @@
+[
+  {
+    "key": "defaultProperty",
+    "value": "default"
+  },
+  {
+    "key": "globalProperty",
+    "value": "global"
+  }
+]
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_inherited_values_on_module.json b/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_inherited_values_on_module.json
new file mode 100644 (file)
index 0000000..15a3ae5
--- /dev/null
@@ -0,0 +1,18 @@
+[
+  {
+    "key": "defaultProperty",
+    "value": "default"
+  },
+  {
+    "key": "projectProperty",
+    "value": "project"
+  },
+  {
+    "key": "globalProperty",
+    "value": "global"
+  },
+  {
+    "key": "moduleProperty",
+    "value": "module"
+  }
+]
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_license_with_hash_settings_when_authenticated_but_not_admin.json b/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_license_with_hash_settings_when_authenticated_but_not_admin.json
new file mode 100644 (file)
index 0000000..e40076c
--- /dev/null
@@ -0,0 +1,18 @@
+[
+  {
+    "key": "plugin.licenseHash.secured",
+    "value": "987654321"
+  },
+  {
+    "key": "foo",
+    "value": "one"
+  },
+  {
+    "key": "plugin.license.secured",
+    "value": "ABCD"
+  },
+  {
+    "key": "commercial.plugin",
+    "value": "ABCD"
+  }
+]
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_module_values.json b/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_module_values.json
new file mode 100644 (file)
index 0000000..5486106
--- /dev/null
@@ -0,0 +1,6 @@
+[
+  {
+    "key": "property",
+    "value": "two"
+  }
+]
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_multi_value_with_coma.json b/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_multi_value_with_coma.json
new file mode 100644 (file)
index 0000000..8411a36
--- /dev/null
@@ -0,0 +1,10 @@
+[
+  {
+    "key": "global",
+    "value": "three,four%2Cfive",
+    "values": [
+      "three",
+      "four,five"
+    ]
+  }
+]
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_multi_values.json b/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_multi_values.json
new file mode 100644 (file)
index 0000000..ce4fcb9
--- /dev/null
@@ -0,0 +1,18 @@
+[
+  {
+    "key": "default",
+    "value": "one,two",
+    "values": [
+      "one",
+      "two"
+    ]
+  },
+  {
+    "key": "global",
+    "value": "three,four",
+    "values": [
+      "three",
+      "four"
+    ]
+  }
+]
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_only_one_setting_when_key_is_provided.json b/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_only_one_setting_when_key_is_provided.json
new file mode 100644 (file)
index 0000000..d98b4c4
--- /dev/null
@@ -0,0 +1,6 @@
+[
+  {
+    "key": "foo",
+    "value": "one"
+  }
+]
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_project_values.json b/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_project_values.json
new file mode 100644 (file)
index 0000000..5486106
--- /dev/null
@@ -0,0 +1,6 @@
+[
+  {
+    "key": "property",
+    "value": "two"
+  }
+]
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_property_set.json b/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_property_set.json
new file mode 100644 (file)
index 0000000..327d187
--- /dev/null
@@ -0,0 +1,22 @@
+[
+  {
+    "key": "foo",
+    "value": "1,2",
+    "values": [
+      "1",
+      "2"
+    ]
+  },
+  {
+    "key": "foo.1.key",
+    "value": "key1"
+  },
+  {
+    "key": "foo.2.key",
+    "value": "key2"
+  },
+  {
+    "key": "foo.1.size",
+    "value": "size1"
+  }
+]
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_secured_and_license_settings_in_property_set_when_system_admin.json b/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_secured_and_license_settings_in_property_set_when_system_admin.json
new file mode 100644 (file)
index 0000000..3da8aab
--- /dev/null
@@ -0,0 +1,21 @@
+[
+  {
+    "key": "foo",
+    "value": "1",
+    "values": [
+      "1"
+    ]
+  },
+  {
+    "key": "foo.1.key",
+    "value": "key1"
+  },
+  {
+    "key": "foo.1.plugin.license.secured",
+    "value": "ABCD"
+  },
+  {
+    "key": "foo.1.secret.secured",
+    "value": "123456"
+  }
+]
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_secured_and_license_settings_when_project_admin.json b/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_secured_and_license_settings_when_project_admin.json
new file mode 100644 (file)
index 0000000..29261a0
--- /dev/null
@@ -0,0 +1,18 @@
+[
+  {
+    "key": "secret.secured",
+    "value": "password"
+  },
+  {
+    "key": "plugin.licenseHash.secured",
+    "value": "987654321"
+  },
+  {
+    "key": "foo",
+    "value": "one"
+  },
+  {
+    "key": "plugin.license.secured",
+    "value": "ABCD"
+  }
+]
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_secured_and_license_settings_when_system_admin.json b/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_secured_and_license_settings_when_system_admin.json
new file mode 100644 (file)
index 0000000..29261a0
--- /dev/null
@@ -0,0 +1,18 @@
+[
+  {
+    "key": "secret.secured",
+    "value": "password"
+  },
+  {
+    "key": "plugin.licenseHash.secured",
+    "value": "987654321"
+  },
+  {
+    "key": "foo",
+    "value": "one"
+  },
+  {
+    "key": "plugin.license.secured",
+    "value": "ABCD"
+  }
+]
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_simple_value.json b/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_simple_value.json
new file mode 100644 (file)
index 0000000..d98b4c4
--- /dev/null
@@ -0,0 +1,6 @@
+[
+  {
+    "key": "foo",
+    "value": "one"
+  }
+]
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_values_even_if_no_property_definition.json b/server/sonar-server/src/test/resources/org/sonar/server/config/ws/IndexActionTest/return_values_even_if_no_property_definition.json
new file mode 100644 (file)
index 0000000..c3d4ba1
--- /dev/null
@@ -0,0 +1,6 @@
+[
+  {
+    "key": "globalPropertyWithoutDefinition",
+    "value": "value"
+  }
+]
index fb7718edcda2d0a65da8a70bdf52046d87b1aaae..ee83f0e5040ff1acffb3117df9c2284c2d06af28 100644 (file)
@@ -162,6 +162,10 @@ public class PropertiesDao implements Dao {
       partitionComponentIds -> getMapper(session).selectByKeysAndComponentIds(partitionKeys, partitionComponentIds)));
   }
 
+  public List<PropertyDto> selectPropertiesByComponentIds(DbSession session, Set<Long> componentIds) {
+    return executeLargeInputs(componentIds, getMapper(session)::selectByComponentIds);
+  }
+
   private List<PropertyDto> selectByKeys(DbSession session, Set<String> keys, @Nullable Long componentId) {
     return executeLargeInputs(keys, partitionKeys -> getMapper(session).selectByKeys(partitionKeys, componentId));
   }
index 05ea17e97fc73b40dbe0ef6715fb74c5bba5dd33..8d4d996a9d155ee0c4c181e75a01e5e650963880 100644 (file)
@@ -39,6 +39,8 @@ public interface PropertiesMapper {
 
   List<PropertyDto> selectByKeysAndComponentIds(@Param("keys") List<String> keys, @Param("componentIds") List<Long> componentIds);
 
+  List<PropertyDto> selectByComponentIds(@Param("componentIds") List<Long> componentIds);
+
   List<PropertyDto> selectByQuery(@Param("query") PropertyQuery query);
 
   List<PropertyDto> selectDescendantModuleProperties(@Param("moduleUuid") String moduleUuid, @Param(value = "scope") String scope,
index 6656440da635535a0813647d8f04234d159b1791..517eb1c324eb5f5aebdbe6fa09caa913f28f9cf2 100644 (file)
       and p.user_id is null
   </select>
 
+  <select id="selectByComponentIds" parameterType="map" resultType="ScrapProperty">
+    select
+      <include refid="columnsToScrapPropertyDto"/>
+    from
+      properties p
+    where
+      p.resource_id in
+      <foreach collection="componentIds" open="(" close=")" item="componentId" separator=",">
+        #{componentId}
+      </foreach>
+      and p.user_id is null
+  </select>
+
   <select id="selectByQuery" parameterType="map" resultType="ScrapProperty">
     select
       <include refid="columnsToScrapPropertyDto"/>
index fa6a9ee3fb9fa458a7aca0ce0149845a684a4d13..c177eeadbe69b5cbf02a3b63ba0370268351ca0f 100644 (file)
@@ -30,7 +30,6 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import javax.annotation.Nullable;
-import org.assertj.core.groups.Tuple;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
@@ -47,6 +46,7 @@ import org.sonar.db.user.UserTesting;
 import static com.google.common.collect.Sets.newHashSet;
 import static java.util.Collections.singletonList;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.groups.Tuple.tuple;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 import static org.sonar.db.property.PropertyTesting.newComponentPropertyDto;
@@ -422,6 +422,37 @@ public class PropertiesDaoTest {
     assertThat(underTest.selectPropertiesByKeysAndComponentId(session, newHashSet(key), 123456789L)).isEmpty();
   }
 
+  @Test
+  public void select_component_properties_by_ids() throws Exception {
+    ComponentDto project = ComponentTesting.newProjectDto();
+    dbClient.componentDao().insert(session, project);
+    ComponentDto project2 = ComponentTesting.newProjectDto();
+    dbClient.componentDao().insert(session, project2);
+
+    UserDto user = UserTesting.newUserDto();
+    dbClient.userDao().insert(session, user);
+
+    String key = "key";
+    String anotherKey = "anotherKey";
+    insertProperties(
+      newGlobalPropertyDto().setKey(key),
+      newComponentPropertyDto(project).setKey(key),
+      newComponentPropertyDto(project2).setKey(key),
+      newComponentPropertyDto(project2).setKey(anotherKey),
+      newUserPropertyDto(user).setKey(key));
+
+    assertThat(underTest.selectPropertiesByComponentIds(session, newHashSet(project.getId())))
+      .extracting("key", "resourceId").containsOnly(tuple(key, project.getId()));
+    assertThat(underTest.selectPropertiesByComponentIds(session, newHashSet(project.getId(), project2.getId())))
+      .extracting("key", "resourceId").containsOnly(
+        tuple(key, project.getId()),
+        tuple(key, project2.getId()),
+        tuple(anotherKey, project2.getId())
+      );
+
+    assertThat(underTest.selectPropertiesByComponentIds(session, newHashSet(123456789L))).isEmpty();
+  }
+
   @Test
   public void select_properties_by_keys_and_component_ids() throws Exception {
     ComponentDto project = ComponentTesting.newProjectDto();
@@ -442,16 +473,16 @@ public class PropertiesDaoTest {
       newUserPropertyDto(user).setKey(key));
 
     assertThat(underTest.selectPropertiesByKeysAndComponentIds(session, newHashSet(key), newHashSet(project.getId())))
-      .extracting("key", "resourceId").containsOnly(Tuple.tuple(key, project.getId()));
+      .extracting("key", "resourceId").containsOnly(tuple(key, project.getId()));
     assertThat(underTest.selectPropertiesByKeysAndComponentIds(session, newHashSet(key), newHashSet(project.getId(), project2.getId())))
       .extracting("key", "resourceId").containsOnly(
-        Tuple.tuple(key, project.getId()),
-        Tuple.tuple(key, project2.getId()));
+        tuple(key, project.getId()),
+        tuple(key, project2.getId()));
     assertThat(underTest.selectPropertiesByKeysAndComponentIds(session, newHashSet(key, anotherKey), newHashSet(project.getId(), project2.getId())))
       .extracting("key", "resourceId").containsOnly(
-        Tuple.tuple(key, project.getId()),
-        Tuple.tuple(key, project2.getId()),
-        Tuple.tuple(anotherKey, project2.getId()));
+        tuple(key, project.getId()),
+        tuple(key, project2.getId()),
+        tuple(anotherKey, project2.getId()));
 
     assertThat(underTest.selectPropertiesByKeysAndComponentIds(session, newHashSet("unknown"), newHashSet(project.getId()))).isEmpty();
     assertThat(underTest.selectPropertiesByKeysAndComponentIds(session, newHashSet("key"), newHashSet(123456789L))).isEmpty();
index 3edc0647afa6a473d183b6096b0e76616d1f63e0..5c86c340ecf052b23902b9ec76ba8ec79dddd3db 100644 (file)
@@ -27,6 +27,7 @@ package org.sonar.api.server.ws;
 public class RailsHandler implements RequestHandler {
 
   public static final RequestHandler INSTANCE = new RailsHandler();
+  public static final String PARAM_FORMAT = "format";
 
   private RailsHandler() {
     // Nothing
@@ -38,7 +39,7 @@ public class RailsHandler implements RequestHandler {
   }
 
   public static WebService.NewParam addFormatParam(WebService.NewAction action) {
-    return action.createParam("format")
+    return action.createParam(PARAM_FORMAT)
       .setDescription("Response format can be set through:" +
         "<ul>" +
         "<li>Parameter format: xml | json</li>" +
@@ -54,7 +55,7 @@ public class RailsHandler implements RequestHandler {
   }
 
   public static WebService.NewParam addJsonOnlyFormatParam(WebService.NewAction action) {
-    return action.createParam("format")
+    return action.createParam(PARAM_FORMAT)
       .setDescription("Only json response format is available")
       .setPossibleValues("json");
   }