You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by bd...@apache.org on 2016/04/29 16:03:33 UTC

svn commit: r1741632 [2/4] - in /sling/trunk/testing/http: ./ clients/ clients/src/ clients/src/main/ clients/src/main/java/ clients/src/main/java/org/ clients/src/main/java/org/apache/ clients/src/main/java/org/apache/sling/ clients/src/main/java/org/...

Added: sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/SlingClient.java
URL: http://svn.apache.org/viewvc/sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/SlingClient.java?rev=1741632&view=auto
==============================================================================
--- sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/SlingClient.java (added)
+++ sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/SlingClient.java Fri Apr 29 14:03:32 2016
@@ -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);
+        }
+    }
+}

Added: sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/SlingClientConfig.java
URL: http://svn.apache.org/viewvc/sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/SlingClientConfig.java?rev=1741632&view=auto
==============================================================================
--- sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/SlingClientConfig.java (added)
+++ sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/SlingClientConfig.java Fri Apr 29 14:03:32 2016
@@ -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);
+        }
+    }
+}

Added: sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/SlingHttpResponse.java
URL: http://svn.apache.org/viewvc/sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/SlingHttpResponse.java?rev=1741632&view=auto
==============================================================================
--- sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/SlingHttpResponse.java (added)
+++ sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/SlingHttpResponse.java Fri Apr 29 14:03:32 2016
@@ -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();
+    }
+}

Added: sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/instance/InstanceConfiguration.java
URL: http://svn.apache.org/viewvc/sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/instance/InstanceConfiguration.java?rev=1741632&view=auto
==============================================================================
--- sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/instance/InstanceConfiguration.java (added)
+++ sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/instance/InstanceConfiguration.java Fri Apr 29 14:03:32 2016
@@ -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

Added: sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/instance/InstanceSetup.java
URL: http://svn.apache.org/viewvc/sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/instance/InstanceSetup.java?rev=1741632&view=auto
==============================================================================
--- sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/instance/InstanceSetup.java (added)
+++ sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/instance/InstanceSetup.java Fri Apr 29 14:03:32 2016
@@ -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;
+    }
+}

Added: sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/interceptors/DelayRequestInterceptor.java
URL: http://svn.apache.org/viewvc/sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/interceptors/DelayRequestInterceptor.java?rev=1741632&view=auto
==============================================================================
--- sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/interceptors/DelayRequestInterceptor.java (added)
+++ sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/interceptors/DelayRequestInterceptor.java Fri Apr 29 14:03:32 2016
@@ -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();
+        }
+    }
+
+}

Added: sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/interceptors/StickyCookieHolder.java
URL: http://svn.apache.org/viewvc/sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/interceptors/StickyCookieHolder.java?rev=1741632&view=auto
==============================================================================
--- sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/interceptors/StickyCookieHolder.java (added)
+++ sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/interceptors/StickyCookieHolder.java Fri Apr 29 14:03:32 2016
@@ -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();
+    }
+}

Added: sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/interceptors/StickyCookieInterceptor.java
URL: http://svn.apache.org/viewvc/sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/interceptors/StickyCookieInterceptor.java?rev=1741632&view=auto
==============================================================================
--- sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/interceptors/StickyCookieInterceptor.java (added)
+++ sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/interceptors/StickyCookieInterceptor.java Fri Apr 29 14:03:32 2016
@@ -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

Added: sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/interceptors/StickyCookieSpec.java
URL: http://svn.apache.org/viewvc/sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/interceptors/StickyCookieSpec.java?rev=1741632&view=auto
==============================================================================
--- sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/interceptors/StickyCookieSpec.java (added)
+++ sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/interceptors/StickyCookieSpec.java Fri Apr 29 14:03:32 2016
@@ -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

Added: sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/interceptors/TestDescriptionHolder.java
URL: http://svn.apache.org/viewvc/sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/interceptors/TestDescriptionHolder.java?rev=1741632&view=auto
==============================================================================
--- sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/interceptors/TestDescriptionHolder.java (added)
+++ sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/interceptors/TestDescriptionHolder.java Fri Apr 29 14:03:32 2016
@@ -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();
+    }
+}

Added: sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/interceptors/TestDescriptionInterceptor.java
URL: http://svn.apache.org/viewvc/sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/interceptors/TestDescriptionInterceptor.java?rev=1741632&view=auto
==============================================================================
--- sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/interceptors/TestDescriptionInterceptor.java (added)
+++ sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/interceptors/TestDescriptionInterceptor.java Fri Apr 29 14:03:32 2016
@@ -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());
+    }
+}

Added: sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/interceptors/package-info.java
URL: http://svn.apache.org/viewvc/sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/interceptors/package-info.java?rev=1741632&view=auto
==============================================================================
--- sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/interceptors/package-info.java (added)
+++ sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/interceptors/package-info.java Fri Apr 29 14:03:32 2016
@@ -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;
+

Added: sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/osgi/Bundle.java
URL: http://svn.apache.org/viewvc/sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/osgi/Bundle.java?rev=1741632&view=auto
==============================================================================
--- sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/osgi/Bundle.java (added)
+++ sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/osgi/Bundle.java Fri Apr 29 14:03:32 2016
@@ -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

Added: sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/osgi/BundleInfo.java
URL: http://svn.apache.org/viewvc/sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/osgi/BundleInfo.java?rev=1741632&view=auto
==============================================================================
--- sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/osgi/BundleInfo.java (added)
+++ sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/osgi/BundleInfo.java Fri Apr 29 14:03:32 2016
@@ -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;
+    }
+
+}

Added: sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/osgi/BundlesInfo.java
URL: http://svn.apache.org/viewvc/sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/osgi/BundlesInfo.java?rev=1741632&view=auto
==============================================================================
--- sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/osgi/BundlesInfo.java (added)
+++ sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/osgi/BundlesInfo.java Fri Apr 29 14:03:32 2016
@@ -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