You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ro...@apache.org on 2017/10/20 14:44:20 UTC
[sling-org-apache-sling-testing-clients] 01/37: SLING-5703 - new
http/clients module,
extracted and enhanced from testing/tools. Contributed by Andrei Dulvac,
thanks!
This is an automated email from the ASF dual-hosted git repository.
rombert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-testing-clients.git
commit b29fdd63e70afb0732c7851fca882143844f5e82
Author: Bertrand Delacretaz <bd...@apache.org>
AuthorDate: Fri Apr 29 14:03:32 2016 +0000
SLING-5703 - new http/clients module, extracted and enhanced from testing/tools. Contributed by Andrei Dulvac, thanks!
git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1741632 13f79535-47bb-0310-9956-ffa450edef68
---
README.md | 211 ++++++
pom.xml | 148 +++++
.../sling/testing/clients/AbstractSlingClient.java | 715 +++++++++++++++++++++
.../sling/testing/clients/ClientException.java | 61 ++
.../apache/sling/testing/clients/Constants.java | 55 ++
.../apache/sling/testing/clients/SlingClient.java | 658 +++++++++++++++++++
.../sling/testing/clients/SlingClientConfig.java | 227 +++++++
.../sling/testing/clients/SlingHttpResponse.java | 396 ++++++++++++
.../clients/instance/InstanceConfiguration.java | 41 ++
.../testing/clients/instance/InstanceSetup.java | 88 +++
.../interceptors/DelayRequestInterceptor.java | 47 ++
.../clients/interceptors/StickyCookieHolder.java | 40 ++
.../interceptors/StickyCookieInterceptor.java | 61 ++
.../clients/interceptors/StickyCookieSpec.java | 47 ++
.../interceptors/TestDescriptionHolder.java | 49 ++
.../interceptors/TestDescriptionInterceptor.java | 42 ++
.../testing/clients/interceptors/package-info.java | 24 +
.../apache/sling/testing/clients/osgi/Bundle.java | 55 ++
.../sling/testing/clients/osgi/BundleInfo.java | 124 ++++
.../sling/testing/clients/osgi/BundlesInfo.java | 144 +++++
.../testing/clients/osgi/BundlesInstaller.java | 186 ++++++
.../sling/testing/clients/osgi/Component.java | 51 ++
.../sling/testing/clients/osgi/ComponentInfo.java | 69 ++
.../sling/testing/clients/osgi/ComponentsInfo.java | 95 +++
.../testing/clients/osgi/OsgiConsoleClient.java | 324 ++++++++++
.../testing/clients/osgi/OsgiInstanceConfig.java | 94 +++
.../testing/clients/osgi/WebconsoleClient.java | 190 ++++++
.../sling/testing/clients/osgi/package-info.java | 23 +
.../apache/sling/testing/clients/package-info.java | 24 +
.../testing/clients/util/FormEntityBuilder.java | 81 +++
.../sling/testing/clients/util/HttpUtils.java | 183 ++++++
.../clients/util/InputStreamBodyWithLength.java | 69 ++
.../sling/testing/clients/util/JsonUtils.java | 44 ++
.../sling/testing/clients/util/PortAllocator.java | 58 ++
.../sling/testing/clients/util/ResourceUtil.java | 66 ++
.../sling/testing/clients/util/SlingParameter.java | 108 ++++
.../testing/clients/util/URLParameterBuilder.java | 79 +++
.../sling/testing/clients/util/UniquePaths.java | 67 ++
.../sling/testing/clients/util/XSSUtils.java | 137 ++++
.../clients/util/config/InstanceConfig.java | 40 ++
.../clients/util/config/InstanceConfigCache.java | 25 +
.../util/config/InstanceConfigException.java | 39 ++
.../util/config/impl/EmptyInstanceConfig.java | 36 ++
.../util/config/impl/InstanceConfigCacheImpl.java | 120 ++++
.../testing/clients/util/config/package-info.java | 24 +
.../clients/util/poller/AbstractPoller.java | 69 ++
.../sling/testing/clients/util/poller/Poller.java | 28 +
.../sling/testing/timeouts/TimeoutsProvider.java | 77 +++
.../testing/AbstractSlingClientGetPathTest.java | 162 +++++
.../AbstractSlingClientGetServerUrlTest.java | 56 ++
.../testing/AbstractSlingClientGetUrlTest.java | 122 ++++
.../sling/testing/DelayRequestInterceptorTest.java | 34 +
.../apache/sling/testing/util/UniquePathsTest.java | 76 +++
.../testing/util/poller/AbstractPollerTest.java | 107 +++
54 files changed, 6196 insertions(+)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..adce441
--- /dev/null
+++ b/README.md
@@ -0,0 +1,211 @@
+# Sling Http Clients
+
+`SlingClient` is a specialized
+[`HttpClient`](https://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/client/HttpClient.html)
+that provides additional functionalities specific to Sling. It is designed to be easy to use out of the box, but also fully customizable.
+This library comes with a bunch of other specialized clients (built on top of `SlingClient`) that are ready to use.
+
+## <a name="architecture"></a> Architecture
+
+`SlingClient` implements the `HttpClient` interface, but [deletegates](https://en.wikipedia.org/wiki/Delegation_pattern)
+this functionality to a `private final CloseableHttpClient http` field.
+The config is stored in a `private final SlingClientConfig config` field which is immutable and may be shared across multiple clients
+(more about it in the [How to configure a SlingClient](#config) section).
+These two objects define the state of the client and are built to make the client thread safe.
+
+`SlingClient` is designed in two layers:
+* The base `class AbstractSlingClient implements HttpClient` provides an overlay of basic http methods such as `doGet()`,
+ `doPost()` & similar. These are meant to be full replacements of the ones in `HttpClient` for Sling specific needs,
+ and they add specific customizations. One particularity is that they all return `SlingHttpResponse`, an augmented `HttpResponse`.
+
+ Still, all the methods from `HttpClient` are exposed (through inheritance and delegation) in case one needs the raw functionality.
+ Some useful methods to manipulate Sling paths and URLs have also been added (`getUrl()`, `getPath()`).
+
+ This class encapsulates the mechanisms for extensibility (immutable config field, delegate client field, package private constructor,
+ `adaptTo()`), but it is defined as abstract and should never be used directly.
+
+* The main `class SlingClient extends AbstractSlingClient` is the one that adds Sling specific methods (`createNode()`,
+ `deletePath()` etc.). It has no fields, but makes use of everything that was defined in the super class.
+ Another main functionality defined in `SlingClient` are the mechanisms to instantiate a SlingClient (and any other sub-class):
+
+ * constructor: `public SlingClient(URI url, String user, String password) throws ClientException`
+
+ * builder: `public final static class Builder extends InternalBuilder<SlingClient>` (more in [How to write a `Builder`](#builder))
+
+Any client you write should extend `SlingClient` (more in [How to extend `SlingClient`](#extend))
+
+## <a name="instantiate"></a> How to instantiate `SlingClient`
+There are several ways to obtain a SlingClient (and sub-client) instance, depending on the resources available:
+
+* constructor `SlingClient(URI url, String user, String password)` - handy for obtaining a simple client from the url:
+ ```java
+ SlingClient c = new SlingClient(URI.create("localhost:8080"), "admin", "admin");
+ ```
+
+* builder `class Builder<T extends Builder> extends HttpClientBuilder` - this allows for more complex clients to be created, e.g.
+ with different authentication mechanism, or additional interceptors:
+ ```java
+ SlingClient c = SlingClient.Builder.create("localhost:8080", "admin", "admin").build();
+ ```
+ This gives the possibility to customize the HttpClient (e.g. add interceptors, change auth method) before constructing it.
+
+* `public <T extends AbstractSlingClient> T adaptTo(Class<T> clientClass)` is the convenient method to obtain another specialized
+client form an existing one. The advantage is that the two will share the same configuration and http handler, so they will behave
+like two different "facets" of the same client (think about the analogy of a Web browser with multiple tabs).
+
+Although the constructor and the builder are very handy, the preferred way of working with clients is to obtain it using one of the
+Junit Rules provided (e.g. `ExistingQuickstart`) and then call `adaptTo()`.
+
+## <a name="config"></a> How to configure `SlingClient`
+All the configs specific to `SlingClient` are stored in `private final SlingClientConfig config` which contains fields such as
+`url`, `cookieStore` and authentication parameters. These fields can be set only indirectly, through constructor or Builder, and only
+before constructing the client. They cannot be changed, so if you need to change something, you must instantiate another client.
+
+`SlingClient` was designed to be immutable, so thread safe. You don't have to worry about synchronizing it when running tests in parallel.
+Also, the immutable config is the base for the `adaptTo()` mechanism, since the two clients can share the same config.
+
+## <a name="extend"></a> How to extend `SlingClient`
+The `SlingClient` was designed with extensibility in mind. That's why it provides only basic functionality, leaving other specialized
+clients to implement the rest. To create a new client class (let's call it `MyClient`), you need to:
+* extend SlingClient: `class MyClient extends SlingClient`
+* implement the two constructors:
+ * the one for simple uses:
+ ```java
+ public MyClient(URI serverUrl, String userName, String password) throws ClientException {
+ super(serverUrl, userName, password);
+ }
+ ```
+ * the one used by `adaptTo()` (so don't forget it!):
+ ```java
+ public MyClient(CloseableHttpClient http, SlingClientConfig config) throws ClientException {
+ super(http, config);
+ }
+ ```
+ * optionally create your `Builder`, but only if needed (more in [How to write a `Builder`](#builder))
+
+A good example of how `SlingClient` can be extended is `OsgiConsoleClient`. Note you can further extend the sub-clients in the same way.
+
+## <a name="builder"></a> How to write a `Builder`
+If you need to make your client customizable you will have to write your own Builder (you were no thinking to break the immutability
+by adding a setter, right?). Below is an example of how to create the Builder mechanism that you can take and adapt for your needs.
+In this case, we try to expose only one field `foo`, but it can be extended to any number of fields. Although it seems complicated,
+if you follow exactly the example, you cannot fail. Trying to simplify it will burn you (sooner or later), you have been warned!
+
+A short description of the Builder architecture would be: the `InternalBuilder` contains all the logic while staying extensible, while
+`Builder` takes all the credit by exposing the `build()` method. Yet, the `Builder` cannot be extended because all the sub-classes would
+return a `SlingClient` when calling `build()` (and not a subclass instance).
+
+```java
+@Immutable
+public class MyClient extends SlingClient {
+
+ private final String foo;
+
+ public MyClient(URI serverUrl, String user, String password) throws ClientException {
+ super(serverUrl, user, password);
+ }
+
+ /**
+ * Constructor used by Builders and adaptTo(). <b>Should never be called directly from the code.</b>
+ *
+ * @see AbstractSlingClient#AbstractSlingClient(CloseableHttpClient, SlingClientConfig)
+ */
+ public MyClient(CloseableHttpClient http, SlingClientConfig config) throws ClientException {
+ super(http, config);
+ }
+
+ public static abstract class InternalBuilder<T extends MyClient> extends SlingClient.InternalBuilder<T> {
+ protected String foo;
+
+ protected InternalBuilder(URI url, String user, String password) {
+ super(url, user, password);
+ }
+
+ public InternalBuilder<T> withFoo(String foo) {
+ this.foo = foo;
+ }
+ }
+
+ public final static class Builder extends InternalBuilder<MyClient> {
+
+ private Builder(URI url, String user, String password) {
+ super(url, user, password);
+ }
+
+ @Override
+ public MyClient build() throws ClientException {
+ MyClient client = new MyClient(buildHttpClient(), buildSlingClientConfig());
+ client.foo = this.foo;
+ return client;
+ }
+
+ public static Builder create(URI url, String user, String password) {
+ return new Builder(url, user, password);
+ }
+ }
+}
+```
+
+## FAQ
+##### How can I change the server url of an existing client?
+You don't. As described in [How to configure a `SlingClient`](#config), you have to instantiate another client to change the config.
+
+##### How can I create a client for a server url with context path?
+The server `url` (passed in the constructor or builder) must contain all the elements, including protocol, hostname, port and eventually
+the context path, e.g.: `http://localhost:8080/mycontextpath/`.
+The url may (or may not) contain the trailing slash. Yet, the client will always store it with a trailing slash:
+```java
+SlingClient client = new SlingClient("http://localhost:4502/mycontextpath", "user", "pass");
+System.out.println(client.getUrl());
+// prints http://localhost:4502/mycontextpath/
+```
+
+##### How can I customize the underlying `HttpClient`?
+The `SlingClient.Builder` directly exposes the most useful methods from `HttpClientBuilder`, but not all of them.
+First, check if you can find it there. If you haven't found your specific method, then the `Builder` exposes an `HttpClientBuilder` through
+`public HttpClientBuilder httpClientBuilder()` which you can use to config it. Note that in this case you cannot chain the methods
+to build the client, so you will need to keep a reference to the `SlingClient.Builder`:
+```java
+SlingClient.Builder builder = SlingClient.Builder.create("http://localhost:8080", "user", "pass");
+HttpClientBuilder httpBuilder = builder.httpClientBuilder();
+httpBuilder.setProxy(myProxy);
+builder.setUser("another");
+SlingClient client = builder.build();
+```
+
+##### Why is the `Builder` pattern so complicated? Do I really need two classes?
+Don't try to get creative here. Respect the examples provided and don't take shortcuts, otherwise you will hurt yourself.
+
+We have tried different ways of designing the Builder. This is the best compromise between extensibility and simplicity. The
+`HttpClientBuilder` does not offer any extensibility support, so `SlingClient.Builder` does not extend it, it just uses it internally.
+Always remember that you don't need to create your Builder, unless you want to add custom fields to the client.
+
+##### Why I cannot use the entity's content InputStream?
+`SlingClient#doRequest()`, `SlingClient#doGet()`, `SlingClient#doPost()` & co. are all consuming the entity and caching it as
+String. This is by design, since there's a big risk to forget closing the connections and to run out of sockets quickly.
+If you need the response content as InputStream (e.g. for downloading a binary), you can use `doStreamGet()` or similar. These
+methods were written specially to not consume the entity so it's the caller's responsibility to close it when finished. Remember to use
+them with caution and only when needed.
+
+##### Can my client use another authentication method?
+The username and password required by the constructor and builder are there for convenience (since more than 90% of cases will use
+basic auth). But you can easily overwrite the `CredentialsProvider` in Builder so those will be ignored. Or do anything you want with
+that `HttpClientBuilder`...
+
+##### How can I obtain the context path?
+`client.getUrl().getPath()`
+
+##### How can I obtain the "relative" url (excluding hostname and port, but including context path)?
+`client.getUrl(path).getPath()`
+
+##### How can I remove the context path from a path?
+`client.getPath(path)`
+
+##### What if I pass an url or a path with or without context path to `getUrl()` or `getPath()`?
+We have tried to make these methods as robust as possible. Their job is very clear:
+* `getUrl(String path)` to transform a Sling path into a full url
+* `getPath(String url)` to transform a full url into a Sling path
+
+Any input that does not respect the contract might not work. Check `AbstractSlingClientGetPathTest` and `AbstractSlingClientGetUrlTest`
+for an extensive list of cases that we have considered when writing these methods.
+
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..f0da527
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,148 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>sling</artifactId>
+ <version>26</version>
+ <relativePath />
+ </parent>
+
+ <artifactId>org.apache.sling.testing.clients</artifactId>
+ <version>0.1.0-SNAPSHOT</version>
+ <packaging>bundle</packaging>
+
+ <name>Apache Sling Testing Clients</name>
+ <description>
+ Sling testing http clients and utils
+ </description>
+
+ <scm>
+ <connection>scm:svn:http://svn.apache.org/repos/asf/sling/trunk/testing/http/clients</connection>
+ <developerConnection> scm:svn:https://svn.apache.org/repos/asf/sling/trunk/testing/http/clients</developerConnection>
+ <url>http://svn.apache.org/viewvc/sling/trunk/testing/http/clients</url>
+ </scm>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-scr-plugin</artifactId>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <extensions>true</extensions>
+ <configuration>
+ <instructions>
+ <Export-Package>
+ org.apache.sling.testing.clients.*,
+ </Export-Package>
+ <Import-Package>
+ org.apache.commons.exec.*; resolution:=optional,
+ *
+ </Import-Package>
+ </instructions>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.testing.tools</artifactId>
+ <version>1.0.12</version>
+ <exclusions>
+ <exclusion>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpcore</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.apache.felix.scr.annotations</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-exec</artifactId>
+ <version>1.1</version>
+ </dependency>
+ <dependency>
+ <groupId>org.codehaus.jackson</groupId>
+ <artifactId>jackson-core-asl</artifactId>
+ <version>1.5.5</version>
+ </dependency>
+ <dependency>
+ <groupId>org.codehaus.jackson</groupId>
+ <artifactId>jackson-mapper-asl</artifactId>
+ <version>1.5.5</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpclient</artifactId>
+ <version>4.5</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpmime</artifactId>
+ <version>4.5</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-lang3</artifactId>
+ <version>3.4</version>
+ </dependency>
+ <dependency>
+ <groupId>org.jsoup</groupId>
+ <artifactId>jsoup</artifactId>
+ <version>1.7.2</version>
+ </dependency>
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ <version>14.0.1</version>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ <version>1.7.5</version>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-simple</artifactId>
+ <version>1.7.5</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.xss</artifactId>
+ <version>1.0.4</version>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/src/main/java/org/apache/sling/testing/clients/AbstractSlingClient.java b/src/main/java/org/apache/sling/testing/clients/AbstractSlingClient.java
new file mode 100644
index 0000000..24d731f
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/AbstractSlingClient.java
@@ -0,0 +1,715 @@
+/*
+ * 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;
+
+import org.apache.http.*;
+import org.apache.http.annotation.Immutable;
+import org.apache.http.client.*;
+import org.apache.http.client.methods.*;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.client.utils.URIBuilder;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.message.BasicHttpRequest;
+import org.apache.http.protocol.HttpContext;
+import org.apache.sling.testing.clients.util.HttpUtils;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * The abstract base client for all implementing integration test clients.
+ */
+@Immutable
+public class AbstractSlingClient implements HttpClient {
+
+ private final org.slf4j.Logger log = LoggerFactory.getLogger(getClass());
+
+ private final static URI slash = URI.create("/");
+
+ /**
+ * The clientId for the client, generated automatically during instantiation of client.
+ */
+ private final String clientId;
+
+ /**
+ * The HttpClient object to which http calls are delegated.
+ * It can be shared across multiple AbstractSlingClients (by using adaptTo())
+ */
+ private final CloseableHttpClient http;
+
+ /**
+ * A wrapper object containing the sling config for this client.
+ * It can be shared across multiple AbstractSlingClients (by using adaptTo())
+ */
+ private final SlingClientConfig config;
+
+ /**
+ * Constructor used by Builders and adaptTo(). <b>Should never be called directly from the code.</b>
+ *
+ * @param http http client to handle the delegated calls
+ * @param config immutable object holding the config
+ * @throws ClientException if the client could not be initialized
+ */
+ AbstractSlingClient(CloseableHttpClient http, SlingClientConfig config) throws ClientException {
+ // Generate client ID
+ this.clientId = this.getClass() + "-" + UUID.randomUUID().toString();
+ this.http = http;
+ this.config = config;
+ }
+
+ /**
+ * Returns the unique id for this client, generated automatically during instantiation.<br>
+ *
+ * @return client's unique id
+ */
+ protected String getClientId() {
+ return clientId;
+ }
+
+ /**
+ * <p>Base HTTP URI of the server under test. It includes the context path, if present, and always ends with a slash</p>
+ * <p>Example: {@code http://localhost:8080/a/}</p>
+ *
+ * @return the server's URL
+ */
+ public URI getUrl() {
+ return config.getUrl();
+ }
+
+
+ /**
+ * Returns the name of the user that will be used to authenticate the requests (by basic auth, if not replaced).
+ *
+ * @return user's name
+ */
+ public String getUser() {
+ return config.getUser();
+ }
+
+ /**
+ * Returns the password of the user that will be used to authenticate the requests (by basic auth, if not replaced).
+ *
+ * @return user's password
+ */
+ public String getPassword() {
+ return config.getPassword();
+ }
+
+ /**
+ * <p>Gets the full URL for a given path.</p>
+ *
+ * <p>The input path is considered relative to server url path ("/" or context path), even though it starts with a slash.
+ * The path is relativized and appended to the {@code server url}.</p>
+ *
+ * <p>Note: in the case of a server url with context path - the input path should not contain the context path, otherwise
+ * it will be duplicated in the resulting url</p>
+ *
+ * @param path the relative path
+ * @return the absolute URI
+ * @throws IllegalArgumentException if path cannot be parsed into an URI
+ * @throws NullPointerException if path is null
+ */
+ public URI getUrl(String path) {
+ try {
+ URI pathUri = slash.relativize(new URI(path));
+ return getUrl().resolve(pathUri);
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ /**
+ * Creates a full URL for a given path with additional parameters. Same as {@link #getUrl(String)}, but adds the parameters in the URI.
+ *
+ * @param path path relative to server url; can start with / but should not include the server context path
+ * @param parameters url parameters to be added to the url
+ * @return full url as URI
+ * @throws IllegalArgumentException if path or parameters cannot be parsed into an URI
+ * @throws NullPointerException if path is null
+ */
+ public URI getUrl(String path, List<NameValuePair> parameters) {
+ // add server url and path
+ URIBuilder uriBuilder = new URIBuilder(getUrl(path));
+ // add parameters
+ parameters = (parameters != null) ? parameters : new ArrayList<NameValuePair>(0);
+ uriBuilder.addParameters(parameters);
+
+ try {
+ return uriBuilder.build();
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ /**
+ * <p>Transforms an external {@code url} into a sling path, by subtracting the {@code server url} (incl. contextPath).
+ * The returned path will not contain the context path, so it can be used with {@link #getUrl(String)}</p>
+ *
+ * <p>The url can be absolute (incl. hostname) or relative to root (starts with "/").</p>
+ *
+ * <p>If the server url is not a prefix of the given url, it returns the given url</p>
+ *
+ * <p>If the url is just a path, it returns the path (with leading slash if not already present)</p>
+ *
+ * @param url full url
+ * @return sling path
+ */
+ public URI getPath(URI url) {
+ // special case for urls that are server urls, but without trailing slash
+ if (url.relativize(getUrl()).equals(URI.create(""))) {
+ return slash;
+ }
+
+ URI contextPath = URI.create(getUrl().getPath());
+ URI relativeUrl = contextPath.relativize(slash.resolve(url));
+
+ if (relativeUrl.relativize(contextPath).equals(URI.create(""))) {
+ return slash;
+ }
+
+ return slash.resolve(getUrl().relativize(relativeUrl));
+ }
+
+ /**
+ * Extracts the relative sling path (to server url) from an url. Identical to {@link AbstractSlingClient#getPath(URI)},
+ * except that it also parses the String int URI
+ *
+ * @param url string containing the full url
+ * @return relative path as URI
+ * @throws IllegalArgumentException if the parameter cannot be parsed
+ * @throws NullPointerException if url is null
+ */
+ public URI getPath(String url) {
+ try {
+ return getPath(new URI(url));
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ /**
+ * <p>Returns an instance of any class extending the AbstractSlingClient. The new client will
+ * use the the same {@link HttpClient} and {@link SlingClientConfig} </p>
+ *
+ * @param clientClass the type of client requested, identified by its Class
+ * @param <T> any class extending the AbstractSlingClient
+ * @return instance of a class extending the AbstractSlingClient
+ * @throws ClientException if client can't be instantiated
+ */
+ @SuppressWarnings("unchecked")
+ public <T extends AbstractSlingClient> T adaptTo(Class<T> clientClass) throws ClientException {
+ T client;
+ try {
+ Constructor cons = clientClass.getConstructor(CloseableHttpClient.class, SlingClientConfig.class);
+ client = (T) cons.newInstance(this.http, this.config);
+ } catch (Exception e) {
+ throw new ClientException("Could not initialize client: '" + clientClass.getCanonicalName() + "'.", e);
+ }
+ return client;
+ }
+
+ /**
+ * Gets the value for {@code key} from the generic values
+ *
+ * @param key the key
+ * @return the value
+ */
+ public String getValue(String key) {
+ return this.config.getValues().get(key);
+ }
+
+ /**
+ * Adds the extra {@code key, value} to the generic values
+ *
+ * @param key the key for witch to add a value
+ * @param value the value
+ */
+ public void addValue(String key, String value) {
+ this.config.getValues().put(key, value);
+ }
+
+ /**
+ * Checks whether the handler has the given generic value
+ *
+ * @param key the key
+ * @return true if the value was found
+ */
+ public boolean hasValue(String key) {
+ return config.getValues().containsKey(key);
+ }
+
+ /**
+ * Returns the extra values map
+ *
+ * @return the map of values
+ */
+ public Map<String, String> getValues() {
+ return config.getValues();
+ }
+
+ /**
+ * @return the cookie store reference
+ */
+ public CookieStore getCookieStore() {
+ return config.getCookieStore();
+ }
+
+ /**
+ * @return the credentials provider
+ */
+ public CredentialsProvider getCredentialsProvider() {
+ return config.getCredsProvider();
+ }
+
+ //
+ // HTTP convenience methods
+ //
+
+ /**
+ * <p>Executes an HTTP request, WITHOUT consuming the entity in the response. The caller is responsible for consuming the entity or
+ * closing the response's InputStream in order to release the connection.
+ * Otherwise, the client might run out of connections and will block</p>
+ *
+ * <p><b>Use this with caution and only if necessary for streaming</b>, otherwise use the safe method
+ * {@link #doRequest(HttpUriRequest, List, int...)}</p>
+ *
+ * <p>Adds the headers and checks the response against expected status</p>
+ *
+ * @param request the request to be executed
+ * @param headers optional headers to be added to the request
+ * @param expectedStatus if passed, the response status is checked against it/them, and has to match at least one of them
+ * @return the response, with the entity not consumed
+ * @throws ClientException if the request could not be executed
+ */
+ public SlingHttpResponse doStreamRequest(HttpUriRequest request, List<Header> headers, int... expectedStatus)
+ throws ClientException {
+ // create context from config
+ HttpClientContext context = createHttpClientContextFromConfig();
+
+ // add headers
+ if (headers != null) {
+ request.setHeaders(headers.toArray(new Header[headers.size()]));
+ }
+
+ try {
+ log.debug("request {} {}", request.getMethod(), request.getURI());
+ SlingHttpResponse response = new SlingHttpResponse(this.execute(request, context));
+ log.debug("response {}", HttpUtils.getHttpStatus(response));
+ // Check the status and throw a ClientException if it doesn't match expectedStatus, but close the entity before
+ if (expectedStatus != null && expectedStatus.length > 0) {
+ try {
+ HttpUtils.verifyHttpStatus(response, expectedStatus);
+ } catch (ClientException e) {
+ // catch the exception to make sure we close the entity before re-throwing it
+ response.close();
+ throw e;
+ }
+ }
+
+ return response;
+ } catch (IOException e) {
+ throw new ClientException("Could not execute http request", e);
+ }
+ }
+
+ /**
+ * <p>Executes a raw HTTP request, WITHOUT consuming the entity in the response. The caller is responsible for consuming the entity or
+ * closing the response's InputStream in order to release the connection.
+ * Otherwise, the client might run out of connections and will block</p>
+ *
+ * <p><b>Use this with caution and only if necessary for custom methods or for paths that must not be encoded</b>,
+ * otherwise use the safe method {@link #doRequest(HttpUriRequest, List, int...)}</p>
+ *
+ * <p>It behaves as {@link #doStreamRequest(HttpUriRequest, List, int...)}, so the entity is not consumed.</p>
+ * <p>Adds the headers and checks the response against expected status</p>
+ *
+ * @param method the request to be executed
+ * @param uri the uri to be sent as it is (will not prepend the context path)
+ * @param headers optional headers to be added to the request
+ * @param expectedStatus if passed, the response status is checked against it/them, and has to match at least one of them
+ * @return the response, with the entity not consumed
+ * @throws ClientException if the request could not be executed
+ */
+ public SlingHttpResponse doRawRequest(String method, String uri, List<Header> headers, int... expectedStatus)
+ throws ClientException {
+ // create context from config
+ HttpClientContext context = createHttpClientContextFromConfig();
+
+ HttpHost host = new HttpHost(getUrl().getHost(), getUrl().getPort(), getUrl().getScheme());
+ HttpRequest request = new BasicHttpRequest(method, uri);
+
+ // add headers
+ if (headers != null) {
+ request.setHeaders(headers.toArray(new Header[headers.size()]));
+ }
+
+ try {
+ log.debug("request {} {}", method, uri);
+ SlingHttpResponse response = new SlingHttpResponse(this.execute(host, request, context));
+ log.debug("response {}", HttpUtils.getHttpStatus(response));
+ // Check the status and throw a ClientException if it doesn't match expectedStatus, but close the entity before
+ if (expectedStatus != null && expectedStatus.length > 0) {
+ try {
+ HttpUtils.verifyHttpStatus(response, expectedStatus);
+ } catch (ClientException e) {
+ // catch the exception to make sure we close the entity before re-throwing it
+ response.close();
+ throw e;
+ }
+ }
+
+ return response;
+ } catch (IOException e) {
+ throw new ClientException("Could not execute http request", e);
+ }
+ }
+
+ private HttpClientContext createHttpClientContextFromConfig() {
+ // create context from config
+ HttpClientContext context = HttpClientContext.create();
+
+ if (config.getCookieStore() != null) {
+ context.setCookieStore(config.getCookieStore());
+ }
+
+ if (config.getCredsProvider() != null) {
+ context.setCredentialsProvider(config.getCredsProvider());
+ }
+
+ if (config.getAuthCache() != null) {
+ context.setAuthCache(config.getAuthCache());
+ }
+
+ return context;
+ }
+
+ /**
+ * <p>Executes a GET request WITHOUT consuming the entity in the response. The caller is responsible to close the connection.
+ * Otherwise, the client might run out of connections and will block</p>
+ *
+ * <p><b>Use this with caution and only if necessary for streaming</b>, otherwise use the safe method
+ * {@link #doGet(String, List, List, int...)}</p>
+ *
+ * <p>Adds the given parameters and headers and checks the response against expected status</p>
+ * @param requestPath path relative to client url
+ * @param parameters optional url parameters to be added
+ * @param headers optional headers to be added
+ * @param expectedStatus if passed, the response status will have to match one of them
+ * @return the response with the entity not consumed
+ * @throws ClientException if the request could not be executed
+ */
+ public SlingHttpResponse doStreamGet(String requestPath, List<NameValuePair> parameters, List<Header> headers, int... expectedStatus)
+ throws ClientException {
+ // create full uri, including server url, given path and given parameters
+ URI uri = getUrl(requestPath, parameters);
+ // execute request
+ HttpUriRequest request = new HttpGet(uri);
+ return doStreamRequest(request, headers, expectedStatus);
+ }
+
+ /**
+ * <p>Executes a POST request WITHOUT consuming the entity in the response. The caller is responsible to close the connection</p>
+ *
+ * <p><b>Use this with caution and only if necessary for streaming</b>, otherwise use the safe method
+ * {@link #doPost(String, HttpEntity, List, int...)}</p>
+ *
+ * <p>Adds the headers and checks the response against expected status</p>
+ * @param requestPath path relative to client url
+ * @param entity http entity to be sent by POST
+ * @param headers optional headers to be added
+ * @param expectedStatus if passed, the response status will have to match one of them
+ * @return the response with the entity not consumed
+ * @throws ClientException if the request could not be executed
+ */
+ public SlingHttpResponse doStreamPost(String requestPath, HttpEntity entity, List<Header> headers, int... expectedStatus)
+ throws ClientException {
+ HttpEntityEnclosingRequestBase request = new HttpPost(getUrl(requestPath));
+ if (entity != null) {
+ request.setEntity(entity);
+ }
+ return doStreamRequest(request, headers, expectedStatus);
+ }
+
+ /**
+ * <p>Execute an HTTP request and consumes the entity in the response. The content is cached and can be retrieved using
+ * {@code response.getContent()}.
+ * This method is safe to use because it closes the entity so the caller has no responsibility.</p>
+ *
+ * <p>This means the response entity SHOULD NOT BE USED to read the content, e.g. {@code response.getEntity().getContent()}</p>
+ *
+ * @param request the request to be executed
+ * @param headers optional headers to be added to the request
+ * @param expectedStatus if passed, the response status will have to match one of them
+ * @return the response with the entity consumed and the content cached
+ * @throws ClientException if the request could not be executed
+ */
+ public SlingHttpResponse doRequest(HttpUriRequest request, List<Header> headers, int... expectedStatus) throws ClientException {
+ SlingHttpResponse response = doStreamRequest(request, headers, expectedStatus);
+
+ // Consume entity and cache the content so the connection is closed
+ response.getContent();
+
+ return response;
+ }
+
+ /**
+ * <p>Executes a GET request and consumes the entity in the response (so the connection is closed immediately)
+ * The content is cached and can be retrieved using {@code response.getContent()}.</p>
+ *
+ * <p>Adds the passed parameters and headers and checks the expected status</p>
+ *
+ * @param requestPath path relative to client url
+ * @param parameters optional url parameters to be added
+ * @param headers optional headers to be added
+ * @param expectedStatus if passed, the response status will have to match one of them
+ * @return the response with the entity consumed amd the content cached
+ * @throws ClientException if the request could not be executed
+ */
+ public SlingHttpResponse doGet(String requestPath, List<NameValuePair> parameters, List<Header> headers, int... expectedStatus)
+ throws ClientException {
+ SlingHttpResponse response = doStreamGet(requestPath, parameters, headers, expectedStatus);
+
+ // Consume entity and cache the content so the connection is closed
+ response.getContent();
+
+ return response;
+ }
+
+ /**
+ * <p>Executes a GET request and consumes the entity in the response (so the connection is closed immediately)
+ * The content is cached and can be retrieved using {@code response.getContent()}.</p>
+ *
+ * <p>Adds the passed parameters and checks the expected status</p>
+ *
+ * @param requestPath path relative to client url
+ * @param parameters optional url parameters to be added
+ * @param expectedStatus if passed, the response status will have to match one of them
+ * @return the response with the entity consumed amd the content cached
+ * @throws ClientException if the request could not be executed
+ */
+ public SlingHttpResponse doGet(String requestPath, List<NameValuePair> parameters, int... expectedStatus)
+ throws ClientException {
+ return doGet(requestPath, parameters, null, expectedStatus);
+ }
+
+ /**
+ * <p>Executes a GET request and consumes the entity in the response (so the connection is closed immediately)
+ * The content is cached and can be retrieved using {@code response.getContent()}.</p>
+ *
+ * @param requestPath path relative to client url
+ * @param expectedStatus if passed, the response status will have to match one of them
+ * @return the response with the entity consumed amd the content cached
+ * @throws ClientException if the request could not be executed
+ */
+ public SlingHttpResponse doGet(String requestPath, int... expectedStatus)
+ throws ClientException {
+ return doGet(requestPath, null, null, expectedStatus);
+ }
+
+ /**
+ * <p>Executes a HEAD request</p>
+ *
+ * <p>Adds the passed parameters and headers and checks the expected status</p>
+ *
+ * @param requestPath path relative to client url
+ * @param parameters optional url parameters to be added
+ * @param headers optional headers to be added
+ * @param expectedStatus if passed, the response status will have to match one of them
+ * @return the response
+ * @throws ClientException if the request could not be executed
+ */
+ public SlingHttpResponse doHead(String requestPath, List<NameValuePair> parameters, List<Header> headers, int... expectedStatus)
+ throws ClientException {
+ HttpUriRequest request = new HttpHead(getUrl(requestPath, parameters));
+ return doRequest(request, headers, expectedStatus);
+ }
+
+
+ /**
+ * <p>Executes a POST request and consumes the entity in the response. The content is cached and be retrieved by calling
+ * {@code response.getContent()}</p>
+ *
+ * <p>Adds the passed entity and headers and checks the expected status</p>
+ *
+ * @param requestPath path relative to client url
+ * @param entity the entity to be added to request
+ * @param headers optional headers to be added
+ * @param expectedStatus if passed, the response status will have to match one of them
+ * @return the response with the entity consumed and the content cached
+ * @throws ClientException if the request could not be executed
+ */
+ public SlingHttpResponse doPost(String requestPath, HttpEntity entity, List<Header> headers, int... expectedStatus)
+ throws ClientException {
+ HttpEntityEnclosingRequestBase request = new HttpPost(getUrl(requestPath));
+ if (entity != null) {
+ request.setEntity(entity);
+ }
+ return doRequest(request, headers, expectedStatus);
+ }
+
+ /**
+ * <p>Executes a POST request and consumes the entity in the response. The content is cached and be retrieved by calling
+ * {@code response.getContent()}</p>
+ *
+ * <p>Adds the passed entity and checks the expected status</p>
+ *
+ * @param requestPath path relative to client url
+ * @param entity the entity to be added to request
+ * @param expectedStatus if passed, the response status will have to match one of them
+ * @return the response with the entity consumed and the content cached
+ * @throws ClientException if the request could not be executed
+ */
+ public SlingHttpResponse doPost(String requestPath, HttpEntity entity, int... expectedStatus)
+ throws ClientException {
+ return doPost(requestPath, entity, null, expectedStatus);
+ }
+
+ /**
+ * <p>Executes a PUT request and consumes the entity in the response. The content is cached and be retrieved by calling
+ * {@code response.getContent()}</p>
+ *
+ * <p>Adds the passed entity and headers and checks the expected status</p>
+ *
+ * @param requestPath path relative to client url
+ * @param entity the entity to be added to request
+ * @param headers optional url parameters to be added
+ * @param expectedStatus if passed, the response status will have to match one of them
+ * @return the response with the entity consumed and the content cached
+ * @throws ClientException if the request could not be executed
+ */
+ public SlingHttpResponse doPut(String requestPath, HttpEntity entity, List<Header> headers, int... expectedStatus)
+ throws ClientException {
+ HttpEntityEnclosingRequestBase request = new HttpPut(getUrl(requestPath));
+ if (entity != null) {
+ request.setEntity(entity);
+ }
+ return doRequest(request, headers, expectedStatus);
+ }
+
+ /**
+ * <p>Executes a PATCH request and consumes the entity in the response. The content is cached and be retrieved by calling
+ * {@code response.getContent()}</p>
+ *
+ * <p>Adds the passed entity and headers and checks the expected status</p>
+ *
+ * @param requestPath path relative to client url
+ * @param entity the entity to be added to request
+ * @param headers optional url parameters to be added
+ * @param expectedStatus if passed, the response status will have to match one of them
+ * @return the response with the entity consumed and the content cached
+ * @throws ClientException if the request could not be executed
+ */
+ public SlingHttpResponse doPatch(String requestPath, HttpEntity entity, List<Header> headers, int... expectedStatus)
+ throws ClientException {
+ HttpEntityEnclosingRequestBase request = new HttpPatch(getUrl(requestPath));
+ if (entity != null) {
+ request.setEntity(entity);
+ }
+ return doRequest(request, headers, expectedStatus);
+ }
+
+ /**
+ * <p>Executes a DELETE request and consumes the entity in the response. The content is cached and be retrieved by calling
+ * {@code response.getContent()}</p>
+ *
+ * <p>Adds the passed parameters and headers and checks the expected status</p>
+ *
+ * @param requestPath path relative to client url
+ * @param parameters optional url parameters to be added
+ * @param headers optional url parameters to be added
+ * @param expectedStatus if passed, the response status will have to match one of them
+ * @return the response with the entity consumed and the content cached
+ * @throws ClientException if the request could not be executed
+ */
+ public SlingHttpResponse doDelete(String requestPath, List<NameValuePair> parameters, List<Header> headers, int... expectedStatus)
+ throws ClientException {
+ HttpUriRequest request = new HttpDelete(getUrl(requestPath, parameters));
+ return doRequest(request, headers, expectedStatus);
+ }
+
+
+ //
+ // HttpClient base methods
+ //
+
+ @Deprecated
+ @SuppressWarnings("deprecation")
+ public org.apache.http.params.HttpParams getParams() {
+ return this.http.getParams();
+ }
+
+ @Deprecated
+ @SuppressWarnings("deprecation")
+ public org.apache.http.conn.ClientConnectionManager getConnectionManager() {
+ return this.http.getConnectionManager();
+ }
+
+ @SuppressWarnings("DuplicateThrows")
+ public HttpResponse execute(HttpUriRequest request) throws IOException, ClientProtocolException {
+ return this.http.execute(request);
+ }
+
+ // maybe throw UnsupportedMethodException
+ @SuppressWarnings("DuplicateThrows")
+ public CloseableHttpResponse execute(HttpUriRequest request, HttpContext context)
+ throws IOException, ClientProtocolException {
+ return this.http.execute(request, context);
+ }
+
+ @SuppressWarnings("DuplicateThrows")
+ public HttpResponse execute(HttpHost target, HttpRequest request)
+ throws IOException, ClientProtocolException {
+ return this.http.execute(target, request);
+ }
+
+ @SuppressWarnings("DuplicateThrows")
+ public CloseableHttpResponse execute(HttpHost target, HttpRequest request, HttpContext context)
+ throws IOException, ClientProtocolException {
+ return this.http.execute(target, request, context);
+ }
+
+ @SuppressWarnings("DuplicateThrows")
+ public <T> T execute(HttpUriRequest request, ResponseHandler<? extends T> responseHandler)
+ throws IOException, ClientProtocolException {
+ return this.http.execute(request, responseHandler);
+ }
+
+ @SuppressWarnings("DuplicateThrows")
+ public <T> T execute(HttpUriRequest request, ResponseHandler<? extends T> responseHandler, HttpContext context)
+ throws IOException, ClientProtocolException {
+ return this.http.execute(request, responseHandler, context);
+ }
+
+ @SuppressWarnings("DuplicateThrows")
+ public <T> T execute(HttpHost target, HttpRequest request, ResponseHandler<? extends T> responseHandler)
+ throws IOException, ClientProtocolException {
+ return this.http.execute(target, request, responseHandler);
+ }
+
+ @SuppressWarnings("DuplicateThrows")
+ public <T> T execute(HttpHost target, HttpRequest request, ResponseHandler<? extends T> responseHandler, HttpContext context)
+ throws IOException, ClientProtocolException {
+ return this.http.execute(target, request, responseHandler, context);
+ }
+}
diff --git a/src/main/java/org/apache/sling/testing/clients/ClientException.java b/src/main/java/org/apache/sling/testing/clients/ClientException.java
new file mode 100644
index 0000000..a598564
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/ClientException.java
@@ -0,0 +1,61 @@
+/*
+ * 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;
+
+/**
+ *
+ */
+public class ClientException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+ private int httpStatusCode = -1;
+
+ public ClientException(String message) {
+ this(message, null);
+ }
+
+ public ClientException(String message, Throwable throwable) {
+ this(message, -1, throwable);
+ }
+
+ public ClientException(String message, int htmlStatusCode, Throwable throwable) {
+ super(message, throwable);
+ this.httpStatusCode = htmlStatusCode;
+ }
+
+ /**
+ * @return the htmlStatusCode
+ */
+ public int getHttpStatusCode() {
+ return httpStatusCode;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.lang.Throwable#getMessage()
+ */
+ @Override
+ public String getMessage() {
+ String message = super.getMessage();
+ if (httpStatusCode > -1) {
+ message = message + "(return code=" + httpStatusCode + ")";
+ }
+ return message;
+ }
+
+}
diff --git a/src/main/java/org/apache/sling/testing/clients/Constants.java b/src/main/java/org/apache/sling/testing/clients/Constants.java
new file mode 100644
index 0000000..3e7ce66
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/Constants.java
@@ -0,0 +1,55 @@
+/*
+ * 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;
+
+public class Constants {
+
+ /**
+ * Prefix for IT-specific system properties
+ */
+ public static final String CONFIG_PROP_PREFIX = "sling.it.";
+ public static final String DEFAULT_URL = "http://localhost:8080/";
+ public static final String DEFAULT_USERNAME = "admin";
+ public static final String DEFAULT_PASSWORD = "admin";
+
+ // Custom delay for requests
+ private static long delay;
+ static {
+ try {
+ Constants.delay = Long.getLong(Constants.CONFIG_PROP_PREFIX + "http.delay", 0);
+ } catch (NumberFormatException e) {
+ Constants.delay = 0;
+ }
+ }
+
+ /**
+ * Custom delay in milliseconds before an HTTP request goes through.
+ * Used by {@link org.apache.sling.testing.clients.interceptors.DelayRequestInterceptor}
+ */
+ public static final long HTTP_DELAY = delay;
+
+ /**
+ * Handle to OSGI console
+ */
+ public static final String OSGI_CONSOLE = "/system/console";
+
+ /**
+ * General parameters and values
+ */
+ public static final String PARAMETER_CHARSET = "_charset_";
+ public static final String CHARSET_UTF8 = "utf-8";
+}
diff --git a/src/main/java/org/apache/sling/testing/clients/SlingClient.java b/src/main/java/org/apache/sling/testing/clients/SlingClient.java
new file mode 100644
index 0000000..7d9bbe3
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/SlingClient.java
@@ -0,0 +1,658 @@
+/*
+ * 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;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpRequestInterceptor;
+import org.apache.http.HttpResponseInterceptor;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.CookieStore;
+import org.apache.http.client.CredentialsProvider;
+import org.apache.http.client.RedirectStrategy;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.mime.MultipartEntityBuilder;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+
+import org.apache.sling.testing.clients.interceptors.DelayRequestInterceptor;
+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.AbstractPoller;
+import org.codehaus.jackson.JsonNode;
+
+import java.io.File;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.apache.http.HttpStatus.SC_CREATED;
+import static org.apache.http.HttpStatus.SC_OK;
+
+/**
+ * <p>The Base class for all Integration Test Clients. It provides generic methods to send HTTP requests to a server. </p>
+ *
+ * <p>It has methods to perform simple node operations on the server like creating and deleting nodes, etc.
+ * on the server using requests. </p>
+ */
+public class SlingClient extends AbstractSlingClient {
+
+ public static final String DEFAULT_NODE_TYPE = "sling:OrderedFolder";
+
+ /**
+ * Constructor used by Builders and adaptTo(). <b>Should never be called directly from the code.</b>
+ *
+ * @param http the underlying HttpClient to be used
+ * @param config sling specific configs
+ * @throws ClientException if the client could not be created
+ *
+ * @see AbstractSlingClient#AbstractSlingClient(CloseableHttpClient, SlingClientConfig)
+ */
+ public SlingClient(CloseableHttpClient http, SlingClientConfig config) throws ClientException {
+ super(http, config);
+ }
+
+ /**
+ * <p>Handy constructor easy to use in simple tests. Creates a client that uses basic authentication.</p>
+ *
+ * <p>For constructing clients with complex configurations, use a {@link InternalBuilder}</p>
+ *
+ * <p>For constructing clients with the same configuration, but a different class, use {@link #adaptTo(Class)}</p>
+ *
+ * @param url url of the server (including context path)
+ * @param user username for basic authentication
+ * @param password password for basic authentication
+ * @throws ClientException never, kept for uniformity with the other constructors
+ */
+ public SlingClient(URI url, String user, String password) throws ClientException {
+ super(Builder.create(url, user, password).buildHttpClient(), Builder.create(url, user, password).buildSlingClientConfig());
+ }
+
+ /**
+ * Moves a sling path to a new location (:operation move)
+ *
+ * @param srcPath source path
+ * @param destPath destination path
+ * @param expectedStatus list of accepted status codes in response
+ * @return the response
+ * @throws ClientException if an error occurs during operation
+ */
+ public SlingHttpResponse move(String srcPath, String destPath, int... expectedStatus) throws ClientException {
+ UrlEncodedFormEntity entity = FormEntityBuilder.create()
+ .addParameter(":operation", "move")
+ .addParameter(":dest", destPath)
+ .build();
+
+ return this.doPost(srcPath, entity, expectedStatus);
+ }
+
+ /**
+ * Deletes a sling path (:operation delete)
+ *
+ * @param path path to be deleted
+ * @param expectedStatus list of accepted status codes in response
+ * @return the response
+ * @throws ClientException if an error occurs during operation
+ */
+ public SlingHttpResponse deletePath(String path, int... expectedStatus) throws ClientException {
+ HttpEntity entity = FormEntityBuilder.create().addParameter(":operation", "delete").build();
+
+ return this.doPost(path, entity, expectedStatus);
+ }
+
+ /**
+ * Recursively creates all the none existing nodes in the given path using the {@link SlingClient#createNode(String, String)} method.
+ * All the created nodes will have the given node type.
+ *
+ * @param path the path to use for creating all the none existing nodes
+ * @param nodeType the node type to use for the created nodes
+ * @return the response to the creation of the leaf node
+ * @throws ClientException if one of the nodes can't be created
+ */
+ public SlingHttpResponse createNodeRecursive(final String path, final String nodeType) throws ClientException {
+ final String parentPath = getParentPath(path);
+ if (!parentPath.isEmpty() && !exists(parentPath)) {
+ createNodeRecursive(parentPath, nodeType);
+ }
+
+ return createNode(path, nodeType);
+ }
+
+ /**
+ * Creates the node specified by a given path with the given node type.<br>
+ * If the given node type is {@code null}, the node will be created with the default type: {@value DEFAULT_NODE_TYPE}.<br>
+ * If the node already exists, the method will return null, with no errors.<br>
+ * The method ignores trailing slashes so a path like this <i>/a/b/c///</i> is accepted and will create the <i>c</i> node if the rest of
+ * the path exists.
+ *
+ * @param path the path to the node to create
+ * @param nodeType the type of the node to create
+ * @return the sling HTTP response or null if the path already existed
+ * @throws ClientException if the node can't be created
+ */
+ public SlingHttpResponse createNode(final String path, final String nodeType) throws ClientException {
+ if (!exists(path)) {
+
+ String nodeTypeValue = nodeType;
+ if (nodeTypeValue == null) {
+ nodeTypeValue = DEFAULT_NODE_TYPE;
+ }
+
+ // Use the property for creating the actual node for working around the Sling issue with dot containing node names.
+ // The request will be similar with doing:
+ // curl -F "nodeName/jcr:primaryType=nodeTypeValue" -u admin:admin http://localhost:8080/nodeParentPath
+ final String nodeName = getNodeNameFromPath(path);
+ final String nodeParentPath = getParentPath(path);
+ final HttpEntity entity = FormEntityBuilder.create().addParameter(nodeName + "/jcr:primaryType", nodeTypeValue).build();
+ return this.doPost(nodeParentPath, entity, SC_OK, SC_CREATED);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * <p>Checks whether a path exists or not by making a GET request to that path with the {@code json} extension</p>
+ * @param path path to be checked
+ * @return true if GET response returns 200
+ * @throws ClientException if the request could not be performed
+ */
+ public boolean exists(String path) throws ClientException {
+ SlingHttpResponse response = this.doGet(path + ".json");
+ final int status = response.getStatusLine().getStatusCode();
+ return status == SC_OK;
+ }
+
+ /**
+ * Extracts the parent path from the given String
+ *
+ * @param path string containing the path
+ * @return the parent path if exists or empty string otherwise
+ */
+ protected String getParentPath(final String path) {
+ // TODO define more precisely what is the parent of a folder and of a file
+ final String normalizedPath = StringUtils.removeEnd(path, "/"); // remove trailing slash in case of folders
+ return StringUtils.substringBeforeLast(normalizedPath, "/");
+ }
+
+ /**
+ * Extracts the node from path
+ *
+ * @param path string containing the path
+ * @return the node without parent path
+ */
+ protected String getNodeNameFromPath(final String path) {
+ // TODO define the output for all the cases (e.g. paths with trailing slash)
+ final String normalizedPath = StringUtils.removeEnd(path, "/"); // remove trailing slash in case of folders
+ final int pos = normalizedPath.lastIndexOf('/');
+ if (pos != -1) {
+ return normalizedPath.substring(pos + 1, normalizedPath.length());
+ }
+ return normalizedPath;
+ }
+
+ /**
+ * <p>Checks whether a path exists or not by making a GET request to that path with the {@code json extension} </p>
+ * <p>It polls the server and waits until the path exists </p>
+ * @param path path to be checked
+ * @param waitMillis time to wait between retries
+ * @param retryCount number of retries before throwing an exception
+ * @throws ClientException if the path was not found
+ * @throws InterruptedException to mark this operation as "waiting"
+ */
+ public void waitUntilExists(final String path, final long waitMillis, int retryCount)
+ throws ClientException, InterruptedException {
+ AbstractPoller poller = new AbstractPoller(waitMillis, retryCount) {
+ boolean found = false;
+ public boolean call() {
+ try {
+ found = exists(path);
+ } catch (ClientException e) {
+ // maybe log
+ found = false;
+ }
+ return true;
+ }
+
+ public boolean condition() {
+ return found;
+ }
+ };
+
+ boolean found = poller.callUntilCondition();
+ if (!found) {
+ throw new ClientException("path " + path + " does not exist after " + retryCount + " retries");
+ }
+ }
+
+ /**
+ * Sets String component property on a node.
+ *
+ * @param nodePath path to the node to be edited
+ * @param propName name of the property to be edited
+ * @param propValue value of the property to be edited
+ * @param expectedStatus list of expected HTTP Status to be returned, if not set, 200 is assumed.
+ * @return the response object
+ * @throws ClientException if something fails during the request/response cycle
+ */
+ public SlingHttpResponse setPropertyString(String nodePath, String propName, String propValue, int... expectedStatus)
+ throws ClientException {
+ // prepare the form
+ HttpEntity formEntry = FormEntityBuilder.create().addParameter(propName, propValue).build();
+ // send the request
+ return this.doPost(nodePath, formEntry, HttpUtils.getExpectedStatus(SC_OK, expectedStatus));
+ }
+
+ /**
+ * Sets a String[] component property on a node.
+ *
+ * @param nodePath path to the node to be edited
+ * @param propName name of the property to be edited
+ * @param propValueList List of String values
+ * @param expectedStatus list of expected HTTP Status to be returned, if not set, 200 is assumed.
+ * @return the response
+ * @throws ClientException if something fails during the request/response cycle
+ */
+ public SlingHttpResponse setPropertyStringArray(String nodePath, String propName, List<String> propValueList, int... expectedStatus)
+ throws ClientException {
+ // prepare the form
+ FormEntityBuilder formEntry = FormEntityBuilder.create();
+ for (String propValue : (propValueList != null) ? propValueList : new ArrayList<String>(0)) {
+ formEntry.addParameter(propName, propValue);
+ }
+ // send the request and return the sling response
+ return this.doPost(nodePath, formEntry.build(), HttpUtils.getExpectedStatus(SC_OK, expectedStatus));
+ }
+
+ /**
+ * Sets multiple String properties on a node in a single request
+ * @param nodePath path to the node to be edited
+ * @param properties list of NameValue pairs with the name and value for each property. String[] properties can be defined
+ * by adding multiple time the same property name with different values
+ * @param expectedStatus list of expected HTTP Status to be returned, if not set, 200 is assumed.
+ * @return the response
+ * @throws ClientException if the operation could not be completed
+ */
+ public SlingHttpResponse setPropertiesString(String nodePath, List<NameValuePair> properties, int... expectedStatus)
+ throws ClientException {
+ // prepare the form
+ HttpEntity formEntry = FormEntityBuilder.create().addAllParameters(properties).build();
+ // send the request and return the sling response
+ return this.doPost(nodePath, formEntry, HttpUtils.getExpectedStatus(SC_OK, expectedStatus));
+ }
+
+ /**
+ * Returns the JSON content of a node already mapped to a {@link org.codehaus.jackson.JsonNode}.<br>
+ * Waits max 10 seconds for the node to be created.
+ *
+ * @param path the path to the content node
+ * @param depth the number of levels to go down the tree, -1 for infinity
+ * @return a {@link org.codehaus.jackson.JsonNode} mapping to the requested content node.
+ * @throws ClientException if something fails during request/response processing
+ * @throws InterruptedException to mark this operation as "waiting"
+ */
+ public JsonNode getJsonNode(String path, int depth) throws ClientException, InterruptedException {
+ return getJsonNode(path, depth, 500, 20);
+ }
+
+ /**
+ * Returns JSON format of a content node already mapped to a {@link org.codehaus.jackson.JsonNode}.
+ *
+ * @param path the path to the content node
+ * @param depth the number of levels to go down the tree, -1 for infinity
+ * @param waitMillis how long it should wait between requests
+ * @param retryNumber number of retries before throwing an exception
+ * @param expectedStatus list of allowed HTTP Status to be returned. If not set,
+ * http status 200 (OK) is assumed.
+ * @return a {@link org.codehaus.jackson.JsonNode} mapping to the requested content node.
+ * @throws ClientException if something fails during request/response cycle
+ * @throws InterruptedException to mark this operation as "waiting"
+ */
+ public JsonNode getJsonNode(String path, int depth, final long waitMillis, final int retryNumber, int... expectedStatus)
+ throws ClientException, InterruptedException {
+
+ // check if path exist and wait if needed
+ waitUntilExists(path, waitMillis, retryNumber);
+
+ // check for infinity
+ if (depth == -1) {
+ path += ".infinity.json";
+ } else {
+ path += "." + depth + ".json";
+ }
+
+ // request the JSON for the page node
+ SlingHttpResponse response = this.doGet(path);
+ HttpUtils.verifyHttpStatus(response, HttpUtils.getExpectedStatus(SC_OK, expectedStatus));
+
+ return JsonUtils.getJsonNodeFromString(response.getContent());
+ }
+
+ /**
+ * Uploads a file to the repository. It creates a leaf node typed {@code nt:file}. The intermediary nodes are created with
+ * type "sling:OrderedFolder" if parameter {@code createFolders} is true
+ *
+ * @param file the file to be uploaded
+ * @param mimeType the MIME Type of the file
+ * @param toPath the complete path of the file in the repository including file name
+ * @param createFolders if true, all non existing parent nodes will be created using node type {@code sling:OrderedFolder}
+ * @param expectedStatus list of expected HTTP Status to be returned, if not set, 201 is assumed.
+ * @return the response
+ * @throws ClientException if something fails during the request/response cycle
+ */
+ public SlingHttpResponse upload(File file, String mimeType, String toPath, boolean createFolders, int... expectedStatus)
+ throws ClientException {
+ // Determine filename and parent folder, depending on whether toPath is a folder or a file
+ String toFileName;
+ String toFolder;
+ if (toPath.endsWith("/")) {
+ toFileName = file.getName();
+ toFolder = toPath;
+ } else {
+ toFileName = getNodeNameFromPath(toPath);
+ toFolder = getParentPath(toPath);
+ }
+
+ if (createFolders) {
+ createNodeRecursive(toFolder, "sling:OrderedFolder");
+ }
+
+ if (mimeType == null) {
+ mimeType = "application/octet-stream";
+ }
+
+ HttpEntity entity = MultipartEntityBuilder.create()
+ .addBinaryBody(toFileName, file, ContentType.create(mimeType), toFileName)
+ .build();
+
+ // return the sling response
+ return this.doPost(toFolder, entity, HttpUtils.getExpectedStatus(SC_CREATED, expectedStatus));
+ }
+
+ /**
+ * Creates a new Folder of type sling:OrderedFolder. Same as using {@code New Folder...} in the Site Admin.
+ *
+ * @param folderName The name of the folder to be used in the URL.
+ * @param folderTitle Title of the Folder to be set in jcr:title
+ * @param parentPath The parent path where the folder gets added.
+ * @param expectedStatus list of expected HTTP Status to be returned, if not set, 201 is assumed.
+ * @return the response
+ * @throws ClientException if something fails during the request/response cycle
+ */
+ public SlingHttpResponse createFolder(String folderName, String folderTitle, String parentPath, int... expectedStatus)
+ throws ClientException {
+ // we assume the parentPath is a folder, even though it doesn't end with a slash
+ parentPath = StringUtils.appendIfMissing(parentPath, "/");
+ String folderPath = parentPath + folderName;
+ HttpEntity feb = FormEntityBuilder.create()
+ .addParameter("./jcr:primaryType", "sling:OrderedFolder") // set primary type for folder node
+ .addParameter("./jcr:content/jcr:primaryType", "nt:unstructured") // add jcr:content as sub node
+ .addParameter("./jcr:content/jcr:title", folderTitle) //set the title
+ .build();
+
+ // execute request and return the sling response
+ return this.doPost(folderPath, feb, HttpUtils.getExpectedStatus(SC_CREATED, expectedStatus));
+ }
+
+ /**
+ * Get uuid from any repository path
+ *
+ * @param repPath path in repository
+ * @return uuid as String
+ * @throws ClientException if something fails during request/response cycle
+ * @throws InterruptedException to mark this operation as "waiting"
+ */
+ public String getUUID(String repPath) throws ClientException, InterruptedException {
+ // TODO review if this check is necessary. Maybe rewrite getJsonNode to wait only if requested
+ if (!exists(repPath)) {
+ return null;
+ }
+ JsonNode jsonNode = getJsonNode(repPath, -1);
+ return getUUId(jsonNode);
+ }
+
+ /**
+ * Get uuid from any repository path
+ *
+ * @param jsonNode {@link JsonNode} in repository
+ * @return uuid as String or null if jsonNode is null or if the uuid was not found
+ * @throws ClientException if something fails during request/response cycle
+ */
+ public String getUUId(JsonNode jsonNode) throws ClientException {
+ // TODO review if this check is necessary. Maybe rewrite getJsonNode to wait only if requested
+ if (jsonNode == null) {
+ return null; // node does not exist
+ }
+
+ JsonNode uuidNode = jsonNode.get("jcr:uuid");
+
+ if (uuidNode == null) {
+ return null;
+ }
+
+ return uuidNode.getValueAsText();
+ }
+
+ //
+ // InternalBuilder class and builder related methods
+ //
+
+ /**
+ * <p>Extensible InternalBuilder for SlingClient. Can be used by calling: {@code SlingClient.builder().create(...).build()}.
+ * Between create() and build(), any number of <i>set</i> methods can be called to customize the client.<br>
+ * It also exposes the underling httpClientBuilder through {@link #httpClientBuilder()} which can be used to customize the client
+ * at http level.
+ * </p>
+ *
+ * <p>The InternalBuilder is created to be easily extensible. A class, e.g. {@code MyClient extends SlingClient}, can have its own InternalBuilder.
+ * This is worth creating if MyClient has fields that need to be initialized. The Skeleton of such InternalBuilder (created inside MyClient) is:
+ * </p>
+ * <blockquote><pre>
+ * {@code
+ * public static abstract class InternalBuilder<T extends MyClient> extends SlingClient.InternalBuilder<T> {
+ * private String additionalField;
+ *
+ * public InternalBuilder(URI url, String user, String password) { super(url, user, password); }
+ *
+ * public InternalBuilder<T> setAdditionalField(String s) { additionalField = s; }
+ * }
+ * }
+ * </pre></blockquote>
+ * <p>Besides this, two more methods need to be implemented directly inside {@code MyClient}: </p>
+ * <blockquote><pre>
+ * {@code
+ * public static InternalBuilder<?> builder(URI url, String user, String password) {
+ * return new InternalBuilder<MyClient>(url, user, password) {
+ * {@literal @}Override
+ * public MyClient build() throws ClientException { return new MyClient(this); }
+ * };
+ * }
+ *
+ * protected MyClient(InternalBuilder<MyClient> builder) throws ClientException {
+ * super(builder);
+ * additionalField = builder.additionalField;
+ * }
+ * }
+ * </pre></blockquote>
+ * Of course, the Clients and InternalBuilder are extensible on several levels, so MyClient.InternalBuilder can be further extended.
+ *
+ * @param <T> type extending SlingClient
+ */
+ public static abstract class InternalBuilder<T extends SlingClient> {
+
+ private final SlingClientConfig.Builder configBuilder;
+
+ private final HttpClientBuilder httpClientBuilder;
+
+ protected InternalBuilder(URI url, String user, String password) {
+ this.httpClientBuilder = HttpClientBuilder.create();
+ this.configBuilder = SlingClientConfig.Builder.create().setUrl(url).setUser(user).setPassword(password);
+
+ setDefaults();
+ }
+
+ public InternalBuilder<T> setUrl(URI url) {
+ this.configBuilder.setUrl(url);
+ return this;
+ }
+
+ public InternalBuilder<T> setUser(String user) {
+ this.configBuilder.setUser(user);
+ return this;
+ }
+
+ public InternalBuilder<T> setPassword(String password) {
+ this.configBuilder.setPassword(password);
+ return this;
+ }
+
+ public InternalBuilder<T> setCredentialsProvider(CredentialsProvider cp) {
+ this.configBuilder.setCredentialsProvider(cp);
+ return this;
+ }
+
+ public InternalBuilder<T> setCookieStore(CookieStore cs) {
+ this.configBuilder.setCookieStore(cs);
+ return this;
+ }
+
+ public HttpClientBuilder httpClientBuilder() {
+ return httpClientBuilder;
+ }
+
+ public abstract T build() throws ClientException;
+
+ protected CloseableHttpClient buildHttpClient() {
+ return httpClientBuilder.build();
+ }
+
+ protected SlingClientConfig buildSlingClientConfig() {
+ return configBuilder.build();
+ }
+
+ /**
+ * Sets defaults to the builder.
+ *
+ * @return this
+ */
+ private InternalBuilder setDefaults() {
+ httpClientBuilder.useSystemProperties();
+ httpClientBuilder.setUserAgent("Java");
+ // Connection
+ httpClientBuilder.setMaxConnPerRoute(10);
+ httpClientBuilder.setMaxConnTotal(100);
+ // Interceptors
+ httpClientBuilder.addInterceptorLast(new DelayRequestInterceptor(Constants.HTTP_DELAY));
+
+ return this;
+ }
+
+ //
+ // HttpClientBuilder delegating methods
+ //
+
+ public final InternalBuilder<T> addInterceptorFirst(final HttpResponseInterceptor itcp) {
+ httpClientBuilder.addInterceptorFirst(itcp);
+ return this;
+ }
+
+ /**
+ * Adds this protocol interceptor to the tail of the protocol processing list.
+ * <p>
+ * Please note this value can be overridden by the {@link HttpClientBuilder#setHttpProcessor(
+ * org.apache.http.protocol.HttpProcessor)} method.
+ * </p>
+ *
+ * @param itcp the interceptor
+ * @return this
+ */
+ public final InternalBuilder<T> addInterceptorLast(final HttpResponseInterceptor itcp) {
+ httpClientBuilder.addInterceptorLast(itcp);
+ return this;
+ }
+
+ /**
+ * Adds this protocol interceptor to the head of the protocol processing list.
+ * <p>
+ * Please note this value can be overridden by the {@link HttpClientBuilder#setHttpProcessor(
+ * org.apache.http.protocol.HttpProcessor)} method.
+ * </p>
+ *
+ * @param itcp the interceptor
+ * @return this
+ */
+ public final InternalBuilder<T> addInterceptorFirst(final HttpRequestInterceptor itcp) {
+ httpClientBuilder.addInterceptorFirst(itcp);
+ return this;
+ }
+
+ /**
+ * Adds this protocol interceptor to the tail of the protocol processing list.
+ * <p>
+ * Please note this value can be overridden by the {@link HttpClientBuilder#setHttpProcessor(
+ * org.apache.http.protocol.HttpProcessor)} method.
+ * </p>
+ *
+ * @param itcp the interceptor
+ * @return this
+ */
+ public final InternalBuilder<T> addInterceptorLast(final HttpRequestInterceptor itcp) {
+ httpClientBuilder.addInterceptorLast(itcp);
+ return this;
+ }
+
+ /**
+ * Assigns {@link RedirectStrategy} instance.
+ * <p>Please note this value can be overridden by the {@link #disableRedirectHandling()} method.</p>
+ *
+ * @param redirectStrategy custom redirect strategy
+ * @return this
+ */
+ public final InternalBuilder<T> setRedirectStrategy(final RedirectStrategy redirectStrategy) {
+ httpClientBuilder.setRedirectStrategy(redirectStrategy);
+ return this;
+ }
+
+ /**
+ * Disables automatic redirect handling.
+ *
+ * @return this
+ */
+ public final InternalBuilder<T> disableRedirectHandling() {
+ httpClientBuilder.disableRedirectHandling();
+ return this;
+ }
+
+ }
+
+ public final static class Builder extends InternalBuilder<SlingClient> {
+
+ private Builder(URI url, String user, String password) {
+ super(url, user, password);
+ }
+
+ @Override
+ public SlingClient build() throws ClientException {
+ return new SlingClient(buildHttpClient(), buildSlingClientConfig());
+ }
+
+ public static Builder create(URI url, String user, String password) {
+ return new Builder(url, user, password);
+ }
+ }
+}
diff --git a/src/main/java/org/apache/sling/testing/clients/SlingClientConfig.java b/src/main/java/org/apache/sling/testing/clients/SlingClientConfig.java
new file mode 100644
index 0000000..58d380c
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/SlingClientConfig.java
@@ -0,0 +1,227 @@
+/*
+ * 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;
+
+import com.google.common.base.Strings;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.http.HttpHost;
+import org.apache.http.annotation.ThreadSafe;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.AuthCache;
+import org.apache.http.client.CookieStore;
+import org.apache.http.client.CredentialsProvider;
+import org.apache.http.client.utils.URIUtils;
+import org.apache.http.impl.auth.BasicScheme;
+import org.apache.http.impl.client.BasicAuthCache;
+import org.apache.http.impl.client.BasicCookieStore;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+@ThreadSafe
+public class SlingClientConfig {
+
+ /**
+ * Base URI of the server under test.
+ */
+ protected final URI url;
+
+ /**
+ * Name of the user that will be used to authenticate the requests.
+ */
+ protected final String user;
+
+ /**
+ * Password of the user that will be used to authenticate the requests.
+ */
+ protected final String password;
+
+ /**
+ * The cookie store
+ */
+ protected final CookieStore cookieStore;
+
+ /**
+ * The credentials provider
+ */
+ protected final CredentialsProvider credsProvider;
+
+ /**
+ * AuthCache for preemptive auth
+ */
+ protected final AuthCache authCache;
+
+ /**
+ * Extra values to be used in interceptors, custom auth mechanisms, etc.
+ */
+ protected final Map<String, String> values;
+
+ protected SlingClientConfig(URI url, String user, String password,
+ CookieStore cookieStore,
+ CredentialsProvider credentialsProvider, AuthCache authCache) {
+ this.url = url;
+ this.user = user;
+ this.password = password;
+
+ this.cookieStore = cookieStore;
+ this.credsProvider = credentialsProvider;
+ this.authCache = authCache;
+
+ this.values = new ConcurrentHashMap<String, String>();
+ }
+
+ /**
+ * @return the base URL that the sling client is pointing to. It should always end with a "/"
+ */
+ public URI getUrl() {
+ return url;
+ }
+
+ /**
+ * @return the user that the client is using.
+ */
+ public String getUser() {
+ return user;
+ }
+
+ /**
+ * @return the user that the client is using.
+ */
+ public String getPassword() {
+ return password;
+ }
+
+ /**
+ * <p>Get the map of extra custom values configured on the client</p>
+ * <p>These may be used by interceptors, for example</p>
+ *
+ * @return the reference to the map
+ */
+ public Map<String, String> getValues() {
+ return values;
+ }
+
+ /**
+ * @return a reference to the cookie store used by the client
+ */
+ public CookieStore getCookieStore() {
+ return cookieStore;
+ }
+
+ /**
+ * @return the reference to the CredentialsProvider used by the client
+ */
+ public CredentialsProvider getCredsProvider() {
+ return credsProvider;
+ }
+
+ /**
+ * @return the reference the AuthCache used by the client
+ */
+ public AuthCache getAuthCache() {
+ return authCache;
+ }
+
+ public static class Builder {
+ protected URI url;
+
+ protected String user;
+
+ protected String password;
+
+ protected CookieStore cookieStore;
+
+ protected CredentialsProvider credsProvider;
+
+ protected AuthCache authCache;
+
+ protected Builder() {
+ }
+
+ public static Builder create() {
+ return new Builder();
+ }
+
+ public Builder setUrl(String url) throws URISyntaxException {
+ return setUrl(new URI(url));
+ }
+
+ public Builder setUrl(URI url) {
+ this.url = url;
+ // Add / as path if none is present
+ if (Strings.isNullOrEmpty(this.url.getPath()) || !this.url.getPath().endsWith("/")) {
+ this.url = this.url.resolve(Strings.nullToEmpty(this.url.getPath()) + "/");
+ }
+ return this;
+ }
+
+ public Builder setUser(String user) {
+ this.user = user;
+ return this;
+ }
+
+ public Builder setPassword(String password) {
+ this.password = password;
+ return this;
+ }
+
+ public Builder setCredentialsProvider(CredentialsProvider credsProvider) {
+ this.credsProvider = credsProvider;
+ return this;
+ }
+
+ public Builder setAuthCache(AuthCache authCache) {
+ this.authCache = authCache;
+ return this;
+ }
+
+ public Builder setCookieStore(CookieStore cookieStore) {
+ this.cookieStore = cookieStore;
+ return this;
+ }
+
+ public SlingClientConfig build() {
+ // Create default CredentialsProvider if not set
+ if (credsProvider == null) {
+ credsProvider = new BasicCredentialsProvider();
+ if (StringUtils.isNotEmpty(this.user)) {
+ HttpHost targetHost = URIUtils.extractHost(this.url);
+ credsProvider.setCredentials(new AuthScope(targetHost.getHostName(), targetHost.getPort()),
+ new UsernamePasswordCredentials(this.user, this.password));
+ }
+ }
+
+ // Create default AuthCache if not set
+ if (authCache == null) {
+ BasicScheme basicScheme = new BasicScheme();
+ authCache = new BasicAuthCache();
+ authCache.put(URIUtils.extractHost(url), basicScheme);
+ }
+
+ // Create default CookieStore if not set
+ if (cookieStore == null) {
+ cookieStore = new BasicCookieStore();
+ }
+
+ return new SlingClientConfig(url, user, password, cookieStore, credsProvider, authCache);
+ }
+ }
+}
diff --git a/src/main/java/org/apache/sling/testing/clients/SlingHttpResponse.java b/src/main/java/org/apache/sling/testing/clients/SlingHttpResponse.java
new file mode 100644
index 0000000..9a54df6
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/SlingHttpResponse.java
@@ -0,0 +1,396 @@
+/*
+ * 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;
+
+import org.apache.http.*;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.params.HttpParams;
+import org.apache.http.util.EntityUtils;
+
+import java.io.IOException;
+import java.util.*;
+import java.util.regex.Pattern;
+
+public class SlingHttpResponse implements CloseableHttpResponse {
+
+ public static final String STATUS = "Status";
+ public static final String MESSAGE = "Message";
+ public static final String LOCATION = "Location";
+ public static final String PARENT_LOCATION = "ParentLocation";
+ public static final String PATH = "Path";
+ public static final String REFERER = "Referer";
+ public static final String CHANGE_LOG = "ChangeLog";
+
+ private final CloseableHttpResponse httpResponse;
+ private String content;
+
+ public SlingHttpResponse(CloseableHttpResponse response) {
+ this.httpResponse = response;
+ }
+
+ /**
+ * <p>Get the {@code String} content of the response.</p>
+ * <p>The content is cached so it is safe to call this method several times.</p>
+ * <p><b>Attention!</b> Calling this method consumes the entity, so it cannot be used as an InputStream later</p>
+ *
+ * @return the content as String
+ */
+ public String getContent() {
+ if (!this.isConsumed()) {
+ try {
+ this.content = EntityUtils.toString(this.getEntity());
+ this.close();
+ } catch (IOException e) {
+ throw new RuntimeException("Could not read content from response", e);
+ }
+ }
+
+ return content;
+ }
+
+ public boolean isConsumed() {
+ return this.content != null || this.getEntity() == null;
+ }
+
+ /**
+ * <p>Assert that response matches supplied status</p>
+ *
+ * @param expected the expected http status
+ * @throws AssertionError if the response does not match the expected
+ */
+ public void checkStatus(int expected) throws ClientException {
+ if (this.getStatusLine().getStatusCode() != expected) {
+ throw new ClientException(this + " has wrong response status ("
+ + this.getStatusLine().getStatusCode() + "). Expected " + expected);
+ }
+ }
+
+ /**
+ * <p>Assert that response matches supplied content type (from Content-Type header)</p>
+ *
+ * @param expected the expected content type
+ * @throws AssertionError if the response content type does not match the expected
+ */
+ public void checkContentType(String expected) throws ClientException {
+ // Remove whatever follows semicolon in content-type
+ String contentType = this.getEntity().getContentType().getValue();
+ if (contentType != null) {
+ contentType = contentType.split(";")[0].trim();
+ }
+
+ // check for match
+ if (!contentType.equals(expected)) {
+ throw new ClientException(this + " has wrong content type (" + contentType + "). Expected " + expected);
+ }
+ }
+
+ /**
+ * <p>For each regular expression, assert that at least one line of the response matches the expression</p>
+ * <p>The regular expressions are automatically prefixed and suffixed with .* it order to partial-match the lines</p>
+ *
+ * @param regexp list of regular expressions
+ * @throws AssertionError if the response content does not match one of the regexp
+ */
+ public void checkContentRegexp(String... regexp) throws ClientException {
+ for(String expr : regexp) {
+ final Pattern p = Pattern.compile(".*" + expr + ".*");
+ final Scanner scanner = new Scanner(this.getContent());
+ boolean matched = false;
+ while (scanner.hasNextLine()) {
+ String line = scanner.nextLine();
+ if (p.matcher(line).matches()) {
+ matched = true;
+ break;
+ }
+ }
+
+ if (!matched) {
+ throw new ClientException("Pattern " + p + " didn't match any line in content");
+ }
+ }
+ }
+
+ /**
+ * <p>Assert that all the provided {@code Strings} are contained in the response</p>
+ *
+ * @param expected list of expected strings
+ */
+ public void checkContentContains(String... expected) throws ClientException {
+ for (String s : expected) {
+ if (!this.getContent().contains(s)) {
+ throw new ClientException("Content does not contain string " + s + ". Content is: \n\n" + getContent());
+ }
+ }
+ }
+
+ /**
+ * Get status from Sling Response
+ *
+ * @return Sling Status
+ */
+ public String getSlingStatus() {
+ String searchPattern = "id=\"" + STATUS + "\">";
+ return extractFromHTMLResponse(searchPattern);
+ }
+
+ /**
+ * Get status from Sling Response as integer
+ *
+ * @return Sling Status
+ */
+ public int getSlingStatusAsInt() throws NumberFormatException {
+ String strStatus = getSlingStatus();
+ return Integer.parseInt(strStatus);
+ }
+
+ /**
+ * Get message from Sling Response
+ *
+ * @return Sling Message
+ */
+ public String getSlingMessage() {
+ String searchPattern = "id=\"" + MESSAGE + "\">";
+ return extractFromHTMLResponse(searchPattern);
+ }
+
+ /**
+ * Get copy paths from message
+ *
+ * @return copy paths as String Array
+ */
+ public String[] getSlingCopyPaths() {
+ String copyPaths = getSlingMessage();
+ StringTokenizer tokenizer = new StringTokenizer(copyPaths);
+ List<String> copies = new ArrayList<String>();
+ while (tokenizer.hasMoreElements()) {
+ copies.add(tokenizer.nextToken());
+ }
+ return copies.toArray(new String[copies.size()]);
+ }
+
+ /**
+ * Get location from Sling Response
+ *
+ * @return Sling Location
+ */
+ public String getSlingLocation() {
+ String searchPattern = "id=\"" + LOCATION + "\">";
+ return extractFromHTMLResponse(searchPattern);
+ }
+
+ /**
+ * Get parent location from Sling Response
+ *
+ * @return Sling Parent Location
+ */
+ public String getSlingParentLocation() {
+ String searchPattern = "id=\"" + PARENT_LOCATION + "\">";
+ return extractFromHTMLResponse(searchPattern);
+ }
+
+ /**
+ * Get path from Sling Response
+ *
+ * @return Sling Path
+ */
+ public String getSlingPath() {
+ String searchPattern = "id=\"" + PATH + "\">";
+ return extractFromHTMLResponse(searchPattern);
+ }
+
+ /**
+ * Get referer from Sling Response
+ *
+ * @return Sling Referer
+ */
+ public String getSlingReferer() {
+ String searchPattern = "id=\"" + REFERER + "\">";
+ return extractFromHTMLResponse(searchPattern);
+ }
+
+ /**
+ * Get change log from Sling Response
+ *
+ * @return Sling Change Log
+ */
+ public String getSlingChangeLog() {
+ String searchPattern = "id=\"" + CHANGE_LOG + "\">";
+ return extractFromHTMLResponse(searchPattern);
+ }
+
+ /**
+ * Extract information from response
+ *
+ * @param searchPattern search pattern to look for
+ * @return Sling information
+ */
+ protected String extractFromHTMLResponse(String searchPattern) {
+ String tmpResponse = null;
+ int start = getContent().indexOf(searchPattern);
+ if (start > 0) {
+ start += searchPattern.length();
+ tmpResponse = getContent().substring(start);
+ int end = tmpResponse.indexOf("<");
+ tmpResponse = tmpResponse.substring(0, end);
+ }
+ return tmpResponse;
+ }
+
+ // HttpResponse delegated methods
+
+ @Override
+ public StatusLine getStatusLine() {
+ return httpResponse.getStatusLine();
+ }
+
+ @Override
+ public void setStatusLine(StatusLine statusline) {
+ httpResponse.setStatusLine(statusline);
+ }
+
+ @Override
+ public void setStatusLine(ProtocolVersion ver, int code) {
+ httpResponse.setStatusLine(ver, code);
+ }
+
+ @Override
+ public void setStatusLine(ProtocolVersion ver, int code, String reason) {
+ httpResponse.setStatusLine(ver, code, reason);
+ }
+
+ @Override
+ public void setStatusCode(int code) throws IllegalStateException {
+ httpResponse.setStatusCode(code);
+ }
+
+ @Override
+ public void setReasonPhrase(String reason) throws IllegalStateException {
+ httpResponse.setReasonPhrase(reason);
+ }
+
+ @Override
+ public HttpEntity getEntity() {
+ return httpResponse.getEntity();
+ }
+
+ @Override
+ public void setEntity(HttpEntity entity) {
+ httpResponse.setEntity(entity);
+ }
+
+ @Override
+ public Locale getLocale() {
+ return httpResponse.getLocale();
+ }
+
+ @Override
+ public void setLocale(Locale loc) {
+ httpResponse.setLocale(loc);
+ }
+
+ @Override
+ public ProtocolVersion getProtocolVersion() {
+ return httpResponse.getProtocolVersion();
+ }
+
+ @Override
+ public boolean containsHeader(String name) {
+ return httpResponse.containsHeader(name);
+ }
+
+ @Override
+ public Header[] getHeaders(String name) {
+ return httpResponse.getHeaders(name);
+ }
+
+ @Override
+ public Header getFirstHeader(String name) {
+ return httpResponse.getFirstHeader(name);
+ }
+
+ @Override
+ public Header getLastHeader(String name) {
+ return httpResponse.getLastHeader(name);
+ }
+
+ @Override
+ public Header[] getAllHeaders() {
+ return httpResponse.getAllHeaders();
+ }
+
+ @Override
+ public void addHeader(Header header) {
+ httpResponse.addHeader(header);
+ }
+
+ @Override
+ public void addHeader(String name, String value) {
+ httpResponse.addHeader(name, value);
+ }
+
+ @Override
+ public void setHeader(Header header) {
+ httpResponse.setHeader(header);
+ }
+
+ @Override
+ public void setHeader(String name, String value) {
+ httpResponse.setHeader(name, value);
+ }
+
+ @Override
+ public void setHeaders(Header[] headers) {
+ httpResponse.setHeaders(headers);
+ }
+
+ @Override
+ public void removeHeader(Header header) {
+ httpResponse.removeHeader(header);
+ }
+
+ @Override
+ public void removeHeaders(String name) {
+ httpResponse.removeHeaders(name);
+ }
+
+ @Override
+ public HeaderIterator headerIterator() {
+ return httpResponse.headerIterator();
+ }
+
+ @Override
+ public HeaderIterator headerIterator(String name) {
+ return httpResponse.headerIterator(name);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public HttpParams getParams() {
+ return httpResponse.getParams();
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public void setParams(HttpParams params) {
+ httpResponse.setParams(params);
+ }
+
+ @Override
+ public void close() throws IOException {
+ httpResponse.close();
+ }
+}
diff --git a/src/main/java/org/apache/sling/testing/clients/instance/InstanceConfiguration.java b/src/main/java/org/apache/sling/testing/clients/instance/InstanceConfiguration.java
new file mode 100644
index 0000000..168e6ab
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/instance/InstanceConfiguration.java
@@ -0,0 +1,41 @@
+/*
+ * 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.instance;
+
+import java.net.URI;
+
+/**
+ * Configuration of a single instance instance.
+ */
+public class InstanceConfiguration {
+
+ private URI url;
+ private final String runmode;
+
+ public InstanceConfiguration(final URI url, final String runmode) {
+ this.url = url;
+ this.runmode = runmode;
+ }
+
+ public URI getUrl() {
+ return this.url;
+ }
+
+ public String getRunmode() {
+ return runmode;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/testing/clients/instance/InstanceSetup.java b/src/main/java/org/apache/sling/testing/clients/instance/InstanceSetup.java
new file mode 100644
index 0000000..de4e5a3
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/instance/InstanceSetup.java
@@ -0,0 +1,88 @@
+/*
+ * 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.instance;
+
+import org.apache.sling.testing.clients.Constants;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Utility class for getting the current instance setup
+ */
+public final class InstanceSetup {
+
+ private static final Logger LOG = LoggerFactory.getLogger(InstanceSetup.class);
+ private static InstanceSetup SINGLETON;
+
+ /**
+ * @return the current setup object.
+ */
+ public static InstanceSetup get() {
+ if ( SINGLETON == null ) {
+ SINGLETON = new InstanceSetup();
+ }
+ return SINGLETON;
+ }
+
+ private final List<InstanceConfiguration> configs = new ArrayList<InstanceConfiguration>();
+
+ private InstanceSetup() {
+ final int number = Integer.valueOf(System.getProperty(Constants.CONFIG_PROP_PREFIX + "instances", "0"));
+ for (int i=1; i<=number; i++ ) {
+ URI url;
+ try {
+ url = new URI(System.getProperty(Constants.CONFIG_PROP_PREFIX + "instance.url." + String.valueOf(i)));
+ } catch (URISyntaxException e) {
+ LOG.error("Could not read URL for instance");
+ continue;
+ }
+ final String runmode = System.getProperty(Constants.CONFIG_PROP_PREFIX + "instance.runmode." + String.valueOf(i));
+
+ final InstanceConfiguration qc = new InstanceConfiguration(url, runmode);
+
+ this.configs.add(qc);
+ }
+ }
+
+ /**
+ * @return all instance configurations.
+ */
+ public List<InstanceConfiguration> getConfigurations() {
+ return this.configs;
+ }
+
+ /**
+ * Get the list of all InstanceConfiguration with a specific {@code runmode}
+ *
+ * @param runmode the desired runmode
+ * @return all instance configurations filtered by runmode.
+ */
+ public List<InstanceConfiguration> getConfigurations(final String runmode) {
+ final List<InstanceConfiguration> result = new ArrayList<InstanceConfiguration>();
+ for(final InstanceConfiguration qc : this.configs) {
+ if ( runmode == null || runmode.equals(qc.getRunmode()) ) {
+ result.add(qc);
+ }
+ }
+ return result;
+ }
+}
diff --git a/src/main/java/org/apache/sling/testing/clients/interceptors/DelayRequestInterceptor.java b/src/main/java/org/apache/sling/testing/clients/interceptors/DelayRequestInterceptor.java
new file mode 100644
index 0000000..ab4b41d
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/interceptors/DelayRequestInterceptor.java
@@ -0,0 +1,47 @@
+/*
+ * 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.interceptors;
+
+import org.apache.http.HttpException;
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpRequestInterceptor;
+import org.apache.http.protocol.HttpContext;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+
+public class DelayRequestInterceptor implements HttpRequestInterceptor {
+
+ private final long milliseconds;
+
+ public DelayRequestInterceptor(long milliseconds) {
+ this.milliseconds = milliseconds;
+ }
+
+ public void process(HttpRequest request, HttpContext context) throws HttpException, IOException {
+ if (milliseconds <= 0) {
+ return;
+ }
+
+ try {
+ Thread.sleep(milliseconds);
+ } catch (InterruptedException e) {
+ throw new InterruptedIOException();
+ }
+ }
+
+}
diff --git a/src/main/java/org/apache/sling/testing/clients/interceptors/StickyCookieHolder.java b/src/main/java/org/apache/sling/testing/clients/interceptors/StickyCookieHolder.java
new file mode 100644
index 0000000..c2b70a9
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/interceptors/StickyCookieHolder.java
@@ -0,0 +1,40 @@
+/*******************************************************************************
+ * 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
+ * <p/>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p/>
+ * 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.interceptors;
+
+import org.apache.http.cookie.Cookie;
+import org.apache.sling.testing.clients.Constants;
+
+public class StickyCookieHolder {
+
+ private static final ThreadLocal<Cookie> testStickySessionCookie = new ThreadLocal<Cookie>();
+ public static final String COOKIE_NAME = System.getProperty(Constants.CONFIG_PROP_PREFIX + "session.cookie.name", "test_session_id");
+
+ public static Cookie getTestStickySessionCookie() {
+ return testStickySessionCookie.get();
+ }
+
+ public static void setTestStickySessionCookie(Cookie stickySessionCookie) {
+ testStickySessionCookie.set(stickySessionCookie);
+ }
+
+ public static void remove() {
+ testStickySessionCookie.remove();
+ }
+}
diff --git a/src/main/java/org/apache/sling/testing/clients/interceptors/StickyCookieInterceptor.java b/src/main/java/org/apache/sling/testing/clients/interceptors/StickyCookieInterceptor.java
new file mode 100644
index 0000000..42a9948
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/interceptors/StickyCookieInterceptor.java
@@ -0,0 +1,61 @@
+/*
+ * 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.interceptors;
+
+
+import org.apache.http.HttpException;
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpRequestInterceptor;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.cookie.Cookie;
+import org.apache.http.impl.client.BasicCookieStore;
+import org.apache.http.protocol.HttpContext;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.ListIterator;
+
+public class StickyCookieInterceptor implements HttpRequestInterceptor {
+
+ public void process(HttpRequest httpRequest, HttpContext httpContext) throws HttpException, IOException {
+ final HttpClientContext clientContext = HttpClientContext.adapt(httpContext);
+ List<Cookie> cookies = clientContext.getCookieStore().getCookies();
+ boolean set = (null != StickyCookieHolder.getTestStickySessionCookie());
+ boolean found = false;
+ ListIterator<Cookie> it = cookies.listIterator();
+ while (it.hasNext()) {
+ Cookie cookie = it.next();
+ if (cookie.getName().equals(StickyCookieHolder.COOKIE_NAME)) {
+ found = true;
+ if (set) {
+ // set the cookie with the value saved for each thread using the rule
+ it.set(StickyCookieHolder.getTestStickySessionCookie());
+ } else {
+ // if the cookie is not set in TestStickySessionRule, remove it from here
+ it.remove();
+ }
+ }
+ }
+ // if the cookie needs to be set from TestStickySessionRule but did not exist in the client cookie list, add it here.
+ if (!found && set) {
+ cookies.add(StickyCookieHolder.getTestStickySessionCookie());
+ }
+ BasicCookieStore cs = new BasicCookieStore();
+ cs.addCookies(cookies.toArray(new Cookie[cookies.size()]));
+ httpContext.setAttribute(HttpClientContext.COOKIE_STORE, cs);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/testing/clients/interceptors/StickyCookieSpec.java b/src/main/java/org/apache/sling/testing/clients/interceptors/StickyCookieSpec.java
new file mode 100644
index 0000000..e295459
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/interceptors/StickyCookieSpec.java
@@ -0,0 +1,47 @@
+/*
+ * 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.interceptors;
+
+import org.apache.http.Header;
+import org.apache.http.cookie.Cookie;
+import org.apache.http.cookie.CookieOrigin;
+import org.apache.http.cookie.CookiePathComparator;
+import org.apache.http.cookie.MalformedCookieException;
+import org.apache.http.impl.cookie.DefaultCookieSpec;
+
+import java.util.List;
+
+public class StickyCookieSpec extends DefaultCookieSpec {
+ private final static CookiePathComparator PATH_COMPARATOR = new CookiePathComparator();
+
+ @Override
+ public List<Cookie> parse(Header header, CookieOrigin origin) throws MalformedCookieException {
+ List<Cookie> cookies = super.parse(header, origin);
+ for (Cookie cookie : cookies) {
+ if (cookie.getName().equals(StickyCookieHolder.COOKIE_NAME)) {
+ // store it in the TestStickySessionRule threadlocal var
+ StickyCookieHolder.setTestStickySessionCookie(cookie);
+ }
+ }
+ return cookies;
+ }
+
+ @Override
+ public List<Header> formatCookies(List<Cookie> cookies) {
+ return super.formatCookies(cookies);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/testing/clients/interceptors/TestDescriptionHolder.java b/src/main/java/org/apache/sling/testing/clients/interceptors/TestDescriptionHolder.java
new file mode 100644
index 0000000..2e5d2df
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/interceptors/TestDescriptionHolder.java
@@ -0,0 +1,49 @@
+/*******************************************************************************
+ * 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
+ * <p/>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p/>
+ * 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.interceptors;
+
+public class TestDescriptionHolder {
+
+ private static final ThreadLocal<String> methodName = new ThreadLocal<String>();
+ private static final ThreadLocal<String> className = new ThreadLocal<String>();
+
+ public static String getMethodName() {
+ return methodName.get();
+ }
+
+ public static void setMethodName(String methodName) {
+ TestDescriptionHolder.methodName.set(methodName);
+ }
+
+ public static void removeMethodName() {
+ TestDescriptionHolder.methodName.remove();
+ }
+
+ public static String getClassName() {
+ return className.get();
+ }
+
+ public static void setClassName(String className) {
+ TestDescriptionHolder.className.set(className);
+ }
+
+ public static void removeClassName() {
+ TestDescriptionHolder.className.remove();
+ }
+}
diff --git a/src/main/java/org/apache/sling/testing/clients/interceptors/TestDescriptionInterceptor.java b/src/main/java/org/apache/sling/testing/clients/interceptors/TestDescriptionInterceptor.java
new file mode 100644
index 0000000..38b269e
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/interceptors/TestDescriptionInterceptor.java
@@ -0,0 +1,42 @@
+/*
+ * 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.interceptors;
+
+import org.apache.http.HttpException;
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpRequestInterceptor;
+import org.apache.http.protocol.HttpContext;
+
+import java.io.IOException;
+
+/**
+ * HttpClient interceptor that propagates the current test name as part HTTP request headers.
+ * Headers can then be logged, exported as MDC info etc. by {@code TestNameLoggingFilter}.
+ *
+ * Meant to help in correlating the server side logs with the test case being executed.
+ *
+ * @see org.slf4j.MDC http://www.slf4j.org/manual.html
+ */
+public class TestDescriptionInterceptor implements HttpRequestInterceptor{
+ public static final String TEST_NAME_HEADER = "sling.test.name";
+ public static final String TEST_CLASS_HEADER = "sling.test.class";
+
+ public void process(HttpRequest httpRequest, HttpContext httpContext) throws HttpException, IOException {
+ httpRequest.addHeader(TEST_NAME_HEADER, TestDescriptionHolder.getMethodName());
+ httpRequest.addHeader(TEST_CLASS_HEADER, TestDescriptionHolder.getClassName());
+ }
+}
diff --git a/src/main/java/org/apache/sling/testing/clients/interceptors/package-info.java b/src/main/java/org/apache/sling/testing/clients/interceptors/package-info.java
new file mode 100644
index 0000000..1de07e5
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/interceptors/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * 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("1.0.0")
+package org.apache.sling.testing.clients.interceptors;
+
+import aQute.bnd.annotation.Version;
+
diff --git a/src/main/java/org/apache/sling/testing/clients/osgi/Bundle.java b/src/main/java/org/apache/sling/testing/clients/osgi/Bundle.java
new file mode 100644
index 0000000..729130e
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/osgi/Bundle.java
@@ -0,0 +1,55 @@
+/*
+ * 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;
+
+/**
+ * Thin Wrapper around a Bundle definition JSON
+ */
+public class Bundle {
+
+ public enum Status {
+
+ ACTIVE("Active"),
+
+ FRAGMENT("Fragment"),
+
+ RESOLVED("Resolved"),
+
+ INSTALLED("Installed");
+
+ String value;
+
+ Status(String value) {
+ this.value = value;
+ }
+
+ public static Status value(String o) {
+ for(Status s : values()) {
+ if(s.value.equalsIgnoreCase(o)) {
+ return s;
+ }
+ }
+ return null;
+ }
+
+ public String toString() {
+ return value;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/testing/clients/osgi/BundleInfo.java b/src/main/java/org/apache/sling/testing/clients/osgi/BundleInfo.java
new file mode 100644
index 0000000..33cdd22
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/osgi/BundleInfo.java
@@ -0,0 +1,124 @@
+/*
+ * 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.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+public class BundleInfo {
+
+ private JsonNode bundle;
+
+ public BundleInfo(JsonNode root) throws ClientException {
+ if(root.get("id") != null) {
+ if(root.get("id") == null) {
+ throw new ClientException("No Bundle Info returned");
+ }
+ bundle = root;
+ } else {
+ if(root.get("data") == null && root.get("data").size() < 1) {
+ throw new ClientException("No Bundle Info returned");
+ }
+ bundle = root.get("data").get(0);
+ }
+ }
+
+ /**
+ * @return the bundle identifier
+ */
+ public int getId() {
+ return bundle.get("id").getIntValue();
+ }
+
+ /**
+ * @return the bundle name
+ */
+ public String getName() {
+ return bundle.get("name").getTextValue();
+ }
+
+ /**
+ * @return the bundle version
+ */
+ public String getVersion() {
+ return bundle.get("version").getTextValue();
+ }
+
+ /**
+ * Returns the indicator if the bundle is a fragment
+ *
+ * @return {@code true} if bundle is a fragment, {@code false} otherwise.
+ */
+ public boolean isFragment() {
+ return bundle.get("fragment").getBooleanValue();
+ }
+
+ /**
+ * @return the bundle current state
+ */
+ public Bundle.Status getStatus() {
+ return Bundle.Status.value(bundle.get("state").getTextValue());
+ }
+
+ /**
+ * @return the bundle symbolic name
+ */
+ public String getSymbolicName() {
+ return bundle.get("symbolicName").getTextValue();
+ }
+
+ /**
+ * @return the category of the bundle
+ */
+ public String getCategory() {
+ return bundle.get("category").getTextValue();
+ }
+
+ /**
+ * Returns the value of a specific key in the bundle
+ *
+ * @param key the property to search
+ * @return a specific bundle property
+ */
+ public String getProperty(String key) {
+ Map<String, String> props = getProperties();
+ return props.get(key);
+ }
+
+ /**
+ * @return the bundle properties in a {@link Map}
+ */
+ public Map<String, String> getProperties() {
+ JsonNode props = bundle.get("props");
+ Map<String, String> entries = new HashMap<String, String>();
+
+ if(props != null) {
+ Iterator<JsonNode> it = props.getElements();
+ while(it.hasNext()) {
+ JsonNode n = it.next();
+ entries.put(n.get("key").getTextValue(), n.get("value").getTextValue());
+ }
+ }
+ return entries;
+ }
+
+}
diff --git a/src/main/java/org/apache/sling/testing/clients/osgi/BundlesInfo.java b/src/main/java/org/apache/sling/testing/clients/osgi/BundlesInfo.java
new file mode 100644
index 0000000..7ada2da
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/osgi/BundlesInfo.java
@@ -0,0 +1,144 @@
+/*
+ * 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.Iterator;
+
+/**
+ * A simple Wrapper around the returned JSON when requesting the status of /system/console/bundles
+ */
+public class BundlesInfo {
+
+ private JsonNode root = null;
+
+ private JsonNode status = 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 BundlesInfo(JsonNode root) throws ClientException {
+ this.root = root;
+ // some simple sanity checks
+ if(root.get("s") == null)
+ throw new ClientException("No Status Info returned!");
+ if(root.get("s").size() != 5)
+ throw new ClientException("Wrong number of status numbers listed!");
+ status = root.get("s");
+ }
+
+ /**
+ * @return the status message of the bundle context
+ * @throws ClientException if the request cannot be completed
+ */
+ public String getStatusMessage() throws ClientException {
+ if(root.get("status") == null)
+ throw new ClientException("No Status message returned!");
+ return root.get("status").getValueAsText();
+ }
+
+ /**
+ * @return total number of bundles.
+ */
+ public int getTotalNumOfBundles() {
+ return Integer.parseInt(status.get(0).getValueAsText());
+ }
+
+ /**
+ * Returns number of bundles that are in specified state
+ *
+ * @param status the requested status
+ * @return the number of bundles
+ */
+ public int getNumBundlesByStatus(Bundle.Status status) {
+ int index = -1;
+ switch(status) {
+ case ACTIVE:
+ index = 1;
+ break;
+ case FRAGMENT:
+ index = 2;
+ break;
+ case RESOLVED:
+ index = 3;
+ break;
+ case INSTALLED:
+ index = 4;
+ break;
+ }
+ return Integer.parseInt(this.status.get(index).getValueAsText());
+ }
+
+ /**
+ * Return bundle info for a bundle with persistence identifier {@code pid}
+ *
+ * @param id the id of the bundle
+ * @return the BundleInfo
+ * @throws ClientException if the info could not be retrieved
+ */
+ public BundleInfo forId(String id) throws ClientException {
+ JsonNode bundle = findBy("id", id);
+ return (bundle != null) ? new BundleInfo(bundle) : null;
+ }
+
+ /**
+ * Return bundle info for a bundle with name {@code name}
+ *
+ * @param name the name of the requested bundle
+ * @return the info, or {@code null} if the bundle is not found
+ * @throws ClientException if the info cannot be retrieved
+ */
+ public BundleInfo forName(String name) throws ClientException {
+ JsonNode bundle = findBy("name", name);
+ return (bundle != null) ? new BundleInfo(bundle) : null;
+ }
+
+ /**
+ * Return bundle info for a bundle with symbolic name {@code name}
+ *
+ * @param name the symbolic name of the requested bundle
+ * @return the info, or {@code null} if the bundle is not found
+ * @throws ClientException if the info cannot be retrieved
+ */
+ public BundleInfo forSymbolicName(String name) throws ClientException {
+ JsonNode bundle = findBy("symbolicName", name);
+ return (bundle != null) ? new BundleInfo(bundle) : null;
+ }
+
+ private JsonNode findBy(String key, String value) {
+ Iterator<JsonNode> nodes = root.get("data").getElements();
+ while(nodes.hasNext()) {
+ JsonNode node = nodes.next();
+ if(node.get(key) != null) {
+ if(node.get(key).isValueNode()) {
+ String valueNode=node.get(key).getTextValue();
+ if (valueNode.equals(value)){
+ return node;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/testing/clients/osgi/BundlesInstaller.java b/src/main/java/org/apache/sling/testing/clients/osgi/BundlesInstaller.java
new file mode 100644
index 0000000..1c737a4
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/osgi/BundlesInstaller.java
@@ -0,0 +1,186 @@
+/*
+ * 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.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.util.LinkedList;
+import java.util.List;
+import java.util.jar.JarInputStream;
+import java.util.jar.Manifest;
+
+
+/** Utility that installs and starts additional bundles for testing */
+public class BundlesInstaller {
+ private final Logger log = LoggerFactory.getLogger(getClass());
+ private final WebconsoleClient webconsoleClient;
+ public static final String ACTIVE_STATE = "active";
+
+ public BundlesInstaller(WebconsoleClient wcc) {
+ webconsoleClient = wcc;
+ }
+
+ public boolean isInstalled(File bundleFile) throws Exception {
+ final String bundleSymbolicName = getBundleSymbolicName(bundleFile);
+ try{
+ log.debug("Checking if installed: "+bundleSymbolicName);
+ webconsoleClient.checkBundleInstalled(bundleSymbolicName, 1);
+ // if this succeeds, then there's no need to install again
+ log.debug("Already installed: "+bundleSymbolicName);
+ return true;
+ } catch(AssertionError e) {
+ log.debug("Not yet installed: "+bundleSymbolicName);
+ return false;
+ }
+
+ }
+
+ /** Check if the installed version matches the one of the bundle (file) **/
+ public boolean isInstalledWithSameVersion(File bundleFile) throws Exception {
+ final String bundleSymbolicName = getBundleSymbolicName(bundleFile);
+ final String versionOnServer = webconsoleClient.getBundleVersion(bundleSymbolicName);
+ final String versionInBundle = getBundleVersion(bundleFile);
+ if (versionOnServer.equals(versionInBundle)) {
+ return true;
+ } else {
+ log.info("Bundle installed doesn't match: "+bundleSymbolicName+
+ ", versionOnServer="+versionOnServer+", versionInBundle="+versionInBundle);
+ return false;
+ }
+ }
+
+ /** Install a list of bundles supplied as Files */
+ public void installBundles(List<File> toInstall, boolean startBundles) throws Exception {
+ for(File f : toInstall) {
+ final String bundleSymbolicName = getBundleSymbolicName(f);
+ if (isInstalled(f)) {
+ if (f.getName().contains("SNAPSHOT")) {
+ log.info("Reinstalling (due to SNAPSHOT version): {}", bundleSymbolicName);
+ webconsoleClient.uninstallBundle(bundleSymbolicName, f);
+ } else if (!isInstalledWithSameVersion(f)) {
+ log.info("Reinstalling (due to version mismatch): {}", bundleSymbolicName);
+ webconsoleClient.uninstallBundle(bundleSymbolicName, f);
+ } else {
+ log.info("Not reinstalling: {}", bundleSymbolicName);
+ continue;
+ }
+ }
+ webconsoleClient.installBundle(f, startBundles);
+ log.info("Installed: {}", bundleSymbolicName);
+ }
+
+ // ensure that bundles are re-wired esp. if an existing bundle was updated
+ webconsoleClient.refreshPackages();
+
+ log.info("{} additional bundles installed", toInstall.size());
+ }
+
+ /** Uninstall a list of bundles supplied as Files */
+ public void uninstallBundles(List<File> toUninstall) throws Exception {
+ for(File f : toUninstall) {
+ final String bundleSymbolicName = getBundleSymbolicName(f);
+ if (isInstalled(f)) {
+ log.info("Uninstalling bundle: {}", bundleSymbolicName);
+ webconsoleClient.uninstallBundle(bundleSymbolicName, f);
+ } else {
+ log.info("Could not uninstall: {} as it never was installed", bundleSymbolicName);
+ }
+ }
+
+ // ensure that bundles are re-wired esp. if an existing bundle was updated
+ webconsoleClient.refreshPackages();
+
+ log.info("{} additional bundles uninstalled", toUninstall.size());
+ }
+
+ /** Wait for all bundles specified in symbolicNames list to be installed in the
+ * remote web console.
+ */
+ public void waitForBundlesInstalled(List<String> symbolicNames, int timeoutSeconds) throws Exception {
+ log.info("Checking that bundles are installed (timeout {} seconds): {}", timeoutSeconds, symbolicNames);
+ for(String symbolicName : symbolicNames) {
+ webconsoleClient.checkBundleInstalled(symbolicName, timeoutSeconds);
+ }
+ }
+
+ public void startAllBundles(List<String> symbolicNames, int timeoutSeconds) throws Exception {
+ log.info("Starting bundles (timeout {} seconds): {}", timeoutSeconds, symbolicNames);
+
+ final long timeout = System.currentTimeMillis() + timeoutSeconds * 1000L;
+ final List<String> toStart = new LinkedList<String>();
+ while(System.currentTimeMillis() < timeout) {
+ toStart.clear();
+ for(String name : symbolicNames) {
+ final String state = webconsoleClient.getBundleState(name);
+ if(!state.equalsIgnoreCase(ACTIVE_STATE)) {
+ toStart.add(name);
+ break;
+ }
+ }
+
+ if(toStart.isEmpty()) {
+ log.info("Ok - all bundles are in the {} state", ACTIVE_STATE);
+ break;
+ }
+
+ for(String name : toStart) {
+ webconsoleClient.startBundle(name);
+ }
+
+ Thread.sleep(500L);
+ }
+
+ if(!toStart.isEmpty()) {
+ throw new Exception("Some bundles did not start: " + toStart);
+ }
+ }
+
+ public 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;
+ }
+
+ public String getBundleVersion(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;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/testing/clients/osgi/Component.java b/src/main/java/org/apache/sling/testing/clients/osgi/Component.java
new file mode 100644
index 0000000..045160c
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/osgi/Component.java
@@ -0,0 +1,51 @@
+/*
+ * 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;
+
+public class Component {
+
+ public enum Status {
+
+ ACTIVE("active"),
+
+ REGISTERED("registered"),
+
+ UNSATISFIED("unsatisfied");
+
+ String value;
+
+ Status(String value) {
+ this.value = value;
+ }
+
+ public static Status value(String o) {
+ for(Status s : values()) {
+ if(s.value.equalsIgnoreCase(o)) {
+ return s;
+ }
+ }
+ return null;
+ }
+
+ public String toString() {
+ return value;
+ }
+
+ }
+
+}
diff --git a/src/main/java/org/apache/sling/testing/clients/osgi/ComponentInfo.java b/src/main/java/org/apache/sling/testing/clients/osgi/ComponentInfo.java
new file mode 100644
index 0000000..32ed94c
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/osgi/ComponentInfo.java
@@ -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.osgi;
+
+import org.apache.sling.testing.clients.ClientException;
+import org.codehaus.jackson.JsonNode;
+
+public class ComponentInfo {
+
+ private JsonNode component;
+
+ public ComponentInfo(JsonNode root) throws ClientException {
+ if(root.get("id") != null) {
+ if(root.get("id") == null) {
+ throw new ClientException("No Component Info returned");
+ }
+ component = root;
+ } else {
+ if(root.get("data") == null && root.get("data").size() < 1) {
+ throw new ClientException("No Component Info returned");
+ }
+ component = root.get("data").get(0);
+ }
+ }
+
+ /**
+ * @return the component identifier
+ */
+ public int getId() {
+ return component.get("id").getIntValue();
+ }
+
+ /**
+ * @return the component name
+ */
+ public String getName() {
+ return component.get("name").getTextValue();
+ }
+
+ /**
+ * @return the component status
+ */
+ public Component.Status getStatus() {
+ return Component.Status.value(component.get("state").getTextValue());
+ }
+
+ /**
+ * @return the component persistent identifier
+ */
+ public String getPid() {
+ return component.get("pid").getTextValue();
+ }
+
+}
diff --git a/src/main/java/org/apache/sling/testing/clients/osgi/ComponentsInfo.java b/src/main/java/org/apache/sling/testing/clients/osgi/ComponentsInfo.java
new file mode 100644
index 0000000..174544e
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/osgi/ComponentsInfo.java
@@ -0,0 +1,95 @@
+/*
+ * 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.Iterator;
+
+/**
+ * Thin wrapper around the list of components
+ */
+public class ComponentsInfo {
+
+ private JsonNode root = null;
+
+ /**
+ * The only constructor.
+ *
+ * @param rootNode the root JSON node of the components info.
+ * @throws ClientException if the info cannot be retrieved
+ */
+ public ComponentsInfo(JsonNode rootNode) throws ClientException {
+ this.root = rootNode;
+ }
+
+ /**
+ * @return the number of installed components
+ * @throws ClientException if the info cannot be retrieved
+ */
+ public int getNumberOfInstalledComponents() throws ClientException {
+ if(root.get("status") == null)
+ throw new ClientException("Number of installed Components not defined!");
+ return Integer.parseInt(root.get("status").getValueAsText());
+ }
+
+ /**
+ * @param id the id of the component
+ * @return the ComponentInfo for a component with the identifier {@code id}
+ * @throws ClientException if the info cannot be retrieved
+ */
+ public ComponentInfo forId(String id) throws ClientException {
+ JsonNode component = findBy("id", id);
+ return (component != null) ? new ComponentInfo(component) : null;
+ }
+
+ /**
+ * @param name the name of the component
+ * @return the ComponentInfo for a component with the name {@code name}
+ * @throws ClientException if the info cannot be retrieved
+ */
+ public ComponentInfo forName(String name) throws ClientException {
+ JsonNode component = findBy("name", name);
+ return (component != null) ? new ComponentInfo(component) : null;
+ }
+
+ /**
+ * @param pid the pid of the component
+ * @return the ComponentInfo for a component with the pid {@code pid}
+ * @throws ClientException if the info cannot be retrieved
+ */
+ public ComponentInfo forPid(String pid) throws ClientException {
+ JsonNode component = findBy("pid", pid);
+ return (component != null) ? new ComponentInfo(component) : null;
+ }
+
+ private JsonNode findBy(String key, String value) {
+ Iterator<JsonNode> nodes = root.get("data").getElements();
+ while(nodes.hasNext()) {
+ JsonNode node = nodes.next();
+ if(node.get(key) != null) {
+ if(node.get(key).isValueNode()) {
+ return node;
+ }
+ }
+ }
+ return null;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/testing/clients/osgi/OsgiConsoleClient.java b/src/main/java/org/apache/sling/testing/clients/osgi/OsgiConsoleClient.java
new file mode 100644
index 0000000..9c5a928
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/osgi/OsgiConsoleClient.java
@@ -0,0 +1,324 @@
+/*
+ * 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.impl.client.CloseableHttpClient;
+import org.apache.sling.testing.clients.ClientException;
+import org.apache.sling.testing.clients.SlingHttpResponse;
+import org.apache.sling.testing.clients.util.JsonUtils;
+import org.apache.sling.testing.clients.util.poller.AbstractPoller;
+import org.apache.sling.testing.clients.SlingClient;
+import org.apache.sling.testing.clients.SlingClientConfig;
+import org.apache.sling.testing.clients.util.FormEntityBuilder;
+import org.apache.sling.testing.clients.util.HttpUtils;
+import org.codehaus.jackson.JsonNode;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.URI;
+import java.util.*;
+
+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.
+ */
+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";
+
+ /**
+ * 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 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.
+ *
+ * @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"
+ */
+ public Map<String, Object> getConfigurationWithWait(long waitCount, String pid, int... expectedStatus)
+ throws ClientException, InterruptedException {
+ ConfigurationPoller poller = new ConfigurationPoller(500L, waitCount, pid, expectedStatus);
+ if (!poller.callUntilCondition())
+ return getConfiguration(pid, expectedStatus);
+ 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.
+ *
+ * @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"
+ */
+ 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;
+ }
+
+ /**
+ * Delete the config referenced by the PID
+ *
+ * @param pid pid
+ * @param expectedStatus expected response status
+ * @throws ClientException if the response status does not match any of the expectedStatus
+ */
+ public void 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));
+ }
+
+
+ class ConfigurationPoller extends AbstractPoller {
+
+ private final String pid;
+ int[] expectedStatus;
+ public Map<String, Object> config;
+
+ public ConfigurationPoller(long waitInterval, long waitCount, String pid, int... expectedStatus) {
+ super(waitInterval, waitCount);
+ this.pid = pid;
+ this.config = null;
+ this.expectedStatus = expectedStatus;
+ }
+
+ @Override
+ public boolean call() {
+ try {
+ config = getConfiguration(pid, expectedStatus);
+ } catch (ClientException e) {
+ LOG.warn("Couldn't get config " + pid, e);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean condition() {
+ return null != config;
+ }
+
+ public Map<String, Object> getConfig() {
+ return config;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/testing/clients/osgi/OsgiInstanceConfig.java b/src/main/java/org/apache/sling/testing/clients/osgi/OsgiInstanceConfig.java
new file mode 100644
index 0000000..41ea39c
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/osgi/OsgiInstanceConfig.java
@@ -0,0 +1,94 @@
+/*
+ * 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;
+
+/**
+ * <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 {
+
+ /**
+ * Number of retries for retrieving the current osgi config for save() and restore()
+ */
+ protected int waitCount = 20;
+
+ private static final Logger LOG = LoggerFactory.getLogger(OsgiInstanceConfig.class);
+ private final OsgiConsoleClient osgiClient;
+ private final String configPID;
+ private Map<String, Object> config;
+
+
+ /**
+ *
+ * @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
+ */
+ public <T extends SlingClient> OsgiInstanceConfig(T client, String configPID) throws ClientException, InstanceConfigException {
+ 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 {
+ try {
+ this.config = osgiClient.getConfigurationWithWait(waitCount, 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 (InterruptedException e) {
+ throw new InstanceConfigException("Saving configuration was interrupted ", 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 {
+ try {
+ osgiClient.editConfigurationWithWait(waitCount, 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 (InterruptedException e) {
+ throw new InstanceConfigException("Restoring configuration was interrupted", e);
+ }
+ return this;
+ }
+}
diff --git a/src/main/java/org/apache/sling/testing/clients/osgi/WebconsoleClient.java b/src/main/java/org/apache/sling/testing/clients/osgi/WebconsoleClient.java
new file mode 100644
index 0000000..bbda9bc
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/osgi/WebconsoleClient.java
@@ -0,0 +1,190 @@
+/*
+ * 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.io.File;
+
+import org.apache.http.entity.mime.MultipartEntity;
+import org.apache.http.entity.mime.content.FileBody;
+import org.apache.http.entity.mime.content.StringBody;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.sling.commons.json.JSONArray;
+import org.apache.sling.commons.json.JSONObject;
+import org.apache.sling.testing.tools.http.RequestBuilder;
+import org.apache.sling.testing.tools.http.RequestExecutor;
+import org.apache.sling.testing.tools.http.RetryingContentChecker;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** HTTP Client for the Felix webconsole - simplistic for now */
+public class WebconsoleClient {
+ private final Logger log = LoggerFactory.getLogger(getClass());
+ private final RequestExecutor executor;
+ private final RequestBuilder builder;
+ private final String username;
+ private final String password;
+
+ 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";
+ public static final String CONSOLE_BUNDLES_PATH = "/system/console/bundles";
+
+ public WebconsoleClient(String slingServerUrl, String username, String password) {
+ this.builder = new RequestBuilder(slingServerUrl);
+ this.executor = new RequestExecutor(new DefaultHttpClient());
+ this.username = username;
+ this.password = password;
+ }
+
+ public void uninstallBundle(String symbolicName, File f) throws Exception {
+ final long bundleId = getBundleId(symbolicName);
+
+ log.info("Uninstalling bundle {} with bundleId {}", symbolicName, bundleId);
+
+ final MultipartEntity entity = new MultipartEntity();
+ entity.addPart("action",new StringBody("uninstall"));
+ executor.execute(
+ builder.buildPostRequest(CONSOLE_BUNDLES_PATH+"/"+bundleId)
+ .withCredentials(username, password)
+ .withEntity(entity)
+ ).assertStatus(200);
+ }
+
+ /** Install a bundle using the Felix webconsole HTTP interface, with a specific start level */
+ public void installBundle(File f, boolean startBundle) throws Exception {
+ installBundle(f, startBundle, 0);
+ }
+
+ /** Install a bundle using the Felix webconsole HTTP interface, with a specific start level */
+ public void installBundle(File f, boolean startBundle, int startLevel) throws Exception {
+
+ // Setup request for Felix Webconsole bundle install
+ final MultipartEntity entity = new MultipartEntity();
+ entity.addPart("action",new StringBody("install"));
+ if(startBundle) {
+ entity.addPart("bundlestart", new StringBody("true"));
+ }
+ entity.addPart("bundlefile", new FileBody(f));
+
+ if(startLevel > 0) {
+ entity.addPart("bundlestartlevel", new StringBody(String.valueOf(startLevel)));
+ log.info("Installing bundle {} at start level {}", f.getName(), startLevel);
+ } else {
+ log.info("Installing bundle {} at default start level", f.getName());
+ }
+
+ // Console returns a 302 on success (and in a POST this
+ // is not handled automatically as per HTTP spec)
+ executor.execute(
+ builder.buildPostRequest(CONSOLE_BUNDLES_PATH)
+ .withCredentials(username, password)
+ .withEntity(entity)
+ ).assertStatus(302);
+ }
+
+ /** Check that specified bundle is installed - must be called
+ * before other methods that take a symbolicName parameter,
+ * in case installBundle was just called and the actual
+ * installation hasn't happened yet. */
+ public void checkBundleInstalled(String symbolicName, int timeoutSeconds) {
+ final String path = getBundlePath(symbolicName, ".json");
+ new RetryingContentChecker(executor, builder, username, password).check(path, 200, timeoutSeconds, 500);
+ }
+
+ private JSONObject getBundleData(String symbolicName) throws Exception {
+ // This 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":""},
+ // ]}
+ final String path = getBundlePath(symbolicName, ".json");
+ final String content = executor.execute(
+ builder.buildGetRequest(path)
+ .withCredentials(username, password)
+ ).assertStatus(200)
+ .getContent();
+
+ final JSONObject root = new JSONObject(content);
+ if(!root.has(JSON_KEY_DATA)) {
+ throw new Exception(path + " does not provide '" + JSON_KEY_DATA + "' element, JSON content=" + content);
+ }
+ final JSONArray data = root.getJSONArray(JSON_KEY_DATA);
+ if(data.length() < 1) {
+ throw new Exception(path + "." + JSON_KEY_DATA + " is empty, JSON content=" + content);
+ }
+ final JSONObject bundle = data.getJSONObject(0);
+ if(!bundle.has(JSON_KEY_STATE)) {
+ throw new Exception(path + ".data[0].state missing, JSON content=" + content);
+ }
+ return bundle;
+ }
+
+ /** Get bundle id */
+ public long getBundleId(String symbolicName) throws Exception {
+ final JSONObject bundle = getBundleData(symbolicName);
+ return bundle.getLong(JSON_KEY_ID);
+ }
+
+ /** Get bundle version **/
+ public String getBundleVersion(String symbolicName) throws Exception {
+ final JSONObject bundle = getBundleData(symbolicName);
+ return bundle.getString(JSON_KEY_VERSION);
+ }
+
+ /** Get specified bundle state */
+ public String getBundleState(String symbolicName) throws Exception {
+ final JSONObject bundle = getBundleData(symbolicName);
+ return bundle.getString(JSON_KEY_STATE);
+ }
+
+ /** Start specified bundle */
+ public void startBundle(String symbolicName) throws Exception {
+ // To start the bundle we POST action=start to its URL
+ final String path = getBundlePath(symbolicName, null);
+ log.info("Starting bundle {} via {}", symbolicName, path);
+
+ final MultipartEntity entity = new MultipartEntity();
+ entity.addPart("action",new StringBody("start"));
+ executor.execute(
+ builder.buildPostRequest(path)
+ .withCredentials(username, password)
+ .withEntity(entity)
+ ).assertStatus(200);
+ }
+
+ private String getBundlePath(String symbolicName, String extension) {
+ return CONSOLE_BUNDLES_PATH + "/" + symbolicName
+ + (extension == null ? "" : extension);
+ }
+
+ /** Calls PackageAdmin.refreshPackages to enforce re-wiring of all bundles. */
+ public void refreshPackages() throws Exception {
+ log.info("Refresh packages.");
+
+ final MultipartEntity entity = new MultipartEntity();
+ entity.addPart("action", new StringBody("refreshPackages"));
+
+ executor.execute(
+ builder.buildPostRequest(CONSOLE_BUNDLES_PATH)
+ .withCredentials(username, password)
+ .withEntity(entity)
+ ).assertStatus(200);
+ }
+
+}
diff --git a/src/main/java/org/apache/sling/testing/clients/osgi/package-info.java b/src/main/java/org/apache/sling/testing/clients/osgi/package-info.java
new file mode 100644
index 0000000..3deee0a
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/osgi/package-info.java
@@ -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.
+ */
+/**
+ * OSGI testing tools.
+ */
+@aQute.bnd.annotation.Version("1.0.0")
+package org.apache.sling.testing.clients.osgi;
diff --git a/src/main/java/org/apache/sling/testing/clients/package-info.java b/src/main/java/org/apache/sling/testing/clients/package-info.java
new file mode 100644
index 0000000..ec6c85a
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * 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("1.0.0")
+package org.apache.sling.testing.clients;
+
+import aQute.bnd.annotation.Version;
+
diff --git a/src/main/java/org/apache/sling/testing/clients/util/FormEntityBuilder.java b/src/main/java/org/apache/sling/testing/clients/util/FormEntityBuilder.java
new file mode 100644
index 0000000..85822cc
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/util/FormEntityBuilder.java
@@ -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);
+ }
+ }
+}
diff --git a/src/main/java/org/apache/sling/testing/clients/util/HttpUtils.java b/src/main/java/org/apache/sling/testing/clients/util/HttpUtils.java
new file mode 100644
index 0000000..c845b28
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/util/HttpUtils.java
@@ -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);
+ }
+
+
+ /**
+ * 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
diff --git a/src/main/java/org/apache/sling/testing/clients/util/InputStreamBodyWithLength.java b/src/main/java/org/apache/sling/testing/clients/util/InputStreamBodyWithLength.java
new file mode 100644
index 0000000..36361ef
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/util/InputStreamBodyWithLength.java
@@ -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
diff --git a/src/main/java/org/apache/sling/testing/clients/util/JsonUtils.java b/src/main/java/org/apache/sling/testing/clients/util/JsonUtils.java
new file mode 100644
index 0000000..e64ac8d
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/util/JsonUtils.java
@@ -0,0 +1,44 @@
+/*
+ * 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.sling.testing.clients.ClientException;
+import org.codehaus.jackson.JsonNode;
+import org.codehaus.jackson.JsonProcessingException;
+import org.codehaus.jackson.map.ObjectMapper;
+
+import java.io.IOException;
+
+public class JsonUtils {
+ /**
+ * Get {@link JsonNode} from a a String containing JSON.
+ *
+ * @param jsonString A string containing JSON
+ * @return A {@link JsonNode} that is the root node of the JSON structure.
+ * @throws ClientException if error occurs while reading json string
+ */
+ public static JsonNode getJsonNodeFromString(String jsonString) throws ClientException {
+ try {
+ ObjectMapper mapper = new ObjectMapper();
+ return mapper.readTree(jsonString);
+ } catch (JsonProcessingException e) {
+ throw new ClientException("Could not read json file.", e);
+ } catch (IOException e) {
+ throw new ClientException("Could not read json node.", e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/testing/clients/util/PortAllocator.java b/src/main/java/org/apache/sling/testing/clients/util/PortAllocator.java
new file mode 100644
index 0000000..a295a85
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/util/PortAllocator.java
@@ -0,0 +1,58 @@
+/*
+ * 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 java.net.ServerSocket;
+import java.util.HashSet;
+import java.util.Set;
+
+public class PortAllocator {
+
+ private static Set<Integer> allocatedPorts;
+
+ static {
+ allocatedPorts = new HashSet<Integer>();
+ }
+
+ public Integer allocatePort() {
+ while (true) {
+ int port = tryAllocation();
+
+ boolean portAdded = checkAndAddPort(port);
+
+ if (portAdded) {
+ return port;
+ }
+ }
+ }
+
+ private int tryAllocation() {
+ try {
+ ServerSocket serverSocket = new ServerSocket(0);
+ int port = serverSocket.getLocalPort();
+ serverSocket.close();
+ return port;
+ } catch (Exception e) {
+ throw new RuntimeException("Can't allocate a port");
+ }
+ }
+
+ private synchronized boolean checkAndAddPort(int port) {
+ return allocatedPorts.add(port);
+ }
+
+}
diff --git a/src/main/java/org/apache/sling/testing/clients/util/ResourceUtil.java b/src/main/java/org/apache/sling/testing/clients/util/ResourceUtil.java
new file mode 100644
index 0000000..6b88c53
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/util/ResourceUtil.java
@@ -0,0 +1,66 @@
+/*
+ * 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 java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+public class ResourceUtil {
+
+ /**
+ * We must get the Resource as a stream from the ContextClassLoader and not from the normal classLoader
+ * acquired by using getClass.getClassLoader, since we must be able to load resources from different threads
+ * e.g. running in ant.
+ *
+ * @param resourcePath path to the resource
+ * @return resource as InputStream
+ */
+ public static InputStream getResourceAsStream(String resourcePath) {
+ return Thread.currentThread().getContextClassLoader().getClass().getResourceAsStream(resourcePath);
+ }
+
+ /**
+ * Helper method to read a resource from class using {@link Class#getResourceAsStream(String)}
+ * and convert into a String.
+ *
+ * @param resource The resource to read.
+ * @return The requested resource as String, resolved using
+ * {@link Class#getResourceAsStream(String)}, or {@code null}
+ * if the requested resource cannot be resolved for some reason
+ * @throws IOException if the Resource Stream cannot be read
+ */
+ public static String readResourceAsString(String resource) throws IOException {
+ InputStream resourceAsStream = ResourceUtil.getResourceAsStream(resource);
+ if (resourceAsStream != null) {
+ StringBuilder sb = new StringBuilder();
+ String line;
+ try {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(resourceAsStream, "UTF-8"));
+ while ((line = reader.readLine()) != null) {
+ sb.append(line).append("\n");
+ }
+ } finally {
+ resourceAsStream.close();
+ }
+ return sb.toString();
+ }
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/apache/sling/testing/clients/util/SlingParameter.java b/src/main/java/org/apache/sling/testing/clients/util/SlingParameter.java
new file mode 100644
index 0000000..fbf6be2
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/util/SlingParameter.java
@@ -0,0 +1,108 @@
+/*
+ * 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.message.BasicNameValuePair;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ *
+ */
+public class SlingParameter {
+
+ private String typeHint = null;
+ private boolean delete = false;
+
+ String parameterName;
+ private String[] values = null;
+ private boolean multiple = false;
+
+ public SlingParameter(String parameterName) {
+ if (parameterName == null || parameterName.length() == 0) {
+ throw new IllegalArgumentException("parameterName must not be null or empty");
+ }
+ this.parameterName = parameterName;
+ }
+
+ public SlingParameter value(String value) {
+ if (value != null) {
+ this.values(new String[]{value});
+ } else {
+ this.values(new String[]{});
+ }
+ return this;
+ }
+
+ public SlingParameter values(String[] values) {
+ if (values == null) {
+ this.values = new String[]{};
+ } else {
+ this.values = values;
+ }
+ return this;
+ }
+
+ public SlingParameter typeHint(String typeHint) {
+ this.typeHint = typeHint;
+ return this;
+ }
+
+ public SlingParameter delete() {
+ this.delete = true;
+ return this;
+ }
+
+ public SlingParameter multiple() {
+ this.multiple = true;
+ return this;
+ }
+
+ public List<NameValuePair> toNameValuePairs() {
+ List<NameValuePair> parameters = new ArrayList<NameValuePair>();
+
+ if (multiple) {
+ for (String value : values) {
+ parameters.add(new BasicNameValuePair(parameterName, value));
+ }
+ } else if (values != null && values.length == 1) {
+ parameters.add(new BasicNameValuePair(parameterName, values[0]));
+ } else if (values != null && values.length > 1) {
+ // TODO not sure about the proper format of the values in this case?
+ // For now, only take the first one.
+ parameters.add(new BasicNameValuePair(parameterName, values[0]));
+ } else {
+ parameters.add(new BasicNameValuePair(parameterName, null));
+ }
+
+ // add @TypeHint suffix
+ if (typeHint != null) {
+ String parameter = parameterName + "@TypeHint";
+ parameters.add(new BasicNameValuePair(parameter, typeHint));
+ }
+
+ // add @Delete suffix
+ if (delete) {
+ String parameter = parameterName + "@Delete";
+ parameters.add(new BasicNameValuePair(parameter, "true"));
+ }
+
+ return parameters;
+ }
+}
diff --git a/src/main/java/org/apache/sling/testing/clients/util/URLParameterBuilder.java b/src/main/java/org/apache/sling/testing/clients/util/URLParameterBuilder.java
new file mode 100644
index 0000000..e47f7ab
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/util/URLParameterBuilder.java
@@ -0,0 +1,79 @@
+/*
+ * 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.utils.URLEncodedUtils;
+import org.apache.http.message.BasicNameValuePair;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class URLParameterBuilder {
+
+ public final static String DEFAULT_ENCODING = "UTF-8";
+
+ private List<NameValuePair> params;
+ private String encoding;
+
+ public static URLParameterBuilder create() {
+ return new URLParameterBuilder();
+ }
+
+ URLParameterBuilder() {
+ params = new ArrayList<NameValuePair>();
+ encoding = DEFAULT_ENCODING;
+ }
+
+ public URLParameterBuilder add(String name, String value) {
+ params.add(new BasicNameValuePair(name, value));
+ return this;
+ }
+
+ public URLParameterBuilder add(NameValuePair pair) {
+ params.add(pair);
+ return this;
+ }
+
+ public URLParameterBuilder add(List<NameValuePair> list) {
+ params.addAll(list);
+ return this;
+ }
+
+ public URLParameterBuilder add(String name, String[] values) {
+ for (String value : values) this.add(name, value);
+ return this;
+ }
+
+ public URLParameterBuilder setEncoding(String encoding) {
+ this.encoding = encoding;
+ return this;
+ }
+
+ /**
+ * Build the URL parameters
+ *
+ * @return The URL parameters string without the leading question mark.
+ */
+ public String getURLParameters() {
+ return URLEncodedUtils.format(params, encoding);
+ }
+
+ public List<NameValuePair> getList() {
+ return params;
+ }
+}
diff --git a/src/main/java/org/apache/sling/testing/clients/util/UniquePaths.java b/src/main/java/org/apache/sling/testing/clients/util/UniquePaths.java
new file mode 100644
index 0000000..a80f97e
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/util/UniquePaths.java
@@ -0,0 +1,67 @@
+/*
+ * 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 java.util.concurrent.atomic.AtomicLong;
+
+/** Generate unique paths, for tests isolation */
+public class UniquePaths {
+
+ private static long startTime = System.currentTimeMillis();
+ private static AtomicLong counter = new AtomicLong();
+ public final static String SEP = "_";
+ public final static String U_PATTERN = "_UNIQ_";
+
+ /**
+ * Return a unique path based on basePath
+ * @param nameReference The simple class name of that object is used as part of the
+ * generated unique ID
+ * @param basePath All occurrences of {@link UniquePaths#U_PATTERN} in basePath are replaced by the generated
+ * unique ID. If $U$ is not found in basePath, unique ID is added at its end.
+ * @return path with a unique value for each call.
+ */
+ public static String get(Object nameReference, String basePath) {
+ if(basePath == null) {
+ basePath = "";
+ }
+
+ final StringBuilder sb = new StringBuilder();
+ sb.append(nameReference.getClass().getSimpleName());
+ sb.append(SEP);
+ sb.append(startTime);
+ sb.append(SEP);
+ sb.append(counter.incrementAndGet());
+
+ if(basePath.contains(U_PATTERN)) {
+ return basePath.replaceAll(U_PATTERN, sb.toString());
+ } else {
+ return basePath + sb.toString();
+ }
+ }
+
+ /**
+ * Get a unique ID with no base path
+ *
+ * @param nameReference The simple class name of that object is used as part of the
+ * generated unique ID
+ * @return path with a unique value for each call
+ */
+ public static String get(Object nameReference) {
+ return get(nameReference, null);
+ }
+}
diff --git a/src/main/java/org/apache/sling/testing/clients/util/XSSUtils.java b/src/main/java/org/apache/sling/testing/clients/util/XSSUtils.java
new file mode 100644
index 0000000..852cb12
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/util/XSSUtils.java
@@ -0,0 +1,137 @@
+/*
+ * 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.commons.lang3.StringEscapeUtils;
+import org.apache.sling.xss.XSSAPI;
+import org.apache.sling.xss.impl.XSSAPIImpl;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+
+/**
+ * Basic class for XSS Testing
+ * The reliability of these methods are not critical
+ */
+public class XSSUtils {
+
+ /**
+ * Use to ensure that HTTP query strings are in proper form, by escaping
+ * special characters such as spaces.
+ *
+ * @param urlString the string to be encoded
+ * @return the encoded string
+ */
+ public static String encodeUrl(String urlString) {
+ try {
+ return URLEncoder.encode(urlString, "UTF-8");
+ } catch (UnsupportedEncodingException ex) {
+ throw new RuntimeException("UTF-8 not supported", ex);
+ }
+ }
+
+ /**
+ * Use to encapsulate old-style escaping of HTML (using StringEscapeUtils).
+ * NB: newer code uses XSSAPI (based on OWASP's ESAPI).
+ *
+ * @param htmlString the string to be escaped
+ * @return the escaped string
+ */
+ public static String escapeHtml(String htmlString) {
+ return StringEscapeUtils.escapeHtml4(htmlString);
+ }
+
+ /**
+ * Use to encapsulate old-style escaping of XML (with JSTL encoding rules).
+ * NB: newer code uses XSSAPI (based on OWASP's ESAPI).
+ *
+ * @param xmlString the string to be escaped
+ * @return the escaped string
+ */
+ public static String escapeXml(String xmlString) {
+ String xssString = xmlString;
+ if (xmlString != null) {
+ xssString = xssString.replace(";", ";");
+ xssString = xssString.replace(" ", " ");
+ xssString = xssString.replace("'", "'");
+ xssString = xssString.replace("\"", """);
+ xssString = xssString.replace(">", ">");
+ xssString = xssString.replace("<", "<");
+ xssString = xssString.replace("/", "/");
+ xssString = xssString.replace("(", "(");
+ xssString = xssString.replace(")", ")");
+ xssString = xssString.replace(":", ":");
+ }
+ return xssString;
+ }
+
+ /**
+ * Use to encapsulate new-style (XSSAPI-based) encoding for HTML element content.
+ *
+ * @param source the string to be encoded
+ * @return the encoded string
+ */
+ public static String encodeForHTML(String source) {
+ XSSAPI xssAPI = new XSSAPIImpl();
+ return xssAPI.encodeForHTML(source);
+ }
+
+ /**
+ * Use to encapsulate new-style (XSSAPI-based) encoding for HTML attribute values.
+ *
+ * @param source the string to be encoded
+ * @return the encoded string
+ */
+ public static String encodeForHTMLAttr(String source) {
+ XSSAPI xssAPI = new XSSAPIImpl();
+ return xssAPI.encodeForHTMLAttr(source);
+ }
+
+ /**
+ * Use to encapsulate new-style (XSSAPI-based) encoding for XML element content.
+ *
+ * @param source the string to be encoded
+ * @return the encoded string
+ */
+ public static String encodeForXML(String source) {
+ XSSAPI xssAPI = new XSSAPIImpl();
+ return xssAPI.encodeForXML(source);
+ }
+
+ /**
+ * Use to encapsulate new-style (XSSAPI-based) encoding for XML attribute values.
+ *
+ * @param source the string to be encoded
+ * @return the encoded string
+ */
+ public static String encodeForXMLAttr(String source) {
+ XSSAPI xssAPI = new XSSAPIImpl();
+ return xssAPI.encodeForXMLAttr(source);
+ }
+
+ /**
+ * Use to encapsulate new-style (XSSAPI-based) encoding for JavaScript strings.
+ *
+ * @param source the string to be encoded
+ * @return the encoded string
+ */
+ public static String encodeForJSString(String source) {
+ XSSAPI xssAPI = new XSSAPIImpl();
+ return xssAPI.encodeForJSString(source);
+ }
+
+}
diff --git a/src/main/java/org/apache/sling/testing/clients/util/config/InstanceConfig.java b/src/main/java/org/apache/sling/testing/clients/util/config/InstanceConfig.java
new file mode 100644
index 0000000..8af55ac
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/util/config/InstanceConfig.java
@@ -0,0 +1,40 @@
+/*
+ * 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.config;
+
+/**
+ * Allows saving and restoring an instance configuration.
+ * Implementations define the behaviour of save() and restore()
+ */
+public interface InstanceConfig {
+
+ /**
+ * Saves the current status of the configuration
+ *
+ * @return this
+ * @throws InstanceConfigException if saving the configuration fails
+ */
+ public InstanceConfig save() throws InstanceConfigException;
+
+ /**
+ * Restores the saved status of the configuration
+ *
+ * @return this
+ * @throws InstanceConfigException if restoring the configuration fails
+ */
+ public InstanceConfig restore() throws InstanceConfigException;
+}
diff --git a/src/main/java/org/apache/sling/testing/clients/util/config/InstanceConfigCache.java b/src/main/java/org/apache/sling/testing/clients/util/config/InstanceConfigCache.java
new file mode 100644
index 0000000..b473c86
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/util/config/InstanceConfigCache.java
@@ -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.
+ */
+package org.apache.sling.testing.clients.util.config;
+
+import java.util.Collection;
+
+/**
+ * A cache for different {@link InstanceConfig} objects
+ */
+public interface InstanceConfigCache extends InstanceConfig, Collection<InstanceConfig> {
+}
diff --git a/src/main/java/org/apache/sling/testing/clients/util/config/InstanceConfigException.java b/src/main/java/org/apache/sling/testing/clients/util/config/InstanceConfigException.java
new file mode 100644
index 0000000..1c2597e
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/util/config/InstanceConfigException.java
@@ -0,0 +1,39 @@
+/*
+ * 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.config;
+
+public class InstanceConfigException extends Exception {
+
+ public InstanceConfigException(Exception e) {
+ super(e);
+ }
+
+ public InstanceConfigException() {
+ }
+
+ public InstanceConfigException(String message) {
+ super(message);
+ }
+
+ public InstanceConfigException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public InstanceConfigException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/src/main/java/org/apache/sling/testing/clients/util/config/impl/EmptyInstanceConfig.java b/src/main/java/org/apache/sling/testing/clients/util/config/impl/EmptyInstanceConfig.java
new file mode 100644
index 0000000..4801ad3
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/util/config/impl/EmptyInstanceConfig.java
@@ -0,0 +1,36 @@
+/*
+ * 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.config.impl;
+
+import org.apache.sling.testing.clients.util.config.InstanceConfig;
+import org.apache.sling.testing.clients.util.config.InstanceConfigException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class EmptyInstanceConfig implements InstanceConfig {
+ private static final Logger LOG = LoggerFactory.getLogger(EmptyInstanceConfig.class);
+
+ public InstanceConfig save() throws InstanceConfigException {
+ LOG.debug("Saved nothing");
+ return this;
+ }
+
+ public InstanceConfig restore() throws InstanceConfigException {
+ LOG.debug("Restored nothing");
+ return this;
+ }
+}
diff --git a/src/main/java/org/apache/sling/testing/clients/util/config/impl/InstanceConfigCacheImpl.java b/src/main/java/org/apache/sling/testing/clients/util/config/impl/InstanceConfigCacheImpl.java
new file mode 100644
index 0000000..4bae77f
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/util/config/impl/InstanceConfigCacheImpl.java
@@ -0,0 +1,120 @@
+/*
+ * 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.config.impl;
+
+import org.apache.sling.testing.clients.util.config.InstanceConfig;
+import org.apache.sling.testing.clients.util.config.InstanceConfigCache;
+import org.apache.sling.testing.clients.util.config.InstanceConfigException;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+public class InstanceConfigCacheImpl implements InstanceConfigCache {
+ List<InstanceConfig> configs;
+
+ public InstanceConfigCacheImpl(List<InstanceConfig> configs) {
+ this.configs = configs;
+ }
+
+ public InstanceConfigCacheImpl() {
+ this.configs = new ArrayList<InstanceConfig>();
+ }
+
+ @Override
+ public int size() {
+ return configs.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return configs.isEmpty();
+ }
+
+ @Override
+ public boolean contains(Object o) {
+ return configs.contains(o);
+ }
+
+ @Override
+ public Iterator<InstanceConfig> iterator() {
+ return configs.iterator();
+ }
+
+ @Override
+ public Object[] toArray() {
+ return configs.toArray();
+ }
+
+ @Override
+ public <T> T[] toArray(T[] a) {
+ return configs.toArray(a);
+ }
+
+ @Override
+ public boolean add(InstanceConfig instanceConfig) {
+ return configs.add(instanceConfig);
+ }
+
+ @Override
+ public boolean remove(Object o) {
+ return configs.remove(o);
+ }
+
+ @Override
+ public boolean containsAll(Collection<?> c) {
+ return configs.containsAll(c);
+ }
+
+ @Override
+ public boolean addAll(Collection<? extends InstanceConfig> c) {
+ return configs.addAll(c);
+ }
+
+ @Override
+ public boolean removeAll(Collection<?> c) {
+ return configs.removeAll(c);
+ }
+
+ @Override
+ public boolean retainAll(Collection<?> c) {
+ return configs.retainAll(c);
+ }
+
+ @Override
+ public void clear() {
+ configs.clear();
+ }
+
+
+ @Override
+ public InstanceConfig save() throws InstanceConfigException {
+ for (InstanceConfig ic : configs) {
+ ic.save();
+ }
+ return this;
+ }
+
+ @Override
+ public InstanceConfig restore() throws InstanceConfigException {
+ for (InstanceConfig ic : configs) {
+ ic.restore();
+ }
+ return this;
+ }
+}
diff --git a/src/main/java/org/apache/sling/testing/clients/util/config/package-info.java b/src/main/java/org/apache/sling/testing/clients/util/config/package-info.java
new file mode 100644
index 0000000..59d0aaa
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/util/config/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * 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("1.0.0")
+package org.apache.sling.testing.clients.util.config;
+
+import aQute.bnd.annotation.Version;
+
diff --git a/src/main/java/org/apache/sling/testing/clients/util/poller/AbstractPoller.java b/src/main/java/org/apache/sling/testing/clients/util/poller/AbstractPoller.java
new file mode 100644
index 0000000..57c34c3
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/util/poller/AbstractPoller.java
@@ -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.poller;
+
+public abstract class AbstractPoller implements Poller {
+
+ private final long waitInterval;
+ private final long waitCount;
+
+ /**
+ * Convenience method to execute a generic call and do polling until a condition is met
+ * The user must implement the {@link Poller#call()} and {@link Poller#condition()} methods
+ * @param waitInterval Number of milliseconds to wait between polls
+ * @param waitCount Number of wait intervals
+ */
+ public AbstractPoller(long waitInterval, long waitCount) {
+ this.waitInterval = waitInterval;
+ this.waitCount = waitCount;
+ }
+
+ /**
+ * Calls the {@link Poller#call()} once and then calls {@link Poller#condition()} until it returns true
+ * The method waits AbstractPoller#waitInterval milliseconds between calls to {@link Poller#condition()}
+ * A maximum of AbstractPoller#waitCount intervals are checked
+ * @return true if the condition is met after waiting a maximum of AbstractPoller#waitCount intervals, false otherwise
+ * @throws InterruptedException to mark this operation as "waiting"
+ */
+ public boolean callAndWait() throws InterruptedException {
+ if (!call()) return false;
+ for (int i=0; i<waitCount; i++) {
+ if (condition()) return true;
+ Thread.sleep(waitInterval);
+ }
+ return false;
+ }
+
+ /**
+ * Calls the @see: Poller#call() and then calls {@link Poller#condition()} until it returns true
+ * The Poller#call() method is called in each wait interval, before the Poller#condition().
+ * The method waits AbstractPoller#waitInterval milliseconds between calls to {@link Poller#condition()}
+ * A maximum of AbstractPoller#waitCount intervals are checked
+ * @return true if the condition is met after waiting a maximum of AbstractPoller#waitCount intervals, false otherwise
+ * @throws InterruptedException to mark this operation as "waiting"
+ */
+ public boolean callUntilCondition() throws InterruptedException {
+ if (!call()) return false;
+ if (condition()) return true;
+ for (int i = 0; i < waitCount; i++) {
+ Thread.sleep(waitInterval);
+ if (!call()) return false;
+ if (condition()) return true;
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/org/apache/sling/testing/clients/util/poller/Poller.java b/src/main/java/org/apache/sling/testing/clients/util/poller/Poller.java
new file mode 100644
index 0000000..e772edb
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/util/poller/Poller.java
@@ -0,0 +1,28 @@
+/*
+ * 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.poller;
+
+/**
+ * Abstract Poller interface.
+ * Provides simple methods to implement custom pollers
+ */
+public interface Poller {
+ boolean call();
+ boolean condition();
+ boolean callAndWait() throws InterruptedException;
+ boolean callUntilCondition() throws InterruptedException;
+}
diff --git a/src/main/java/org/apache/sling/testing/timeouts/TimeoutsProvider.java b/src/main/java/org/apache/sling/testing/timeouts/TimeoutsProvider.java
new file mode 100644
index 0000000..9fe0086
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/timeouts/TimeoutsProvider.java
@@ -0,0 +1,77 @@
+/*
+ * 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.timeouts;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Return timeout values that can be multiplied by a configurable
+ * factor. Useful to cope with slower integration testing systems:
+ * use timeout constants in your code that work for usual development
+ * systems, and set a multiplier when running on a slower system.
+ */
+public class TimeoutsProvider {
+ private static final Logger log = LoggerFactory.getLogger(TimeoutsProvider.class);
+ public static final String PROP_TIMEOUT_MULTIPLIER = "sling.testing.timeout.multiplier";
+ private static float timeoutFactor = -1;
+ private static TimeoutsProvider INSTANCE;
+
+ private TimeoutsProvider() {
+ if(timeoutFactor < 0) {
+ timeoutFactor = 1;
+ final String str = System.getProperty(PROP_TIMEOUT_MULTIPLIER);
+ if(str != null) {
+ try {
+ timeoutFactor = Float.valueOf(str.trim());
+ log.info("Timeout factor set to {} from system property {}",
+ timeoutFactor, PROP_TIMEOUT_MULTIPLIER);
+ } catch(NumberFormatException nfe) {
+ throw new IllegalStateException("Invalid timeout factor: " + PROP_TIMEOUT_MULTIPLIER + "=" + str);
+ }
+ }
+ }
+ }
+
+ public static TimeoutsProvider getInstance() {
+ if(INSTANCE == null) {
+ synchronized (TimeoutsProvider.class) {
+ INSTANCE = new TimeoutsProvider();
+ }
+ }
+ return INSTANCE;
+ }
+
+ public long getTimeout(long nomimalValue) {
+ final long result = (long)(nomimalValue * timeoutFactor);
+ return result;
+ }
+
+ public int getTimeout(int nomimalValue) {
+ final int result = (int)(nomimalValue * timeoutFactor);
+ return result;
+ }
+
+ /** Get timeout from a system property, with default value */
+ public int getTimeout(String systemPropertyName, int defaultNominalValue) {
+ int result = defaultNominalValue;
+ final String str = System.getProperty(systemPropertyName);
+ if(str != null) {
+ result = Integer.parseInt(str);
+ }
+ return getTimeout(result);
+ }
+}
diff --git a/src/test/java/org/apache/sling/testing/AbstractSlingClientGetPathTest.java b/src/test/java/org/apache/sling/testing/AbstractSlingClientGetPathTest.java
new file mode 100644
index 0000000..64f4c73
--- /dev/null
+++ b/src/test/java/org/apache/sling/testing/AbstractSlingClientGetPathTest.java
@@ -0,0 +1,162 @@
+/*
+ * 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;
+
+import org.apache.sling.testing.clients.ClientException;
+import org.apache.sling.testing.clients.SlingClient;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Arrays;
+import java.util.Collection;
+
+import static org.junit.Assert.assertEquals;
+
+@RunWith(Parameterized.class)
+public class AbstractSlingClientGetPathTest {
+
+ @Parameterized.Parameters(name = "{index} - serverUrl: {0}, input: {1}, expected: {2}")
+ public static Collection<String[]> data() {
+ return Arrays.asList(new String[][] {
+ {"http://HOST", "http://HOST/page.html", "/page.html"},
+ {"http://HOST", "http://HOST/my/page.html", "/my/page.html"},
+ {"http://HOST", "http://HOST/my/", "/my/"},
+ {"http://HOST", "http://HOST/my", "/my"},
+ {"http://HOST", "http://HOST/", "/"},
+ {"http://HOST", "http://HOST", "/"},
+ {"http://HOST", "/page.html", "/page.html"},
+ {"http://HOST", "/my/page.html", "/my/page.html"},
+ {"http://HOST", "/my/", "/my/"},
+ {"http://HOST", "/", "/"},
+ {"http://HOST", "page.html", "/page.html"},
+ {"http://HOST", "my/page.html", "/my/page.html"},
+ {"http://HOST", "my", "/my"},
+ {"http://HOST", "", "/"},
+
+ {"http://HOST:4502", "http://HOST:4502/page.html", "/page.html"},
+ {"http://HOST:4502", "http://HOST:4502/my/page.html", "/my/page.html"},
+ {"http://HOST:4502", "http://HOST:4502/my/", "/my/"},
+ {"http://HOST:4502", "http://HOST:4502/my", "/my"},
+ {"http://HOST:4502", "http://HOST:4502/", "/"},
+ {"http://HOST:4502", "http://HOST:4502", "/"},
+ {"http://HOST:4502", "/page.html", "/page.html"},
+ {"http://HOST:4502", "/my/page.html", "/my/page.html"},
+ {"http://HOST:4502", "/my/", "/my/"},
+ {"http://HOST:4502", "/my", "/my"},
+ {"http://HOST:4502", "/", "/"},
+ {"http://HOST:4502", "page.html", "/page.html"},
+ {"http://HOST:4502", "my/page.html", "/my/page.html"},
+ {"http://HOST:4502", "my/", "/my/"},
+ {"http://HOST:4502", "my", "/my"},
+ {"http://HOST:4502", "", "/"},
+
+ {"http://HOST:4502/", "http://HOST:4502/page.html", "/page.html"},
+ {"http://HOST:4502/", "http://HOST:4502/my/page.html", "/my/page.html"},
+ {"http://HOST:4502/", "http://HOST:4502/my/", "/my/"},
+ {"http://HOST:4502/", "http://HOST:4502/my", "/my"},
+ {"http://HOST:4502/", "http://HOST:4502/", "/"},
+ {"http://HOST:4502/", "http://HOST:4502", "/"},
+ {"http://HOST:4502/", "/page.html", "/page.html"},
+ {"http://HOST:4502/", "/my/page.html", "/my/page.html"},
+ {"http://HOST:4502/", "/my/", "/my/"},
+ {"http://HOST:4502/", "/my", "/my"},
+ {"http://HOST:4502/", "/", "/"},
+ {"http://HOST:4502/", "page.html", "/page.html"},
+ {"http://HOST:4502/", "my/page.html", "/my/page.html"},
+ {"http://HOST:4502/", "my/", "/my/"},
+ {"http://HOST:4502/", "my", "/my"},
+ {"http://HOST:4502/", "", "/"},
+
+ {"http://HOST:4502/CTX", "http://HOST:4502/CTX/page.html", "/page.html"},
+ {"http://HOST:4502/CTX", "http://HOST:4502/CTX/my/page.html", "/my/page.html"},
+ {"http://HOST:4502/CTX", "http://HOST:4502/CTX/my/", "/my/"},
+ {"http://HOST:4502/CTX", "http://HOST:4502/CTX/my", "/my"},
+ {"http://HOST:4502/CTX", "http://HOST:4502/CTX/", "/"},
+ {"http://HOST:4502/CTX", "http://HOST:4502/CTX", "/"},
+ {"http://HOST:4502/CTX", "/CTX", "/"},
+ {"http://HOST:4502/CTX", "/CTX/", "/"},
+ {"http://HOST:4502/CTX", "/CTX/page.html", "/page.html"},
+ {"http://HOST:4502/CTX", "/page.html", "/page.html"},
+ {"http://HOST:4502/CTX", "/my/page.html", "/my/page.html"},
+ {"http://HOST:4502/CTX", "/my/", "/my/"},
+ {"http://HOST:4502/CTX", "/my", "/my"},
+ {"http://HOST:4502/CTX", "/", "/"},
+ {"http://HOST:4502/CTX", "CTX", "/"},
+ {"http://HOST:4502/CTX", "CTX/", "/"},
+ {"http://HOST:4502/CTX", "CTX/page.html", "/page.html"},
+ {"http://HOST:4502/CTX", "page.html", "/page.html"},
+ {"http://HOST:4502/CTX", "my/page.html", "/my/page.html"},
+ {"http://HOST:4502/CTX", "my/", "/my/"},
+ {"http://HOST:4502/CTX", "my", "/my"},
+ {"http://HOST:4502/CTX", "", "/"},
+
+ {"http://HOST:4502/CTX/", "http://HOST:4502/CTX/page.html", "/page.html"},
+ {"http://HOST:4502/CTX/", "http://HOST:4502/CTX/my/page.html", "/my/page.html"},
+ {"http://HOST:4502/CTX/", "http://HOST:4502/CTX/my/", "/my/"},
+ {"http://HOST:4502/CTX/", "http://HOST:4502/CTX/my", "/my"},
+ {"http://HOST:4502/CTX/", "http://HOST:4502/CTX/", "/"},
+ {"http://HOST:4502/CTX/", "http://HOST:4502/CTX", "/"},
+ {"http://HOST:4502/CTX/", "/CTX", "/"},
+ {"http://HOST:4502/CTX/", "/CTX/", "/"},
+ {"http://HOST:4502/CTX/", "/CTX/page.html", "/page.html"},
+ {"http://HOST:4502/CTX/", "/page.html", "/page.html"},
+ {"http://HOST:4502/CTX/", "/my/page.html", "/my/page.html"},
+ {"http://HOST:4502/CTX/", "/my/", "/my/"},
+ {"http://HOST:4502/CTX/", "/my", "/my"},
+ {"http://HOST:4502/CTX/", "/", "/"},
+ {"http://HOST:4502/CTX/", "CTX", "/"},
+ {"http://HOST:4502/CTX/", "CTX/", "/"},
+ {"http://HOST:4502/CTX/", "CTX/page.html", "/page.html"},
+ {"http://HOST:4502/CTX/", "page.html", "/page.html"},
+ {"http://HOST:4502/CTX/", "my/page.html", "/my/page.html"},
+ {"http://HOST:4502/CTX/", "my/", "/my/"},
+ {"http://HOST:4502/CTX/", "my", "/my"},
+ {"http://HOST:4502/CTX/", "", "/"},
+
+ {"http://HOST:4502/CTX/", "http://www.google.com", "http://www.google.com"},
+ });
+ }
+
+ @Parameterized.Parameter(value = 0)
+ public String serverUrl;
+
+ @Parameterized.Parameter(value = 1)
+ public String inputUri;
+
+ @Parameterized.Parameter(value = 2)
+ public String expectedPath;
+
+ @Test
+ public void testGetPath() throws ClientException, URISyntaxException {
+ SlingClient c = new SlingClient(URI.create(serverUrl), "USER", "PWD");
+ assertEquals(URI.create(expectedPath), c.getPath(inputUri));
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/test/java/org/apache/sling/testing/AbstractSlingClientGetServerUrlTest.java b/src/test/java/org/apache/sling/testing/AbstractSlingClientGetServerUrlTest.java
new file mode 100644
index 0000000..bde2c13
--- /dev/null
+++ b/src/test/java/org/apache/sling/testing/AbstractSlingClientGetServerUrlTest.java
@@ -0,0 +1,56 @@
+/*
+ * 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;
+
+import org.apache.sling.testing.clients.ClientException;
+import org.apache.sling.testing.clients.SlingClient;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.net.URI;
+import java.util.Arrays;
+import java.util.Collection;
+
+import static org.junit.Assert.assertEquals;
+
+@RunWith(Parameterized.class)
+public class AbstractSlingClientGetServerUrlTest {
+
+ @Parameterized.Parameters(name = "{index} - serverUrl: {0}, path: {1}, expected: {2}")
+ public static Collection<String[]> data() {
+ return Arrays.asList(new String[][] {
+ {"http://HOST", "http://HOST/"},
+ {"http://HOST:4502", "http://HOST:4502/"},
+ {"http://HOST:4502/", "http://HOST:4502/"},
+ {"http://HOST:4502/CTX", "http://HOST:4502/CTX/"},
+ {"http://HOST:4502/CTX/", "http://HOST:4502/CTX/"},
+ });
+ }
+
+ @Parameterized.Parameter(value = 0)
+ public String serverUrl;
+
+ @Parameterized.Parameter(value = 1)
+ public String expectedUrl;
+
+ @Test
+ public void testGetUrl() throws ClientException {
+ SlingClient c = new SlingClient(URI.create(serverUrl), "USER", "PWD");
+ assertEquals("", URI.create(expectedUrl), c.getUrl());
+ }
+}
diff --git a/src/test/java/org/apache/sling/testing/AbstractSlingClientGetUrlTest.java b/src/test/java/org/apache/sling/testing/AbstractSlingClientGetUrlTest.java
new file mode 100644
index 0000000..665b595
--- /dev/null
+++ b/src/test/java/org/apache/sling/testing/AbstractSlingClientGetUrlTest.java
@@ -0,0 +1,122 @@
+/*
+ * 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;
+
+import org.apache.sling.testing.clients.ClientException;
+import org.apache.sling.testing.clients.SlingClient;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.net.URI;
+import java.util.Arrays;
+import java.util.Collection;
+
+import static org.junit.Assert.assertEquals;
+
+@RunWith(Parameterized.class)
+public class AbstractSlingClientGetUrlTest {
+
+ @Parameterized.Parameters(name = "{index} - serverUrl: {0}, path: {1}, expected: {2}")
+ public static Collection<String[]> data() {
+ return Arrays.asList(new String[][] {
+ // Server URL with no port
+ {"http://HOST", "/page.html", "http://HOST/page.html"},
+ {"http://HOST", "/my/page.html", "http://HOST/my/page.html"},
+ {"http://HOST", "/my/", "http://HOST/my/"},
+ {"http://HOST", "/my", "http://HOST/my"},
+ {"http://HOST", "/", "http://HOST/"},
+
+ {"http://HOST", "page.html", "http://HOST/page.html"},
+ {"http://HOST", "my/page.html", "http://HOST/my/page.html"},
+ {"http://HOST", "my/", "http://HOST/my/"},
+ {"http://HOST", "my", "http://HOST/my"},
+ {"http://HOST", "", "http://HOST/"},
+
+ // Server URL with with port
+ {"http://HOST:4502", "/page.html", "http://HOST:4502/page.html"},
+ {"http://HOST:4502", "/my/page.html", "http://HOST:4502/my/page.html"},
+ {"http://HOST:4502", "/my/", "http://HOST:4502/my/"},
+ {"http://HOST:4502", "/my", "http://HOST:4502/my"},
+ {"http://HOST:4502", "/", "http://HOST:4502/"},
+
+ {"http://HOST:4502", "page.html", "http://HOST:4502/page.html"},
+ {"http://HOST:4502", "my/page.html", "http://HOST:4502/my/page.html"},
+ {"http://HOST:4502", "my/", "http://HOST:4502/my/"},
+ {"http://HOST:4502", "my", "http://HOST:4502/my"},
+ {"http://HOST:4502", "", "http://HOST:4502/"},
+
+ // Server URL with with port and trailing slash
+ {"http://HOST:4502/", "/page.html", "http://HOST:4502/page.html"},
+ {"http://HOST:4502/", "/my/page.html", "http://HOST:4502/my/page.html"},
+ {"http://HOST:4502/", "/my/", "http://HOST:4502/my/"},
+ {"http://HOST:4502/", "/my", "http://HOST:4502/my"},
+ {"http://HOST:4502/", "/", "http://HOST:4502/"},
+
+ {"http://HOST:4502/", "page.html", "http://HOST:4502/page.html"},
+ {"http://HOST:4502/", "my/page.html", "http://HOST:4502/my/page.html"},
+ {"http://HOST:4502/", "my/", "http://HOST:4502/my/"},
+ {"http://HOST:4502/", "my", "http://HOST:4502/my"},
+ {"http://HOST:4502/", "", "http://HOST:4502/"},
+
+ // Server URL with with port and context path (no trailing slash)
+ {"http://HOST:4502/CTX", "/page.html", "http://HOST:4502/CTX/page.html"},
+ {"http://HOST:4502/CTX", "/my/page.html", "http://HOST:4502/CTX/my/page.html"},
+ {"http://HOST:4502/CTX", "/my/", "http://HOST:4502/CTX/my/"},
+ {"http://HOST:4502/CTX", "/my", "http://HOST:4502/CTX/my"},
+ {"http://HOST:4502/CTX", "/", "http://HOST:4502/CTX/"},
+
+ {"http://HOST:4502/CTX", "page.html", "http://HOST:4502/CTX/page.html"},
+ {"http://HOST:4502/CTX", "my/page.html", "http://HOST:4502/CTX/my/page.html"},
+ {"http://HOST:4502/CTX", "my/", "http://HOST:4502/CTX/my/"},
+ {"http://HOST:4502/CTX", "my", "http://HOST:4502/CTX/my"},
+ {"http://HOST:4502/CTX", "", "http://HOST:4502/CTX/"},
+
+ // Server URL with with port and context path and trailing slash
+ {"http://HOST:4502/CTX/", "/page.html", "http://HOST:4502/CTX/page.html"},
+ {"http://HOST:4502/CTX/", "/my/page.html", "http://HOST:4502/CTX/my/page.html"},
+ {"http://HOST:4502/CTX/", "/my/", "http://HOST:4502/CTX/my/"},
+ {"http://HOST:4502/CTX/", "/my", "http://HOST:4502/CTX/my"},
+ {"http://HOST:4502/CTX/", "/", "http://HOST:4502/CTX/"},
+
+ {"http://HOST:4502/CTX/", "page.html", "http://HOST:4502/CTX/page.html"},
+ {"http://HOST:4502/CTX/", "my/page.html", "http://HOST:4502/CTX/my/page.html"},
+ {"http://HOST:4502/CTX/", "my/", "http://HOST:4502/CTX/my/"},
+ {"http://HOST:4502/CTX/", "my", "http://HOST:4502/CTX/my"},
+ {"http://HOST:4502/CTX/", "", "http://HOST:4502/CTX/"},
+
+ // External URLs
+ {"http://HOST:4502/CTX/", "http://www.google.com", "http://www.google.com"},
+ {"http://HOST:4502/CTX/", "http://HOST:4502/CTX/my/page.html", "http://HOST:4502/CTX/my/page.html"},
+ });
+ }
+
+ @Parameterized.Parameter(value = 0)
+ public String serverUrl;
+
+ @Parameterized.Parameter(value = 1)
+ public String inputPath;
+
+ @Parameterized.Parameter(value = 2)
+ public String expectedUrl;
+
+ @Test
+ public void testGetUrlWithParam() throws ClientException {
+ SlingClient c = new SlingClient(URI.create(serverUrl), "USER", "PWD");
+ assertEquals("", URI.create(expectedUrl), c.getUrl(inputPath));
+ }
+}
diff --git a/src/test/java/org/apache/sling/testing/DelayRequestInterceptorTest.java b/src/test/java/org/apache/sling/testing/DelayRequestInterceptorTest.java
new file mode 100644
index 0000000..c44d238
--- /dev/null
+++ b/src/test/java/org/apache/sling/testing/DelayRequestInterceptorTest.java
@@ -0,0 +1,34 @@
+/*
+ * 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;
+
+import org.apache.sling.testing.clients.interceptors.DelayRequestInterceptor;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class DelayRequestInterceptorTest {
+
+ @Test
+ public void testDelay() throws Exception {
+ DelayRequestInterceptor interceptor = new DelayRequestInterceptor(1000);
+ long before = System.currentTimeMillis();
+ interceptor.process(null, null);
+ long after = System.currentTimeMillis();
+ Assert.assertTrue(after - before >= 1000);
+ }
+
+}
diff --git a/src/test/java/org/apache/sling/testing/util/UniquePathsTest.java b/src/test/java/org/apache/sling/testing/util/UniquePathsTest.java
new file mode 100644
index 0000000..c7e40e1
--- /dev/null
+++ b/src/test/java/org/apache/sling/testing/util/UniquePathsTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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.util;
+
+import org.apache.sling.testing.clients.util.UniquePaths;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.lang.reflect.Field;
+import java.util.concurrent.atomic.AtomicLong;
+
+import static org.junit.Assert.assertEquals;
+
+public class UniquePathsTest {
+
+ @Before
+ public void setup() throws Exception {
+ // Set known startTime and counter values for tests
+ {
+ final Field f = UniquePaths.class.getDeclaredField("startTime");
+ f.setAccessible(true);
+ f.set(UniquePaths.class, 1234L);
+ }
+ {
+ final Field f = UniquePaths.class.getDeclaredField("counter");
+ f.setAccessible(true);
+ f.set(UniquePaths.class, new AtomicLong(9362L));
+ }
+ }
+
+ @Test
+ public void testNoUPattern() {
+ assertEquals("/tmp/UniquePathsTest_1234_9363", UniquePaths.get(this, "/tmp/"));
+ assertEquals("/bar/UniquePathsTest_1234_9364", UniquePaths.get(this, "/bar/"));
+ }
+
+ @Test
+ public void testSingleUPattern() {
+ assertEquals("/tmp/UniquePathsTest_1234_9363/foo", UniquePaths.get(this, "/tmp/_UNIQ_/foo"));
+ }
+
+ @Test
+ public void testMultipleUPattern() {
+ assertEquals(
+ "/tmp/UniquePathsTest_1234_9363/foo/UniquePathsTest_1234_9363.html",
+ UniquePaths.get(this, "/tmp/_UNIQ_/foo/_UNIQ_.html"));
+ }
+
+ @Test
+ public void testNullPattern() {
+ assertEquals(
+ "UniquePathsTest_1234_9363",
+ UniquePaths.get(this, null));
+ }
+
+ @Test
+ public void testNoPattern() {
+ assertEquals(
+ "UniquePathsTest_1234_9363",
+ UniquePaths.get(this));
+ }
+}
diff --git a/src/test/java/org/apache/sling/testing/util/poller/AbstractPollerTest.java b/src/test/java/org/apache/sling/testing/util/poller/AbstractPollerTest.java
new file mode 100644
index 0000000..c7681a3
--- /dev/null
+++ b/src/test/java/org/apache/sling/testing/util/poller/AbstractPollerTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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.util.poller;
+
+import org.apache.sling.testing.clients.util.poller.AbstractPoller;
+import org.junit.Assert;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class AbstractPollerTest {
+ private static final Logger LOG = LoggerFactory.getLogger(AbstractPollerTest.class);
+
+ @Test
+ public void testCallAndWaitSuccess() throws InterruptedException {
+ AbstractPoller poller = new AbstractPoller(100, 5) {
+ int callNumber = 0;
+
+ @Override
+ public boolean call() {
+ return true;
+ }
+
+ @Override
+ public boolean condition() {
+ callNumber += 1;
+ LOG.debug("Call nr " + callNumber);
+ if (callNumber == 4) {
+ return true;
+ }
+ return false;
+ }
+ };
+ Assert.assertTrue(poller.callAndWait());
+ }
+
+ @Test
+ public void testCallAndWaitFailure() throws InterruptedException {
+ AbstractPoller poller = new AbstractPoller(100, 5) {
+ @Override
+ public boolean call() {
+ return true;
+ }
+
+ @Override
+ public boolean condition() {
+ return false;
+ }
+ };
+ Assert.assertFalse(poller.callAndWait());
+ }
+
+ @Test
+ public void testCallUntilSuccess() throws InterruptedException {
+ AbstractPoller poller = new AbstractPoller(100, 5) {
+ int callNumber = 0;
+
+ @Override
+ public boolean call() {
+ callNumber += 1;
+ LOG.debug("Call nr " + callNumber);
+ return true;
+ }
+
+ @Override
+ public boolean condition() {
+ if (callNumber == 4) {
+ return true;
+ }
+ return false;
+ }
+ };
+ Assert.assertTrue(poller.callUntilCondition());
+ }
+
+ @Test
+ public void testCallUntilFailure() throws InterruptedException {
+ AbstractPoller poller = new AbstractPoller(100, 5) {
+ @Override
+ public boolean call() {
+ return true;
+ }
+
+ @Override
+ public boolean condition() {
+ return false;
+ }
+ };
+ Assert.assertFalse(poller.callUntilCondition());
+ }
+
+
+}
--
To stop receiving notification emails like this one, please contact
"commits@sling.apache.org" <co...@sling.apache.org>.