You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by du...@apache.org on 2020/04/09 13:50:43 UTC
svn commit: r38870 [4/6] - in /release/sling: ./ src/ src/main/
src/main/java/ src/main/java/org/ src/main/java/org/apache/
src/main/java/org/apache/sling/ src/main/java/org/apache/sling/testing/
src/main/java/org/apache/sling/testing/clients/ src/main...
Added: release/sling/src/main/java/org/apache/sling/testing/clients/osgi/OsgiConsoleClient.java
==============================================================================
--- release/sling/src/main/java/org/apache/sling/testing/clients/osgi/OsgiConsoleClient.java (added)
+++ release/sling/src/main/java/org/apache/sling/testing/clients/osgi/OsgiConsoleClient.java Thu Apr 9 13:50:42 2020
@@ -0,0 +1,898 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.apache.sling.testing.clients.osgi;
+
+import org.apache.http.Header;
+import org.apache.http.entity.mime.MultipartEntityBuilder;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.sling.testing.clients.ClientException;
+import org.apache.sling.testing.clients.SlingClient;
+import org.apache.sling.testing.clients.SlingClientConfig;
+import org.apache.sling.testing.clients.SlingHttpResponse;
+import org.apache.sling.testing.clients.util.FormEntityBuilder;
+import org.apache.sling.testing.clients.util.HttpUtils;
+import org.apache.sling.testing.clients.util.JsonUtils;
+import org.apache.sling.testing.clients.util.poller.PathPoller;
+import org.apache.sling.testing.clients.util.poller.Polling;
+import org.codehaus.jackson.JsonNode;
+import org.osgi.framework.Constants;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeoutException;
+import java.util.jar.JarInputStream;
+import java.util.jar.Manifest;
+
+import static org.apache.http.HttpStatus.SC_MOVED_TEMPORARILY;
+import static org.apache.http.HttpStatus.SC_OK;
+
+/**
+ * A client that wraps the Felix OSGi Web Console REST API calls.
+ * @see <a href=http://felix.apache.org/documentation/subprojects/apache-felix-web-console/web-console-restful-api.html>
+ * Web Console RESTful API</a>
+ */
+public class OsgiConsoleClient extends SlingClient {
+
+ private static final Logger LOG = LoggerFactory.getLogger(OsgiConsoleClient.class);
+ /**
+ * All System Console REST API calls go to /system/console and below
+ */
+ private final String CONSOLE_ROOT_URL = "/system/console";
+
+ /**
+ * The URL for configuration requests
+ */
+ private final String URL_CONFIGURATION = CONSOLE_ROOT_URL + "/configMgr";
+
+ /**
+ * The URL for bundle requests
+ */
+ private final String URL_BUNDLES = CONSOLE_ROOT_URL + "/bundles";
+
+ /**
+ * The URL for components requests
+ */
+ private final String URL_COMPONENTS = CONSOLE_ROOT_URL + "/components";
+
+ /**
+ * The URL for service requests
+ */
+ private final String URL_SERVICES = CONSOLE_ROOT_URL + "/services";
+
+
+ public static final String JSON_KEY_ID = "id";
+ public static final String JSON_KEY_VERSION = "version";
+ public static final String JSON_KEY_DATA = "data";
+ public static final String JSON_KEY_STATE = "state";
+
+ /**
+ * Default constructor. Simply calls {@link SlingClient#SlingClient(URI, String, String)}
+ *
+ * @param serverUrl the URL to the server under test
+ * @param userName the user name used for authentication
+ * @param password the password for this user
+ * @throws ClientException if the client cannot be instantiated
+ */
+ public OsgiConsoleClient(URI serverUrl, String userName, String password) throws ClientException {
+ super(serverUrl, userName, password);
+ }
+
+ /**
+ * Constructor used by adaptTo() and InternalBuilder classes. Should not be called directly in the code
+ *
+ * @param http http client to be used for requests
+ * @param config sling specific configs
+ * @throws ClientException if the client cannot be instantiated
+ */
+ public OsgiConsoleClient(CloseableHttpClient http, SlingClientConfig config) throws ClientException {
+ super(http, config);
+ }
+
+ /**
+ * Returns the wrapper for the bundles info json
+ *
+ * @param expectedStatus list of accepted statuses of the response
+ * @return all the bundles info
+ * @throws ClientException if the response status does not match any of the expectedStatus
+ */
+ public BundlesInfo getBundlesInfo(int... expectedStatus) throws ClientException {
+ // request the bundles information
+ SlingHttpResponse resp = this.doGet(URL_BUNDLES + ".json", HttpUtils.getExpectedStatus(SC_OK, expectedStatus));
+ // return the wrapper
+ return new BundlesInfo(JsonUtils.getJsonNodeFromString(resp.getContent()));
+ }
+
+ /**
+ * Returns the wrapper for the bundle info json
+ *
+ * @param id the id of the bundle
+ * @param expectedStatus list of accepted statuses of the response
+ * @return the bundle info
+ * @throws ClientException if the response status does not match any of the expectedStatus
+ */
+ public BundleInfo getBundleInfo(String id, int... expectedStatus) throws ClientException {
+ SlingHttpResponse resp = this.doGet(URL_BUNDLES + "/" + id + ".json");
+ HttpUtils.verifyHttpStatus(resp, HttpUtils.getExpectedStatus(SC_OK, expectedStatus));
+ return new BundleInfo(JsonUtils.getJsonNodeFromString(resp.getContent()));
+ }
+
+ /**
+ * Returns the wrapper for the components info json
+ *
+ * @param expectedStatus list of accepted statuses of the response
+ * @return the components info
+ * @throws ClientException if the response status does not match any of the expectedStatus
+ */
+ public ComponentsInfo getComponentsInfo(int... expectedStatus) throws ClientException {
+ SlingHttpResponse resp = this.doGet(URL_COMPONENTS + ".json");
+ HttpUtils.verifyHttpStatus(resp, HttpUtils.getExpectedStatus(SC_OK, expectedStatus));
+ return new ComponentsInfo(JsonUtils.getJsonNodeFromString(resp.getContent()));
+ }
+
+ /**
+ * Returns the wrapper for the component info json
+ *
+ * @param id the id of the component
+ * @param expectedStatus list of accepted statuses of the response
+ * @return the component info
+ * @throws ClientException if the response status does not match any of the expectedStatus
+ */
+ public ComponentInfo getComponentInfo(String id, int expectedStatus) throws ClientException {
+ SlingHttpResponse resp = this.doGet(URL_COMPONENTS + "/" + id + ".json");
+ HttpUtils.verifyHttpStatus(resp, HttpUtils.getExpectedStatus(SC_OK, expectedStatus));
+ return new ComponentInfo(JsonUtils.getJsonNodeFromString(resp.getContent()));
+ }
+
+ /**
+ * Returns the wrapper for the component info json
+ *
+ * @param id the id of the component
+ * @return the component info or {@code null} if the component with that name is not found
+ */
+ private ComponentInfo getComponentInfo(String name) throws ClientException {
+ SlingHttpResponse resp = this.doGet(URL_COMPONENTS + "/" + name + ".json");
+ if (HttpUtils.getHttpStatus(resp) == SC_OK) {
+ return new ComponentInfo(JsonUtils.getJsonNodeFromString(resp.getContent()));
+ }
+ return null;
+ }
+
+ /**
+ * Returns the service info wrapper for all services implementing the given type.
+ *
+ * @param name the type of the service
+ * @return the service infos or {@code null} if no service for the given type is registered
+ */
+ private Collection<ServiceInfo> getServiceInfos(String type) throws ClientException {
+ SlingHttpResponse resp = this.doGet(URL_SERVICES + ".json");
+ if (HttpUtils.getHttpStatus(resp) == SC_OK) {
+ return new ServicesInfo(JsonUtils.getJsonNodeFromString(resp.getContent())).forType(type);
+ }
+ return null;
+ }
+
+ /**
+ * Wait until the component with the given name is registered. This means the component must be either in state "Registered" or "Active".
+ * The state registered is called "satisfied" in the Felix DS Web Console
+ * @param componentName the component's name
+ * @param timeout how long to wait for the component to become registered before throwing a {@code TimeoutException} in milliseconds
+ * @param delay time to wait between checks of the state in milliseconds
+ * @throws TimeoutException if the component did not become registered before timeout was reached
+ * @throws InterruptedException if interrupted
+ * @see "OSGi Comp. R6, §112.5 Component Life Cycle"
+ */
+ public void waitComponentRegistered(final String componentName, final long timeout, final long delay) throws TimeoutException, InterruptedException {
+ Polling p = new Polling() {
+ @Override
+ public Boolean call() throws Exception {
+ ComponentInfo info = getComponentInfo(componentName);
+ if (info != null) {
+ return ((info.getStatus() == Component.Status.SATISFIED) || (info.getStatus() == Component.Status.ACTIVE));
+ } else {
+ LOG.debug("Could not get component info for component name {}", componentName);
+ }
+ return false;
+ }
+
+ @Override
+ protected String message() {
+ return "Component " + componentName + " was not registered in %1$d ms";
+ }
+ };
+ p.poll(timeout, delay);
+ }
+
+ /**
+ * Wait until the service with the given name is registered. This means the component must be either in state "Registered" or "Active".
+ * @param type the type of the service (usually the name of a Java interface)
+ * @param bundleSymbolicName the symbolic name of the bundle supposed to register that service.
+ * May be {@code null} in which case this method just waits for any service with the requested type being registered (independent of the registering bundle).
+ * @param timeout how long to wait for the component to become registered before throwing a {@code TimeoutException} in milliseconds
+ * @param delay time to wait between checks of the state in milliseconds
+ * @throws TimeoutException if the component did not become registered before timeout was reached
+ * @throws InterruptedException if interrupted
+ */
+ public void waitServiceRegistered(final String type, final String bundleSymbolicName , final long timeout, final long delay) throws TimeoutException, InterruptedException {
+ Polling p = new Polling() {
+ @Override
+ public Boolean call() throws Exception {
+ Collection<ServiceInfo> infos = getServiceInfos(type);
+ if (infos != null) {
+ if (bundleSymbolicName != null) {
+ for (ServiceInfo info : infos) {
+ if (bundleSymbolicName.equals(info.getBundleSymbolicName())) {
+ return true;
+ }
+ }
+ LOG.debug("Could not find service info for service type {} provided by bundle {}", type, bundleSymbolicName);
+ return false;
+ } else {
+ return !infos.isEmpty();
+ }
+ } else {
+ LOG.debug("Could not find any service info for service type {}", type);
+ }
+ return false;
+ }
+
+ @Override
+ protected String message() {
+ return "Service with type " + type + " was not registered in %1$d ms";
+ }
+ };
+ p.poll(timeout, delay);
+ }
+
+ //
+ // OSGi configurations
+ //
+
+ /**
+ * Returns a map of all properties set for the config referenced by the PID, where the map keys
+ * are the property names.
+ *
+ * @param pid the pid of the configuration
+ * @param expectedStatus list of accepted statuses of the response
+ * @return the properties as a map
+ * @throws ClientException if the response status does not match any of the expectedStatus
+ */
+ public Map<String, Object> getConfiguration(String pid, int... expectedStatus) throws ClientException {
+ // make the request
+ SlingHttpResponse resp = this.doPost(URL_CONFIGURATION + "/" + pid, null);
+ // check the returned status
+ HttpUtils.verifyHttpStatus(resp, HttpUtils.getExpectedStatus(SC_OK, expectedStatus));
+ // get the JSON node
+ JsonNode rootNode = JsonUtils.getJsonNodeFromString(resp.getContent());
+ // go through the params
+ Map<String, Object> props = new HashMap<String, Object>();
+ if(rootNode.get("properties") == null)
+ return props;
+ JsonNode properties = rootNode.get("properties");
+ for(Iterator<String> it = properties.getFieldNames(); it.hasNext();) {
+ String propName = it.next();
+ JsonNode value = properties.get(propName).get("value");
+ if(value != null) {
+ props.put(propName, value.getValueAsText());
+ continue;
+ }
+ value = properties.get(propName).get("values");
+ if(value != null) {
+ Iterator<JsonNode> iter = value.getElements();
+ List<String> list = new ArrayList<String>();
+ while(iter.hasNext()) {
+ list.add(iter.next().getValueAsText());
+ }
+ props.put(propName, list.toArray(new String[list.size()]));
+ }
+ }
+ return props;
+ }
+ /**
+ * Returns a map of all properties set for the config referenced by the PID, where the map keys
+ * are the property names. The method waits until the configuration has been set.
+ *
+ * @deprecated use {@link #waitGetConfiguration(long, String, int...)}
+ *
+ * @param waitCount The number of maximum wait intervals of 500ms.
+ * Between each wait interval, the method polls the backend to see if the configuration ahs been set.
+ * @param pid pid
+ * @param expectedStatus expected response status
+ * @return the config properties
+ * @throws ClientException if the response status does not match any of the expectedStatus
+ * @throws InterruptedException to mark this operation as "waiting"
+ */
+ @Deprecated
+ public Map<String, Object> getConfigurationWithWait(long waitCount, String pid, int... expectedStatus)
+ throws ClientException, InterruptedException {
+ ConfigurationPoller poller = new ConfigurationPoller(pid, expectedStatus);
+ try {
+ poller.poll(500L * waitCount, 500);
+ } catch (TimeoutException e) {
+ throw new ClientException("Cannot retrieve configuration.", e);
+ }
+ return poller.getConfig();
+ }
+
+ /**
+ * Returns a map of all properties set for the config referenced by the PID, where the map keys
+ * are the property names. The method waits until the configuration has been set.
+ *
+ * @param timeout Maximum time to wait for the configuration to be available, in ms.
+ * @param pid service pid
+ * @param expectedStatus expected response status
+ * @return the config properties
+ * @throws ClientException if the response status does not match any of the expectedStatus
+ * @throws InterruptedException to mark this operation as "waiting"
+ * @throws TimeoutException if the timeout was reached
+ */
+ public Map<String, Object> waitGetConfiguration(long timeout, String pid, int... expectedStatus)
+ throws ClientException, InterruptedException, TimeoutException {
+
+ ConfigurationPoller poller = new ConfigurationPoller(pid, expectedStatus);
+ poller.poll(timeout, 500);
+
+ return poller.getConfig();
+ }
+
+ /**
+ * Sets properties of a config referenced by its PID. the properties to be edited are passed as
+ * a map of property name,value pairs.
+ *
+ * @param PID Persistent identity string
+ * @param factoryPID Factory persistent identity string or {@code null}
+ * @param configProperties map of properties
+ * @param expectedStatus expected response status
+ * @return the location of the config
+ * @throws ClientException if the response status does not match any of the expectedStatus
+ */
+ public String editConfiguration(String PID, String factoryPID, Map<String, Object> configProperties, int... expectedStatus)
+ throws ClientException {
+ FormEntityBuilder builder = FormEntityBuilder.create();
+ builder.addParameter("apply", "true");
+ builder.addParameter("action", "ajaxConfigManager");
+ // send factory PID if set
+ if (factoryPID != null) {
+ builder.addParameter("factoryPid", factoryPID);
+ }
+ // add properties to edit
+ StringBuilder propertyList = new StringBuilder("");
+ for (String propName : configProperties.keySet()) {
+ Object o = configProperties.get(propName);
+ if (o instanceof String) {
+ builder.addParameter(propName, (String)o);
+ } else if (o instanceof String[]) {
+ for (String s : (String[])o) {
+ builder.addParameter(propName, s);
+ }
+ }
+ propertyList.append(propName).append(",");
+ }
+ // cut off the last comma
+ builder.addParameter("propertylist", propertyList.substring(0, propertyList.length() - 1));
+ // make the request
+ SlingHttpResponse resp = this.doPost(URL_CONFIGURATION + "/" + PID, builder.build());
+ // check the returned status
+ HttpUtils.verifyHttpStatus(resp, HttpUtils.getExpectedStatus(SC_MOVED_TEMPORARILY, expectedStatus));
+
+ Header[] locationHeader = resp.getHeaders("Location");
+ if (locationHeader!=null && locationHeader.length==1) {
+ return locationHeader[0].getValue().substring(URL_CONFIGURATION.length()+1);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Sets properties of a config referenced by its PID. the properties to be edited are passed as
+ * a map of property (name,value) pairs. The method waits until the configuration has been set.
+ *
+ * @deprecated use {@link #waitEditConfiguration(long, String, String, Map, int...)}
+ *
+ * @param waitCount The number of maximum wait intervals of 500ms.
+ * Between each wait interval, the method polls the backend to see if the configuration ahs been set.
+ * @param PID Persistent identity string
+ * @param factoryPID Factory persistent identity string or {@code null}
+ * @param configProperties map of properties
+ * @param expectedStatus expected response status
+ * @return the pid
+ * @throws ClientException if the response status does not match any of the expectedStatus
+ * @throws InterruptedException to mark this operation as "waiting"
+ */
+ @Deprecated
+ public String editConfigurationWithWait(int waitCount, String PID, String factoryPID, Map<String, Object> configProperties,
+ int... expectedStatus) throws ClientException, InterruptedException {
+ String pid = editConfiguration(PID, factoryPID, configProperties, expectedStatus);
+ getConfigurationWithWait(waitCount, pid);
+ return pid;
+ }
+
+ /**
+ * Sets properties of a config referenced by its PID. the properties to be edited are passed as
+ * a map of property (name,value) pairs. The method waits until the configuration has been set.
+ *
+ * @param timeout Max time to wait for the configuration to be set, in ms
+ * @param PID Persistent identity string
+ * @param factoryPID Factory persistent identity string or {@code null}
+ * @param configProperties map of properties
+ * @param expectedStatus expected response status
+ * @return the pid
+ * @throws ClientException if the response status does not match any of the expectedStatus
+ * @throws InterruptedException to mark this operation as "waiting"
+ * @throws TimeoutException if the timeout was reached
+ */
+ public String waitEditConfiguration(long timeout, String PID, String factoryPID, Map<String, Object> configProperties,
+ int... expectedStatus)
+ throws ClientException, InterruptedException, TimeoutException {
+ String pid = editConfiguration(PID, factoryPID, configProperties, expectedStatus);
+ waitGetConfiguration(timeout, pid);
+ return pid;
+ }
+
+ /**
+ * Delete the config referenced by the PID
+ *
+ * @param pid pid
+ * @param expectedStatus expected response status
+ * @return the sling response
+ * @throws ClientException if the response status does not match any of the expectedStatus
+ */
+ public SlingHttpResponse deleteConfiguration(String pid, int... expectedStatus) throws ClientException {
+ FormEntityBuilder builder = FormEntityBuilder.create();
+ builder.addParameter("apply", "1");
+ builder.addParameter("delete", "1");
+ // make the request
+ SlingHttpResponse resp = this.doPost(URL_CONFIGURATION + "/" + pid, builder.build());
+ // check the returned status
+ HttpUtils.verifyHttpStatus(resp, HttpUtils.getExpectedStatus(200, expectedStatus));
+ return resp;
+ }
+
+ //
+ // Bundles
+ //
+
+ /**
+ * Uninstall a bundle
+ * @param symbolicName bundle symbolic name
+ * @return the sling response
+ * @throws ClientException if something went wrong with the request
+ */
+ public SlingHttpResponse uninstallBundle(String symbolicName) throws ClientException {
+ final long bundleId = getBundleId(symbolicName);
+ LOG.info("Uninstalling bundle {} with bundleId {}", symbolicName, bundleId);
+ FormEntityBuilder builder = FormEntityBuilder.create();
+ builder.addParameter("action", "uninstall");
+ return this.doPost(getBundlePath(symbolicName), builder.build(), 200);
+ }
+
+ /**
+ * Install a bundle using the Felix webconsole HTTP interface
+ * @param f the bundle file
+ * @param startBundle whether to start the bundle or not
+ * @return the sling response
+ * @throws ClientException if the request failed
+ */
+ public SlingHttpResponse installBundle(File f, boolean startBundle) throws ClientException {
+ return installBundle(f, startBundle, 0);
+ }
+
+ /**
+ * Install a bundle using the Felix webconsole HTTP interface, with a specific start level
+ * @param f bundle file
+ * @param startBundle whether to start or just install the bundle
+ * @param startLevel start level
+ * @return the sling response
+ * @throws ClientException if the request failed
+ */
+ public SlingHttpResponse installBundle(File f, boolean startBundle, int startLevel) throws ClientException {
+ // Setup request for Felix Webconsole bundle install
+ MultipartEntityBuilder builder = MultipartEntityBuilder.create()
+ .addTextBody("action", "install")
+ .addBinaryBody("bundlefile", f);
+ if (startBundle) {
+ builder.addTextBody("bundlestart", "true");
+ }
+ if (startLevel > 0) {
+ builder.addTextBody("bundlestartlevel", String.valueOf(startLevel));
+ LOG.info("Installing bundle {} at start level {}", f.getName(), startLevel);
+ } else {
+ LOG.info("Installing bundle {} at default start level", f.getName());
+ }
+
+ return this.doPost(URL_BUNDLES, builder.build(), 302);
+
+ }
+
+ /**
+ * Check that specified bundle is installed and retries every {{waitTime}} milliseconds, until the
+ * bundle is installed or the number of retries was reached
+ * @deprecated does not respect polling practices; use {@link #waitBundleInstalled(String, long, long)} instead
+ * @param symbolicName the name of the bundle
+ * @param waitTime How many milliseconds to wait between retries
+ * @param retries the number of retries
+ * @return true if the bundle was installed until the retries stop, false otherwise
+ * @throws InterruptedException if interrupted
+ */
+ @Deprecated
+ public boolean checkBundleInstalled(String symbolicName, int waitTime, int retries) throws InterruptedException {
+ final String path = getBundlePath(symbolicName, ".json");
+ return new PathPoller(this, path, waitTime, retries).callAndWait();
+ }
+
+ /**
+ * Install a bundle using the Felix webconsole HTTP interface and wait for it to be installed
+ * @deprecated {@link #waitInstallBundle(File, boolean, int, long, long)}
+ * @param f the bundle file
+ * @param startBundle whether to start the bundle or not
+ * @param startLevel the start level of the bundle. negative values mean default start level
+ * @param waitTime how long to wait between retries of checking the bundle
+ * @param retries how many times to check for the bundle to be installed, until giving up
+ * @return true if the bundle was successfully installed, false otherwise
+ * @throws ClientException if the request failed
+ * @throws InterruptedException if interrupted
+ */
+ @Deprecated
+ public boolean installBundleWithRetry(File f, boolean startBundle, int startLevel, int waitTime, int retries)
+ throws ClientException, InterruptedException {
+ installBundle(f, startBundle, startLevel);
+ try {
+ return this.checkBundleInstalled(OsgiConsoleClient.getBundleSymbolicName(f), waitTime, retries);
+ } catch (IOException e) {
+ throw new ClientException("Cannot get bundle symbolic name", e);
+ }
+ }
+
+ /**
+ * Install a bundle using the Felix webconsole HTTP interface and wait for it to be installed.
+ * @param f the bundle file
+ * @param startBundle whether to start the bundle or not
+ * @param startLevel the start level of the bundle. negative values mean default start level
+ * @param timeout how long to wait for the bundle to be installed before throwing a {@code TimeoutException} in milliseconds
+ * @param delay time to wait between checks of the state in milliseconds
+ * @throws ClientException if the request failed
+ * @throws TimeoutException if the bundle did not install before timeout was reached
+ * @throws InterruptedException if interrupted
+ */
+ public void waitInstallBundle(File f, boolean startBundle, int startLevel, long timeout, long delay)
+ throws ClientException, InterruptedException, TimeoutException {
+
+ installBundle(f, startBundle, startLevel);
+ try {
+ waitBundleInstalled(getBundleSymbolicName(f), timeout, delay);
+ } catch (IOException e) {
+ throw new ClientException("Cannot get bundle symbolic name", e);
+ }
+ }
+
+ /**
+ * Wait until the bundle is installed.
+ * @param symbolicName symbolic name of bundle
+ * @param timeout how long to wait for the bundle to be installed before throwing a {@code TimeoutException} in milliseconds
+ * @param delay time to wait between checks of the state in milliseconds
+ * @throws TimeoutException if the bundle did not install before timeout was reached
+ * @throws InterruptedException if interrupted
+ * @see "OSGi Core R6, §4.4.2 Bundle State"
+ */
+ public void waitBundleInstalled(final String symbolicName, final long timeout, final long delay)
+ throws TimeoutException, InterruptedException {
+
+ final String path = getBundlePath(symbolicName);
+ Polling p = new Polling() {
+ @Override
+ public Boolean call() throws Exception {
+ return exists(path);
+ }
+
+ @Override
+ protected String message() {
+ return "Bundle " + symbolicName + " did not install in %1$d ms";
+ }
+ };
+
+ p.poll(timeout, delay);
+ }
+
+ /**
+ * Wait until the bundle is started
+ * @param symbolicName symbolic name of bundle
+ * @param timeout how long to wait for the bundle to be installed before throwing a {@code TimeoutException} in milliseconds.
+ * @param delay time to wait between checks of the state in milliseconds.
+ * @throws TimeoutException if the bundle did not install before timeout was reached
+ * @throws InterruptedException if interrupted
+ * @see "OSGi Core R6, §4.4.2 Bundle State"
+ */
+ public void waitBundleStarted(final String symbolicName, final long timeout, final long delay)
+ throws TimeoutException, InterruptedException {
+
+ Polling p = new Polling() {
+ @Override
+ public Boolean call() throws Exception {
+ try {
+ BundleInfo bundleInfo = getBundleInfo(symbolicName, 200);
+ return (bundleInfo.getStatus() == Bundle.Status.ACTIVE);
+ } catch (ClientException e) {
+ LOG.debug("Could not get bundle state for {}: {}", symbolicName, e.getLocalizedMessage(), e);
+ return false;
+ }
+ }
+
+ @Override
+ protected String message() {
+ return "Bundle " + symbolicName + " did not start in %1$d ms";
+ }
+ };
+
+ p.poll(timeout, delay);
+ }
+
+ /**
+ * Get the id of the bundle
+ * @param symbolicName bundle symbolic name
+ * @return the id
+ * @throws ClientException if the id cannot be retrieved
+ */
+ public long getBundleId(String symbolicName) throws ClientException {
+ final JsonNode bundle = getBundleData(symbolicName);
+ final JsonNode idNode = bundle.get(JSON_KEY_ID);
+
+ if (idNode == null) {
+ throw new ClientException("Cannot get id from bundle json");
+ }
+
+ return idNode.getLongValue();
+ }
+
+ /**
+ * Get the version of the bundle
+ * @param symbolicName bundle symbolic name
+ * @return bundle version
+ * @throws ClientException if the version is not retrieved
+ */
+ public String getBundleVersion(String symbolicName) throws ClientException {
+ final JsonNode bundle = getBundleData(symbolicName);
+ final JsonNode versionNode = bundle.get(JSON_KEY_VERSION);
+
+ if (versionNode == null) {
+ throw new ClientException("Cannot get version from bundle json");
+ }
+
+ return versionNode.getTextValue();
+ }
+
+ /**
+ * Get the state of the bundle
+ * @param symbolicName bundle symbolic name
+ * @return the state of the bundle
+ * @throws ClientException if the state cannot be retrieved
+ */
+ public String getBundleState(String symbolicName) throws ClientException {
+ final JsonNode bundle = getBundleData(symbolicName);
+ final JsonNode stateNode = bundle.get(JSON_KEY_STATE);
+
+ if (stateNode == null) {
+ throw new ClientException("Cannot get state from bundle json");
+ }
+
+ return stateNode.getTextValue();
+ }
+
+ /**
+ * Starts a bundle
+ * @param symbolicName the name of the bundle
+ * @throws ClientException if the request failed
+ */
+ public void startBundle(String symbolicName) throws ClientException {
+ // To start the bundle we POST action=start to its URL
+ final String path = getBundlePath(symbolicName);
+ LOG.info("Starting bundle {} via {}", symbolicName, path);
+ this.doPost(path, FormEntityBuilder.create().addParameter("action", "start").build(), SC_OK);
+ }
+
+ /**
+ * Stop a bundle
+ * @param symbolicName the name of the bundle
+ * @throws ClientException if the request failed
+ */
+ public void stopBundle(String symbolicName) throws ClientException {
+ // To stop the bundle we POST action=stop to its URL
+ final String path = getBundlePath(symbolicName);
+ LOG.info("Stopping bundle {} via {}", symbolicName, path);
+ this.doPost(path, FormEntityBuilder.create().addParameter("action", "stop").build(), SC_OK);
+ }
+
+
+ /**
+ * Starts a bundle and waits for it to be started
+ * @deprecated use {@link #waitStartBundle(String, long, long)}
+ * @param symbolicName the name of the bundle
+ * @param waitTime How many milliseconds to wait between retries
+ * @param retries the number of retries
+ * @throws ClientException if the request failed
+ * @throws InterruptedException if interrupted
+ */
+ @Deprecated
+ public void startBundlewithWait(String symbolicName, int waitTime, int retries)
+ throws ClientException, InterruptedException {
+ // start a bundle
+ startBundle(symbolicName);
+ // wait for it to be in the started state
+ checkBundleInstalled(symbolicName, waitTime, retries);
+ }
+
+ /**
+ * Starts a bundle and waits for it to be started
+ * @param symbolicName the name of the bundle
+ * @param timeout max time to wait for the bundle to start, in ms
+ * @param delay time to wait between status checks, in ms
+ * @throws ClientException if the request failed
+ * @throws InterruptedException if interrupted
+ * @throws TimeoutException if starting timed out
+ */
+ public void waitStartBundle(String symbolicName, long timeout, long delay)
+ throws ClientException, InterruptedException, TimeoutException {
+ startBundle(symbolicName);
+ // FIXME this should wait for the started state
+ waitBundleInstalled(symbolicName, timeout, delay);
+ }
+
+ /**
+ * Calls PackageAdmin.refreshPackages to force re-wiring of all the bundles.
+ * @throws ClientException if the request failed
+ */
+ public void refreshPackages() throws ClientException {
+ LOG.info("Refreshing packages.");
+ FormEntityBuilder builder = FormEntityBuilder.create();
+ builder.addParameter("action", "refreshPackages");
+ this.doPost(URL_BUNDLES, builder.build(), 200);
+ }
+
+
+ //
+ // private methods
+ //
+
+ private String getBundlePath(String symbolicName, String extension) {
+ return getBundlePath(symbolicName) + extension;
+ }
+
+ private String getBundlePath(String symbolicName) {
+ return URL_BUNDLES + "/" + symbolicName;
+ }
+
+ /**
+ * Returns a data structure like:
+ *
+ * {
+ * "status" : "Bundle information: 173 bundles in total - all 173 bundles active.",
+ * "s" : [173,171,2,0,0],
+ * "data": [{
+ * "id":0,
+ * "name":"System Bundle",
+ * "fragment":false,
+ * "stateRaw":32,
+ * "state":"Active",
+ * "version":"3.0.7",
+ * "symbolicName":"org.apache.felix.framework",
+ * "category":""
+ * }]
+ * }
+ */
+ private JsonNode getBundleData(String symbolicName) throws ClientException {
+ final String path = getBundlePath(symbolicName, ".json");
+ final String content = this.doGet(path, SC_OK).getContent();
+ final JsonNode root = JsonUtils.getJsonNodeFromString(content);
+
+ if (root.get(JSON_KEY_DATA) == null) {
+ throw new ClientException(path + " does not provide '" + JSON_KEY_DATA + "' element, JSON content=" + content);
+ }
+
+ Iterator<JsonNode> data = root.get(JSON_KEY_DATA).getElements();
+ if (!data.hasNext()) {
+ throw new ClientException(path + "." + JSON_KEY_DATA + " is empty, JSON content=" + content);
+ }
+
+ final JsonNode bundle = data.next();
+ if (bundle.get(JSON_KEY_STATE) == null) {
+ throw new ClientException(path + ".data[0].state missing, JSON content=" + content);
+ }
+
+ return bundle;
+ }
+
+ //
+ // static methods
+ //
+
+ /**
+ * Get the symbolic name from a bundle file by looking at the manifest
+ * @param bundleFile bundle file
+ * @return the name extracted from the manifest
+ * @throws IOException if reading the jar failed
+ */
+ public static String getBundleSymbolicName(File bundleFile) throws IOException {
+ String name = null;
+ final JarInputStream jis = new JarInputStream(new FileInputStream(bundleFile));
+ try {
+ final Manifest m = jis.getManifest();
+ if (m == null) {
+ throw new IOException("Manifest is null in " + bundleFile.getAbsolutePath());
+ }
+ name = m.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME);
+ } finally {
+ jis.close();
+ }
+ return name;
+ }
+
+ /**
+ * Get the version form a bundle file by looking at the manifest
+ * @param bundleFile bundle file
+ * @return the version
+ * @throws IOException if reading the bundle jar failed
+ */
+ public static String getBundleVersionFromFile(File bundleFile) throws IOException {
+ String version = null;
+ final JarInputStream jis = new JarInputStream(new FileInputStream(bundleFile));
+ try {
+ final Manifest m = jis.getManifest();
+ if(m == null) {
+ throw new IOException("Manifest is null in " + bundleFile.getAbsolutePath());
+ }
+ version = m.getMainAttributes().getValue(Constants.BUNDLE_VERSION);
+ } finally {
+ jis.close();
+ }
+ return version;
+ }
+
+
+ class ConfigurationPoller extends Polling {
+
+ private final String pid;
+ private final int[] expectedStatus;
+ private Map<String, Object> config;
+
+ public ConfigurationPoller(String pid, int... expectedStatus) {
+ super();
+
+ this.pid = pid;
+ this.expectedStatus = expectedStatus;
+ this.config = null;
+ }
+
+ @Override
+ public Boolean call() throws Exception {
+ config = getConfiguration(pid, expectedStatus);
+ return config != null;
+ }
+
+ public Map<String, Object> getConfig() {
+ return config;
+ }
+ }
+}
Added: release/sling/src/main/java/org/apache/sling/testing/clients/osgi/OsgiInstanceConfig.java
==============================================================================
--- release/sling/src/main/java/org/apache/sling/testing/clients/osgi/OsgiInstanceConfig.java (added)
+++ release/sling/src/main/java/org/apache/sling/testing/clients/osgi/OsgiInstanceConfig.java Thu Apr 9 13:50:42 2020
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.sling.testing.clients.osgi;
+
+import org.apache.sling.testing.clients.ClientException;
+import org.apache.sling.testing.clients.util.config.InstanceConfig;
+import org.apache.sling.testing.clients.util.config.InstanceConfigException;
+import org.apache.sling.testing.clients.SlingClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Map;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * <p>Allows saving and restoring the OSGiConfig to be used before and after altering OSGi configurations for tests</p>
+ * <p>See {@link InstanceConfig}</p>
+ */
+public class OsgiInstanceConfig implements InstanceConfig {
+ private static final Logger LOG = LoggerFactory.getLogger(OsgiInstanceConfig.class);
+
+ /**
+ * Time im ms to wait for retrieving the current osgi config for save() and restore()
+ */
+ private static final long WAIT_TIMEOUT = 20000; // in ms
+
+ private final OsgiConsoleClient osgiClient;
+ private final String configPID;
+ private Map<String, Object> config;
+
+ @Deprecated
+ protected int waitCount = 20;
+
+ /**
+ *
+ * @param client The Granite Client to be used internally
+ * @param configPID The PID for the OSGi configuration
+ * @param <T> The type of the Granite Client
+ * @throws ClientException if the client cannot be initialized
+ * @throws InstanceConfigException if the config cannot be saved
+ * @throws InterruptedException if interrupted
+ */
+ public <T extends SlingClient> OsgiInstanceConfig(T client, String configPID)
+ throws ClientException, InstanceConfigException, InterruptedException {
+ this.osgiClient = client.adaptTo(OsgiConsoleClient.class);
+ this.configPID = configPID;
+
+ // Save the configuration
+ save();
+ }
+
+ /**
+ * Save the current OSGi configuration for the PID defined in the constructor
+ *
+ * @throws InstanceConfigException if the config cannot be saved
+ */
+ public InstanceConfig save() throws InstanceConfigException, InterruptedException {
+ try {
+ this.config = osgiClient.waitGetConfiguration(WAIT_TIMEOUT, this.configPID);
+ LOG.info("Saved OSGi config for {}. It is currently this: {}", this.configPID, this.config);
+ } catch (ClientException e) {
+ throw new InstanceConfigException("Error getting config", e);
+ } catch (TimeoutException e) {
+ throw new InstanceConfigException("Timeout of " + WAIT_TIMEOUT + " ms was reached while waiting for the configuration", e);
+ }
+ return this;
+ }
+
+ /**
+ * Restore the current OSGi configuration for the PID defined in the constructor
+ *
+ * @throws InstanceConfigException if the config cannot be restored
+ */
+ public InstanceConfig restore() throws InstanceConfigException, InterruptedException {
+ try {
+ osgiClient.waitEditConfiguration(WAIT_TIMEOUT, this.configPID, null, config);
+ LOG.info("restored OSGi config for {}. It is now this: {}", this.configPID, this.config);
+ } catch (ClientException e) {
+ throw new InstanceConfigException("Could not edit OSGi configuration", e);
+ } catch (TimeoutException e) {
+ throw new InstanceConfigException("Timeout of " + WAIT_TIMEOUT + " ms was reached while waiting for the configuration", e);
+ }
+ return this;
+ }
+}
Added: release/sling/src/main/java/org/apache/sling/testing/clients/osgi/ServiceInfo.java
==============================================================================
--- release/sling/src/main/java/org/apache/sling/testing/clients/osgi/ServiceInfo.java (added)
+++ release/sling/src/main/java/org/apache/sling/testing/clients/osgi/ServiceInfo.java Thu Apr 9 13:50:42 2020
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.apache.sling.testing.clients.osgi;
+
+import java.util.List;
+
+import org.apache.sling.testing.clients.ClientException;
+import org.codehaus.jackson.JsonNode;
+
+public class ServiceInfo {
+
+ private JsonNode service;
+
+ public ServiceInfo(JsonNode root) throws ClientException {
+ if(root.get("id") != null) {
+ service = root;
+ } else {
+ if(root.get("data") == null && root.get("data").size() < 1) {
+ throw new ClientException("No service info returned");
+ }
+ service = root.get("data").get(0);
+ }
+ }
+
+ /**
+ * @return the service identifier
+ */
+ public int getId() {
+ return service.get("id").getIntValue();
+ }
+
+ /**
+ * @return the service types name
+ */
+ public List<String> getTypes() {
+ // this is not a proper JSON array (https://issues.apache.org/jira/browse/FELIX-5762)
+ return ServicesInfo.splitPseudoJsonValueArray(service.get("types").getTextValue());
+ }
+
+ public String getPid() {
+ return service.get("pid").getTextValue();
+ }
+
+ /**
+ * @return the bundle id of the bundle exposing the service
+ */
+ public int getBundleId() {
+ return service.get("bundleId").getIntValue();
+ }
+
+ /**
+ * @return the bundle symbolic name of bundle implementing the service
+ */
+ public String getBundleSymbolicName() {
+ return service.get("bundleSymbolicName").getTextValue();
+ }
+
+}
Added: release/sling/src/main/java/org/apache/sling/testing/clients/osgi/ServicesInfo.java
==============================================================================
--- release/sling/src/main/java/org/apache/sling/testing/clients/osgi/ServicesInfo.java (added)
+++ release/sling/src/main/java/org/apache/sling/testing/clients/osgi/ServicesInfo.java Thu Apr 9 13:50:42 2020
@@ -0,0 +1,142 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.apache.sling.testing.clients.osgi;
+
+import org.apache.sling.testing.clients.ClientException;
+import org.codehaus.jackson.JsonNode;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * A simple Wrapper around the returned JSON when requesting the status of /system/console/services
+ */
+public class ServicesInfo {
+
+ private JsonNode root = null;
+
+ /**
+ * The only constructor.
+ *
+ * @param root the root JSON node of the bundles info.
+ * @throws ClientException if the json does not contain the proper info
+ */
+ public ServicesInfo(JsonNode root) throws ClientException {
+ this.root = root;
+ // some simple sanity checks
+ if(root.get("status") == null)
+ throw new ClientException("No Status returned!");
+ if(root.get("serviceCount") == null)
+ throw new ClientException("No serviceCount returned!");
+ }
+
+ /**
+ * @return total number of bundles.
+ */
+ public int getTotalNumOfServices() {
+ return root.get("serviceCount").getIntValue();
+ }
+
+ /**
+ * Return service info for a service with given id
+ *
+ * @param id the id of the service
+ * @return the BundleInfo
+ * @throws ClientException if the info could not be retrieved
+ */
+ public ServiceInfo forId(String id) throws ClientException {
+ JsonNode serviceInfo = findBy("id", id);
+ return (serviceInfo != null) ? new ServiceInfo(serviceInfo) : null;
+ }
+
+ /**
+ * Return service infos for a bundle with name {@code name}
+ *
+ * @param type the type of the service
+ * @return a Collection of {@link ServiceInfo}s of all services with the given type. Might be empty, never {@code null}
+ * @throws ClientException if the info cannot be retrieved
+ */
+ public Collection<ServiceInfo> forType(String type) throws ClientException {
+ List<ServiceInfo> results = new LinkedList<>();
+ List<JsonNode> serviceInfoNodes = findAllContainingValueInArray("types", type);
+ for (JsonNode serviceInfoNode : serviceInfoNodes) {
+ results.add(new ServiceInfo(serviceInfoNode));
+ }
+ return results;
+ }
+
+ private JsonNode findBy(String key, String value) {
+ List<JsonNode> result = findBy(key, value, true, false);
+ if (result.isEmpty()) {
+ return null;
+ } else {
+ return result.get(0);
+ }
+ }
+
+ private List<JsonNode> findAllContainingValueInArray(String key, String value) {
+ return findBy(key, value, false, true);
+ }
+
+ private List<JsonNode> findBy(String key, String value, boolean onlyReturnFirstMatch, boolean arrayContainingMatch) {
+ Iterator<JsonNode> nodes = root.get("data").getElements();
+ List<JsonNode> results = new LinkedList<>();
+ while(nodes.hasNext()) {
+ JsonNode node = nodes.next();
+ if ((null != node.get(key)) && (node.get(key).isValueNode())) {
+ final String valueNode = node.get(key).getTextValue();
+ if (arrayContainingMatch) {
+ if (splitPseudoJsonValueArray(valueNode).contains(value)) {
+ results.add(node);
+ }
+ } else {
+ if (valueNode.equals(value)) {
+ results.add(node);
+ }
+ }
+ }
+ }
+ return results;
+ }
+
+ /**
+ * Array values are not returned as proper JSON array for Apache Felix.
+ * Therefore we need this dedicated split method, which extracts the individual values from this "pseudo" JSON array.
+ * Example value:
+ * <pre>
+ * [java.lang.Runnable, org.apache.sling.event.impl.jobs.queues.QueueManager, org.osgi.service.event.EventHandler]
+ * </pre>
+ * @param value the value to split
+ * @return the list of the individual values in the given array.
+ * @see <a href="https://issues.apache.org/jira/browse/FELIX-5762">FELIX-5762</a>
+ */
+ static final List<String> splitPseudoJsonValueArray(String value) {
+ // is this an array?
+ if (value.startsWith("[") && value.length() >= 2) {
+ // strip of first and last character
+ String pureArrayValues = value.substring(1, value.length() - 1);
+ String[] resultArray = pureArrayValues.split(", |,");
+ return Arrays.asList(resultArray);
+ }
+ return Collections.singletonList(value);
+ }
+}
\ No newline at end of file
Added: release/sling/src/main/java/org/apache/sling/testing/clients/osgi/package-info.java
==============================================================================
--- release/sling/src/main/java/org/apache/sling/testing/clients/osgi/package-info.java (added)
+++ release/sling/src/main/java/org/apache/sling/testing/clients/osgi/package-info.java Thu Apr 9 13:50:42 2020
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+/**
+ * OSGI testing tools.
+ */
+@Version("2.0.0")
+package org.apache.sling.testing.clients.osgi;
+
+import org.osgi.annotation.versioning.Version;
Added: release/sling/src/main/java/org/apache/sling/testing/clients/package-info.java
==============================================================================
--- release/sling/src/main/java/org/apache/sling/testing/clients/package-info.java (added)
+++ release/sling/src/main/java/org/apache/sling/testing/clients/package-info.java Thu Apr 9 13:50:42 2020
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+@Version("2.0.0")
+package org.apache.sling.testing.clients;
+
+import org.osgi.annotation.versioning.Version;
Added: release/sling/src/main/java/org/apache/sling/testing/clients/query/QueryClient.java
==============================================================================
--- release/sling/src/main/java/org/apache/sling/testing/clients/query/QueryClient.java (added)
+++ release/sling/src/main/java/org/apache/sling/testing/clients/query/QueryClient.java Thu Apr 9 13:50:42 2020
@@ -0,0 +1,224 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.sling.testing.clients.query;
+
+import org.apache.http.NameValuePair;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.sling.testing.clients.ClientException;
+import org.apache.sling.testing.clients.SlingClient;
+import org.apache.sling.testing.clients.SlingClientConfig;
+import org.apache.sling.testing.clients.SlingHttpResponse;
+import org.apache.sling.testing.clients.osgi.OsgiConsoleClient;
+import org.apache.sling.testing.clients.query.servlet.QueryServlet;
+import org.apache.sling.testing.clients.util.JsonUtils;
+import org.apache.sling.testing.clients.util.URLParameterBuilder;
+import org.codehaus.jackson.JsonNode;
+import org.ops4j.pax.tinybundles.core.TinyBundles;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.nio.file.Files;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
+import static org.apache.http.HttpStatus.SC_NOT_FOUND;
+import static org.apache.http.HttpStatus.SC_OK;
+
+/**
+ * <p>Sling client for performing oak queries.</p>
+ *
+ * <p>Uses a custom servlet {@link QueryServlet} to execute the query on the server
+ * and return the results as a json. If the servlet is not yet present, it automatically
+ * installs it and creates the corresponding nodes</p>
+ *
+ * <p>The servlet is exposed under {@value QueryServlet#SERVLET_PATH}.</p>
+ *
+ * <p>The servlet is not automatically uninstalled to avoid too much noise on the instance.
+ * The caller should take care of it, if needed, by calling {@link #uninstallServlet()}</p>
+ */
+public class QueryClient extends SlingClient {
+
+ /**
+ * Query types, as defined in {@code org.apache.jackrabbit.oak.query.QueryEngineImpl}
+ */
+ public enum QueryType {
+ SQL2("JCR-SQL2"),
+ SQL("sql"),
+ XPATH("xpath"),
+ JQOM("JCR-JQOM");
+
+ private final String name;
+
+ QueryType(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+ }
+
+ private static final Logger LOG = LoggerFactory.getLogger(QueryClient.class);
+
+ private static final String BUNDLE_BSN = "org.apache.sling.testing.clients.query";
+ private static final String BUNDLE_NAME = "Sling Testing Clients Query Servlet";
+ private static final String BUNDLE_VERSION = "1.0.0";
+
+ private static final long BUNDLE_START_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
+
+ /**
+ * Constructor used by adaptTo
+ *
+ * @param http underlying HttpClient
+ * @param config config state
+ * @throws ClientException if the client cannot be created
+ */
+ public QueryClient(CloseableHttpClient http, SlingClientConfig config) throws ClientException {
+ super(http, config);
+ }
+
+ /**
+ * Convenience constructor
+ *
+ * @param url host url
+ * @param user username
+ * @param password password
+ * @throws ClientException if the client cannot be constructed
+ */
+ public QueryClient(URI url, String user, String password) throws ClientException {
+ super(url, user, password);
+ }
+
+ /**
+ * Executes a query on the server and returns the results as a json
+ *
+ * @param query query to be executed
+ * @param type type of the query
+ * @return the results in json as exported by {@link QueryServlet}
+ * @throws ClientException if the request failed to execute
+ * @throws InterruptedException to mark that this method blocks
+ */
+ public JsonNode doQuery(final String query, final QueryType type) throws ClientException, InterruptedException {
+ return doQuery(query, type, true, false);
+ }
+
+ /**
+ * Executes a query on the server and returns only the number of rows in the result
+ *
+ * @param query query to be executed
+ * @param type type of the query
+ * @return total results returned by the query
+ * @throws ClientException if the request failed to execute
+ * @throws InterruptedException to mark that this method blocks
+ */
+ public long doCount(final String query, final QueryType type) throws ClientException, InterruptedException {
+ return doQuery(query, type, false, false).get("total").getLongValue();
+ }
+
+ /**
+ * Retrieves the plan of the query. Useful for determining which index is used
+ *
+ * @param query query to be executed
+ * @param type type of the query
+ * @return total results returned by the query
+ * @throws ClientException if the request failed to execute
+ * @throws InterruptedException to mark that this method blocks
+ */
+ public String getPlan(final String query, final QueryType type) throws ClientException, InterruptedException {
+ return doQuery(query, type, false, true).get("plan").toString();
+ }
+
+ protected JsonNode doQuery(final String query, final QueryType type, final boolean showResults, final boolean explain)
+ throws ClientException, InterruptedException {
+
+ List<NameValuePair> params = URLParameterBuilder.create()
+ .add("query", query)
+ .add("type", type.toString())
+ .add("showresults", Boolean.toString(showResults))
+ .add("explain", Boolean.toString(explain))
+ .getList();
+
+ try {
+ // try optimistically to execute the query
+ SlingHttpResponse response = this.doGet(QueryServlet.SERVLET_PATH, params, SC_OK);
+ return JsonUtils.getJsonNodeFromString(response.getContent());
+ } catch (ClientException e) {
+ if (e.getHttpStatusCode() == SC_NOT_FOUND) {
+ LOG.info("Could not find query servlet, will try to install it");
+ installServlet();
+ LOG.info("Retrying the query");
+ SlingHttpResponse response = this.doGet(QueryServlet.SERVLET_PATH, params, SC_OK);
+ return JsonUtils.getJsonNodeFromString(response.getContent());
+ } else {
+ throw e;
+ }
+ }
+ }
+
+ /**
+ * <p>Installs the servlet to be able to perform queries.</p>
+ *
+ * <p>By default, methods of this client automatically install the servlet if needed,
+ * so there is no need to explicitly call from outside</p>
+ *
+ * @return this
+ * @throws ClientException if the installation fails
+ * @throws InterruptedException to mark that this method blocks
+ */
+ public QueryClient installServlet() throws ClientException, InterruptedException {
+ InputStream bundleStream = TinyBundles.bundle()
+ .set("Bundle-SymbolicName", BUNDLE_BSN)
+ .set("Bundle-Version", BUNDLE_VERSION)
+ .set("Bundle-Name", BUNDLE_NAME)
+ .add(QueryServlet.class)
+ .build(TinyBundles.withBnd());
+
+ try {
+ File bundleFile = File.createTempFile(BUNDLE_BSN + "-" + BUNDLE_VERSION, ".jar");
+ Files.copy(bundleStream, bundleFile.toPath(), REPLACE_EXISTING);
+
+ adaptTo(OsgiConsoleClient.class).installBundle(bundleFile, true);
+ adaptTo(OsgiConsoleClient.class).waitBundleStarted(BUNDLE_BSN, BUNDLE_START_TIMEOUT, 100);
+
+ LOG.info("query servlet installed at {}", getUrl(QueryServlet.SERVLET_PATH));
+ } catch (IOException e) {
+ throw new ClientException("Failed to create the query servlet bundle", e);
+ } catch (TimeoutException e) {
+ throw new ClientException("The query servlet bundle did not successfully start", e);
+ }
+
+ return this;
+ }
+
+ /**
+ * Deletes all the resources created by {@link #installServlet()}
+ *
+ * @return this
+ * @throws ClientException if any of the resources fails to uninstall
+ */
+ public QueryClient uninstallServlet() throws ClientException {
+ adaptTo(OsgiConsoleClient.class).uninstallBundle(BUNDLE_BSN);
+ return this;
+ }
+}
Added: release/sling/src/main/java/org/apache/sling/testing/clients/query/package-info.java
==============================================================================
--- release/sling/src/main/java/org/apache/sling/testing/clients/query/package-info.java (added)
+++ release/sling/src/main/java/org/apache/sling/testing/clients/query/package-info.java Thu Apr 9 13:50:42 2020
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+/**
+ * Query tools leveraging javax.jcr.query
+ */
+@Version("0.1.0")
+package org.apache.sling.testing.clients.query;
+
+import org.osgi.annotation.versioning.Version;
Added: release/sling/src/main/java/org/apache/sling/testing/clients/query/servlet/QueryServlet.java
==============================================================================
--- release/sling/src/main/java/org/apache/sling/testing/clients/query/servlet/QueryServlet.java (added)
+++ release/sling/src/main/java/org/apache/sling/testing/clients/query/servlet/QueryServlet.java Thu Apr 9 13:50:42 2020
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.sling.testing.clients.query.servlet;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.SlingHttpServletResponse;
+import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
+import org.osgi.service.component.annotations.Component;
+
+import javax.jcr.Session;
+import javax.jcr.query.*;
+import javax.servlet.Servlet;
+import javax.servlet.ServletException;
+import java.io.IOException;
+import java.util.Date;
+
+import static org.apache.sling.api.servlets.ServletResolverConstants.SLING_SERVLET_METHODS;
+import static org.apache.sling.api.servlets.ServletResolverConstants.SLING_SERVLET_PATHS;
+
+@Component(
+ name = QueryServlet.SERVLET_NAME,
+ service = {Servlet.class},
+ property = {
+ SLING_SERVLET_PATHS + "=" + QueryServlet.SERVLET_PATH,
+ SLING_SERVLET_METHODS + "=GET"
+ }
+)
+public class QueryServlet extends SlingSafeMethodsServlet {
+ private static final long serialVersionUID = 1L;
+
+ public static final String SERVLET_PATH = "/system/testing/query";
+ public static final String SERVLET_NAME = "Sling Testing Clients Query Servlet";
+
+ @Override
+ protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response)
+ throws ServletException, IOException {
+
+ response.setContentType("application/json");
+ response.setCharacterEncoding("UTF-8");
+
+ try {
+ final QueryManager qm = request.getResourceResolver().adaptTo(Session.class)
+ .getWorkspace().getQueryManager();
+
+ long before = 0;
+ long after = 0;
+ long total = 0;
+
+ String query = request.getParameter("query");
+ String type = request.getParameter("type");
+
+ // default for showResults is true, unless parameter is matching exactly "false"
+ boolean showResults = !("false".equalsIgnoreCase(request.getParameter("showresults")));
+ // default for explainQuery is false, unless parameter is present and is not matching "false"
+ String explainParam = request.getParameter("explain");
+ boolean explainQuery = (explainParam != null) && !("false".equalsIgnoreCase(explainParam));
+
+ boolean tidy = false;
+ for (String selector : request.getRequestPathInfo().getSelectors()) {
+ if ("tidy".equals(selector)) {
+ tidy = true;
+ }
+ }
+
+ if ((query == null) || query.equals("") || (type == null) || type.equals("")) {
+ response.sendError(400, "Parameters query and type are required"); // invalid request
+ return;
+ }
+
+ // prepare
+ if (explainQuery) {
+ query = "explain " + query;
+ }
+
+ Query q = qm.createQuery(query, type);
+
+ // execute
+ before = new Date().getTime();
+ QueryResult result = q.execute();
+ after = new Date().getTime();
+
+ // collect results
+ String firstSelector = null;
+ if (result.getSelectorNames().length > 1) {
+ firstSelector = result.getSelectorNames()[0];
+ try {
+ String[] columnNames = result.getColumnNames();
+ if (columnNames.length > 0) {
+ String firstColumnName = columnNames[0];
+ int firstDot = firstColumnName.indexOf('.');
+ if (firstDot > 0) {
+ firstSelector = firstColumnName.substring(0, firstDot);
+ }
+ }
+ } catch (Exception ignored) {
+ }
+ }
+
+ ObjectMapper mapper = new ObjectMapper();
+ ObjectNode responseJson = mapper.createObjectNode();
+
+ if (explainQuery) {
+ responseJson.put("plan", result.getRows().nextRow().getValue("plan").getString());
+ } else if (showResults) {
+ ArrayNode results = mapper.createArrayNode();
+
+ RowIterator rows = result.getRows();
+ while (rows.hasNext()) {
+ Row row = rows.nextRow();
+ String rowPath = (firstSelector != null) ? row.getPath(firstSelector) : row.getPath();
+ String rowType = (firstSelector != null)
+ ? row.getNode(firstSelector).getPrimaryNodeType().getName()
+ : row.getNode().getPrimaryNodeType().getName();
+
+ ObjectNode rowJson = mapper.createObjectNode();
+ rowJson.put("path", rowPath);
+ rowJson.put("type", rowType);
+ results.add(rowJson);
+
+ total++;
+ }
+
+ responseJson.set("results", results);
+ } else {
+ // only count results
+ RowIterator rows = result.getRows();
+ while (rows.hasNext()) {
+ rows.nextRow();
+ total++;
+ }
+ }
+
+ responseJson.put("total", total);
+ responseJson.put("time", after - before);
+
+ if (tidy) {
+ response.getWriter().write(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(responseJson));
+ } else {
+ response.getWriter().write(responseJson.toString());
+ }
+
+ } catch (InvalidQueryException e) {
+ // Consider InvalidQueryException as an invalid request instead of sending 500 server error
+ response.sendError(400, e.getMessage());
+ e.printStackTrace(response.getWriter());
+ } catch (final Exception e) {
+ response.sendError(500, e.getMessage());
+ e.printStackTrace(response.getWriter());
+ }
+ }
+}
Added: release/sling/src/main/java/org/apache/sling/testing/clients/query/servlet/package-info.java
==============================================================================
--- release/sling/src/main/java/org/apache/sling/testing/clients/query/servlet/package-info.java (added)
+++ release/sling/src/main/java/org/apache/sling/testing/clients/query/servlet/package-info.java Thu Apr 9 13:50:42 2020
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+/**
+ * Query tools leveraging javax.jcr.query
+ */
+@Version("1.2.2")
+package org.apache.sling.testing.clients.query.servlet;
+
+import org.osgi.annotation.versioning.Version;
Added: release/sling/src/main/java/org/apache/sling/testing/clients/util/FormEntityBuilder.java
==============================================================================
--- release/sling/src/main/java/org/apache/sling/testing/clients/util/FormEntityBuilder.java (added)
+++ release/sling/src/main/java/org/apache/sling/testing/clients/util/FormEntityBuilder.java Thu Apr 9 13:50:42 2020
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.sling.testing.clients.util;
+
+import org.apache.http.NameValuePair;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.message.BasicNameValuePair;
+
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Helper for creating Entity objects for POST requests.
+ */
+public class FormEntityBuilder {
+ public final static String DEFAULT_ENCODING = "UTF-8";
+
+ private final List<NameValuePair> params;
+ private String encoding;
+
+ public static FormEntityBuilder create() {
+ return new FormEntityBuilder();
+ }
+
+ FormEntityBuilder() {
+ params = new ArrayList<NameValuePair>();
+ encoding = DEFAULT_ENCODING;
+ }
+
+ public FormEntityBuilder addAllParameters(Map<String, String> parameters) {
+ if (parameters != null) {
+ for (String key : parameters.keySet()) {
+ addParameter(key, parameters.get(key));
+ }
+ }
+
+ return this;
+ }
+
+ public FormEntityBuilder addAllParameters(List<NameValuePair> parameters) {
+ if (parameters != null) {
+ params.addAll(parameters);
+ }
+
+ return this;
+ }
+
+ public FormEntityBuilder addParameter(String name, String value) {
+ params.add(new BasicNameValuePair(name, value));
+ return this;
+ }
+
+ public FormEntityBuilder setEncoding(String encoding) {
+ this.encoding = encoding;
+ return this;
+ }
+
+ public UrlEncodedFormEntity build() {
+ try {
+ return new UrlEncodedFormEntity(params, encoding);
+ } catch (UnsupportedEncodingException ue) {
+ throw new Error("Unexpected UnsupportedEncodingException", ue);
+ }
+ }
+}
Added: release/sling/src/main/java/org/apache/sling/testing/clients/util/HttpUtils.java
==============================================================================
--- release/sling/src/main/java/org/apache/sling/testing/clients/util/HttpUtils.java (added)
+++ release/sling/src/main/java/org/apache/sling/testing/clients/util/HttpUtils.java Thu Apr 9 13:50:42 2020
@@ -0,0 +1,183 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.sling.testing.clients.util;
+
+import org.apache.http.Header;
+import org.apache.http.HttpResponse;
+import org.apache.sling.testing.clients.ClientException;
+import org.apache.sling.testing.clients.SlingHttpResponse;
+
+
+import java.net.URI;
+
+
+public class HttpUtils {
+
+ /**
+ * Verify expected status and dump response in case expected status is not returned.
+ * Warning! It will try to consume the entity in case of error
+ *
+ * @param response The Sling HTTP response
+ * @param expectedStatus List of acceptable HTTP Statuses
+ * @throws ClientException if status is not expected
+ */
+ public static void verifyHttpStatus(SlingHttpResponse response, int... expectedStatus) throws ClientException {
+ if (!checkStatus(response, expectedStatus)) {
+ throwError(response, buildDefaultErrorMessage(response), expectedStatus);
+ }
+ }
+
+ /**
+ * Verify expected status and show error message in case expected status is not returned.
+ *
+ * @param response The SlingHttpResponse of an executed request.
+ * @param errorMessage error message; if {@code null}, errorMessage is extracted from response
+ * @param expectedStatus List of acceptable HTTP Statuses
+ * @throws ClientException if status is not expected
+ */
+ public static void verifyHttpStatus(HttpResponse response, String errorMessage, int... expectedStatus)
+ throws ClientException {
+ if (!checkStatus(response, expectedStatus)) {
+ throwError(response, errorMessage, expectedStatus);
+ }
+ }
+
+ private static boolean checkStatus(HttpResponse response, int... expectedStatus)
+ throws ClientException {
+
+ // if no HttpResponse was given
+ if (response == null) {
+ throw new NullPointerException("The response is null!");
+ }
+
+ // if no expected statuses are given
+ if (expectedStatus == null || expectedStatus.length == 0) {
+ throw new IllegalArgumentException("At least one expected HTTP Status must be set!");
+ }
+
+ // get the returned HTTP Status
+ int givenStatus = getHttpStatus(response);
+
+ // check if it matches with an expected one
+ for (int expected : expectedStatus) {
+ if (givenStatus == expected) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static boolean throwError(HttpResponse response, String errorMessage, int... expectedStatus)
+ throws ClientException {
+ // build error message
+ String errorMsg = "Expected HTTP Status: ";
+ for (int expected : expectedStatus) {
+ errorMsg += expected + " ";
+ }
+
+ // get the returned HTTP Status
+ int givenStatus = getHttpStatus(response);
+
+ errorMsg += ". Instead " + givenStatus + " was returned!\n";
+ if (errorMessage != null) {
+ errorMsg += errorMessage;
+ }
+
+ // throw the exception
+ throw new ClientException(errorMsg, givenStatus);
+ }
+
+
+ /**
+ * Build default error message
+ *
+ * @param resp The response of a sling request
+ * @return default error message
+ */
+ public static String buildDefaultErrorMessage(SlingHttpResponse resp) {
+
+ String content = resp.getContent();
+
+ // if no response content is available
+ if (content == null) return "";
+ String errorMsg = resp.getSlingMessage();
+
+ errorMsg = (errorMsg == null || errorMsg.length() == 0)
+ // any other returned content
+ ? " Response Content:\n" + content
+ // response message from sling response
+ : "Error Message: \n" + errorMsg;
+
+ return errorMsg;
+ }
+
+ /**
+ * Get HTTP Status of the response.
+ *
+ * @param response The RequestExecutor of an executed request.
+ * @return The HTTP Status of the response
+ * @throws ClientException never (kept for uniformity)
+ */
+ public static int getHttpStatus(HttpResponse response) throws ClientException {
+ return response.getStatusLine().getStatusCode();
+ }
+
+ /**
+ * Get the first 'Location' header and verify it's a valid URI.
+ *
+ * @param response HttpResponse the http response
+ * @return the location path
+ * @throws ClientException never (kept for uniformity)
+ */
+ public static String getLocationHeader(HttpResponse response) throws ClientException {
+ if (response == null) throw new ClientException("Response must not be null!");
+
+ String locationPath = null;
+ Header locationHeader = response.getFirstHeader("Location");
+ if (locationHeader != null) {
+ String location = locationHeader.getValue();
+ URI locationURI = URI.create(location);
+ locationPath = locationURI.getPath();
+ }
+
+ if (locationPath == null) {
+ throw new ClientException("not able to determine location path");
+ }
+ return locationPath;
+ }
+
+ /**
+ * Check if expected status is in range
+ *
+ * @param response the http response
+ * @param range the http status range
+ * @return true if response is in range
+ */
+ public static boolean isInHttpStatusRange(HttpResponse response, int range) {
+ return range == response.getStatusLine().getStatusCode() / 100 * 100;
+ }
+
+ public static int[] getExpectedStatus(int defaultStatus, int... expectedStatus) {
+ if (expectedStatus == null || expectedStatus.length == 0) {
+ expectedStatus = new int[]{defaultStatus};
+ }
+ return expectedStatus;
+ }
+
+
+}
\ No newline at end of file
Added: release/sling/src/main/java/org/apache/sling/testing/clients/util/InputStreamBodyWithLength.java
==============================================================================
--- release/sling/src/main/java/org/apache/sling/testing/clients/util/InputStreamBodyWithLength.java (added)
+++ release/sling/src/main/java/org/apache/sling/testing/clients/util/InputStreamBodyWithLength.java Thu Apr 9 13:50:42 2020
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.sling.testing.clients.util;
+
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.mime.content.InputStreamBody;
+import org.apache.sling.testing.clients.ClientException;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * If we want to upload a file that is a resource in a jar file, the http client expects a content length.
+ */
+public class InputStreamBodyWithLength extends InputStreamBody {
+ private long streamLength;
+
+ public InputStreamBodyWithLength(String resourcePath, String contentType, String fileName) throws ClientException {
+ super(ResourceUtil.getResourceAsStream(resourcePath), ContentType.create(contentType), fileName);
+ this.streamLength = getResourceStreamLength(resourcePath);
+ }
+
+ @Override
+ public long getContentLength() {
+ return streamLength;
+ }
+
+ /**
+ * Returns the length of a resource (which is needed for the InputStreamBody
+ * to work. Can't currently think of a better solution than going through
+ * the resource stream and count.
+ *
+ * @param resourcePath path to the file
+ * @return the size of the resource
+ */
+ private static long getResourceStreamLength(String resourcePath) throws ClientException {
+ int streamLength = 0;
+ InputStream stream = ResourceUtil.getResourceAsStream(resourcePath);
+ try {
+ for (int avail = stream.available(); avail > 0; avail = stream.available()) {
+ streamLength += avail;
+ stream.skip(avail);
+ }
+ } catch (IOException e) {
+ throw new ClientException("Could not read " + resourcePath + "!", e);
+ } finally {
+ try {
+ stream.close();
+ } catch (IOException e) {
+ throw new ClientException("Could not close Inputstream for " + resourcePath + "!", e);
+ }
+ }
+ return streamLength;
+ }
+}
\ No newline at end of file