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(";", "&#x3b;");
+            xssString = xssString.replace(" ", "&#x20;");
+            xssString = xssString.replace("'", "&#x27;");
+            xssString = xssString.replace("\"", "&quot;");
+            xssString = xssString.replace(">", "&gt;");
+            xssString = xssString.replace("<", "&lt;");
+            xssString = xssString.replace("/", "&#x2f;");
+            xssString = xssString.replace("(", "&#x28;");
+            xssString = xssString.replace(")", "&#x29;");
+            xssString = xssString.replace(":", "&#x3a;");
+        }
+        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>.