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/05/13 14:02:04 UTC

svn commit: r1743676 - in /sling/trunk/testing/http/clients: ./ src/main/java/org/apache/sling/testing/clients/osgi/ src/main/java/org/apache/sling/testing/clients/util/poller/

Author: bdelacretaz
Date: Fri May 13 14:02:03 2016
New Revision: 1743676

URL: http://svn.apache.org/viewvc?rev=1743676&view=rev
Log:
SLING-5725 - Remove o.a.s.testing.tools dependency in o.a.s.testing.clients and merge OSGi console clients - contribued by Andrei Dulvac, thanks!

Added:
    sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/util/poller/PathPoller.java
Removed:
    sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/osgi/WebconsoleClient.java
Modified:
    sling/trunk/testing/http/clients/pom.xml
    sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/osgi/BundlesInstaller.java
    sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/osgi/OsgiConsoleClient.java

Modified: sling/trunk/testing/http/clients/pom.xml
URL: http://svn.apache.org/viewvc/sling/trunk/testing/http/clients/pom.xml?rev=1743676&r1=1743675&r2=1743676&view=diff
==============================================================================
--- sling/trunk/testing/http/clients/pom.xml (original)
+++ sling/trunk/testing/http/clients/pom.xml Fri May 13 14:02:03 2016
@@ -69,17 +69,6 @@
 
     <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>
@@ -144,5 +133,18 @@
             <artifactId>org.apache.sling.xss</artifactId>
             <version>1.0.4</version>
         </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.commons.json</artifactId>
+            <version>2.0.16</version>
+        </dependency>
+
+        <!-- For tests -->
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>4.12</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 </project>

Modified: sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/osgi/BundlesInstaller.java
URL: http://svn.apache.org/viewvc/sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/osgi/BundlesInstaller.java?rev=1743676&r1=1743675&r2=1743676&view=diff
==============================================================================
--- sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/osgi/BundlesInstaller.java (original)
+++ sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/osgi/BundlesInstaller.java Fri May 13 14:02:03 2016
@@ -16,171 +16,202 @@
  */
 package org.apache.sling.testing.clients.osgi;
 
-import org.osgi.framework.Constants;
+import org.apache.sling.testing.clients.ClientException;
+import org.apache.sling.testing.clients.util.poller.AbstractPoller;
 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 */ 
+/**
+ * Utility for installing and starting additional bundles for testing
+ */
 public class BundlesInstaller {
     private final Logger log = LoggerFactory.getLogger(getClass());
-    private final WebconsoleClient webconsoleClient;
+    private final OsgiConsoleClient osgiConsoleClient;
     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);
+
+    public BundlesInstaller(OsgiConsoleClient cc) {
+        osgiConsoleClient = cc;
+    }
+
+    /**
+     * Checks if a bundle is installed or not. Does not retry.
+     * @param bundleFile
+     * @return
+     * @throws ClientException
+     * @throws IOException
+     * @throws InterruptedException
+     */
+    public boolean isInstalled(File bundleFile) throws ClientException, InterruptedException, IOException {
+        final String bundleSymbolicName = OsgiConsoleClient.getBundleSymbolicName(bundleFile);
+        log.debug("Checking if installed: " + bundleSymbolicName);
+        boolean installed = osgiConsoleClient.checkBundleInstalled(bundleSymbolicName, 1000, 1);
+        // if this succeeds, then there's no need to install again
+        if (installed) {
+            log.debug("Already installed: " + bundleSymbolicName);
             return true;
-        } catch(AssertionError e) {
-            log.debug("Not yet installed: "+bundleSymbolicName);
+        } else {
+            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);
+
+    /**
+     * Check if the installed version matches the one of the bundle (file)
+     * @param bundleFile
+     * @return
+     * @throws Exception
+     */
+    public boolean isInstalledWithSameVersion(File bundleFile) throws ClientException, IOException {
+        final String bundleSymbolicName = OsgiConsoleClient.getBundleSymbolicName(bundleFile);
+        final String versionOnServer = osgiConsoleClient.getBundleVersion(bundleSymbolicName);
+        final String versionInBundle = OsgiConsoleClient.getBundleVersionFromFile(bundleFile);
         if (versionOnServer.equals(versionInBundle)) {
             return true;
         } else {
-            log.info("Bundle installed doesn't match: "+bundleSymbolicName+
-                    ", versionOnServer="+versionOnServer+", versionInBundle="+versionInBundle);
+            log.warn("Installed bundle doesn't match: {}, versionOnServer={}, versionInBundle={}",
+                    bundleSymbolicName, versionOnServer, versionInBundle);
             return false;
         }
     }
-    
-    /** Install a list of bundles supplied as Files */
-    public void installBundles(List<File> toInstall, boolean startBundles) throws Exception {
+
+    /**
+     * Install a list of bundles supplied as Files
+     * @param toInstall
+     * @param startBundles
+     * @throws Exception
+     */
+    public void installBundles(List<File> toInstall, boolean startBundles) throws ClientException, IOException, InterruptedException {
         for(File f : toInstall) {
-            final String bundleSymbolicName = getBundleSymbolicName(f);
+            final String bundleSymbolicName = OsgiConsoleClient.getBundleSymbolicName(f);
             if (isInstalled(f)) {
                 if (f.getName().contains("SNAPSHOT")) {
                     log.info("Reinstalling (due to SNAPSHOT version): {}", bundleSymbolicName);
-                    webconsoleClient.uninstallBundle(bundleSymbolicName, f);
+                    osgiConsoleClient.uninstallBundle(bundleSymbolicName);
                 } else if (!isInstalledWithSameVersion(f)) {
                     log.info("Reinstalling (due to version mismatch): {}", bundleSymbolicName);
-                    webconsoleClient.uninstallBundle(bundleSymbolicName, f);
+                    osgiConsoleClient.uninstallBundle(bundleSymbolicName);
                 } else {
                     log.info("Not reinstalling: {}", bundleSymbolicName);
                     continue;
                 }
             }
-            webconsoleClient.installBundle(f, startBundles);
+            osgiConsoleClient.installBundle(f, startBundles);
             log.info("Installed: {}", bundleSymbolicName);
         }
-        
+
         // ensure that bundles are re-wired esp. if an existing bundle was updated
-        webconsoleClient.refreshPackages();
+        osgiConsoleClient.refreshPackages();
 
         log.info("{} additional bundles installed", toInstall.size());
     }
-    
-    /** Uninstall a list of bundles supplied as Files */
-    public void uninstallBundles(List<File> toUninstall) throws Exception {
+
+    /**
+     * Uninstall a list of bundles supplied as Files
+     * @param toUninstall
+     * @throws ClientException
+     * @throws IOException
+     * @throws InterruptedException
+     */
+    public void uninstallBundles(List<File> toUninstall) throws ClientException, IOException, InterruptedException {
         for(File f : toUninstall) {
-            final String bundleSymbolicName = getBundleSymbolicName(f);
+            final String bundleSymbolicName = OsgiConsoleClient.getBundleSymbolicName(f);
             if (isInstalled(f)) {
                 log.info("Uninstalling bundle: {}", bundleSymbolicName);
-                webconsoleClient.uninstallBundle(bundleSymbolicName, f);
+                osgiConsoleClient.uninstallBundle(bundleSymbolicName);
             } 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();
+        osgiConsoleClient.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);
-        }
+
+
+    /**
+     * Wait for all bundles specified in symbolicNames list to be installed in the OSGi web console.
+     * @param symbolicNames the list of names for the bundles
+     * @param timeoutSeconds how many seconds to wait
+     * @return
+     * @throws Exception
+     */
+    public boolean waitForBundlesInstalled(List<String> symbolicNames, int timeoutSeconds) throws ClientException, InterruptedException {
+        log.info("Checking that the following bundles are installed (timeout {} seconds): {}", timeoutSeconds, symbolicNames);
+        for (String symbolicName : symbolicNames) {
+            boolean started = osgiConsoleClient.checkBundleInstalled(symbolicName, 500, 2 * timeoutSeconds);
+            if (!started) return false;
+        }
+        return true;
     }
-    
-    public void startAllBundles(List<String> symbolicNames, int timeoutSeconds) throws Exception {
+
+    /**
+     * Start all the bundles in a {{List}}
+     * @param symbolicNames the list of bundles to start
+     * @param timeoutSeconds number of seconds until it times out
+     * @throws ClientException
+     * @throws InterruptedException
+     */
+    public void startAllBundles(final List<String> symbolicNames, int timeoutSeconds) throws ClientException, InterruptedException {
         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;
+        class StartAllBundlesPoller extends AbstractPoller {
+            private ClientException exception;
+            public StartAllBundlesPoller(List<String> symbolicNames, long waitInterval, long waitCount) {
+                super(waitInterval, waitCount);
+            }
+
+            @Override
+            public boolean call() {
+                for (String bundle : symbolicNames) {
+                    final String state;
+                    try {
+                        state = osgiConsoleClient.getBundleState(bundle);
+                        if (!state.equalsIgnoreCase(ACTIVE_STATE)) {
+                            osgiConsoleClient.startBundle(bundle);
+                        }
+                    } catch (ClientException e) {
+                        this.exception = e;
+                        return false;
+                    }
+                }
+                return true;
+            }
+
+            @Override
+            public boolean condition() {
+                for (String bundle : symbolicNames) {
+                    final String state;
+                    try {
+                        state = osgiConsoleClient.getBundleState(bundle);
+                        if (!state.equalsIgnoreCase(ACTIVE_STATE)) {
+                            return false;
+                        }
+                    } catch (ClientException e) {
+                        this.exception = e;
+                        return false;
+                    }
                 }
+                return true;
+            }
+
+            public ClientException getException() {
+                return exception;
             }
-            
-            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;
+        StartAllBundlesPoller poller = new StartAllBundlesPoller(symbolicNames, 1000, timeoutSeconds);
+        if (!poller.callUntilCondition()) {
+            throw new ClientException("Some bundles did not start or timed out", poller.getException());
+        }
+
     }
+
+
+
+
 }
\ No newline at end of file

Modified: sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/osgi/OsgiConsoleClient.java
URL: http://svn.apache.org/viewvc/sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/osgi/OsgiConsoleClient.java?rev=1743676&r1=1743675&r2=1743676&view=diff
==============================================================================
--- sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/osgi/OsgiConsoleClient.java (original)
+++ sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/osgi/OsgiConsoleClient.java Fri May 13 14:02:03 2016
@@ -18,7 +18,11 @@
 package org.apache.sling.testing.clients.osgi;
 
 import org.apache.http.Header;
+import org.apache.http.entity.mime.MultipartEntityBuilder;
 import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.sling.commons.json.JSONArray;
+import org.apache.sling.commons.json.JSONException;
+import org.apache.sling.commons.json.JSONObject;
 import org.apache.sling.testing.clients.ClientException;
 import org.apache.sling.testing.clients.SlingHttpResponse;
 import org.apache.sling.testing.clients.util.JsonUtils;
@@ -27,18 +31,27 @@ import org.apache.sling.testing.clients.
 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.apache.sling.testing.clients.util.poller.PathPoller;
 import org.codehaus.jackson.JsonNode;
+import org.osgi.framework.Constants;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
 import java.net.URI;
 import java.util.*;
+import java.util.jar.JarInputStream;
+import java.util.jar.Manifest;
 
 import static org.apache.http.HttpStatus.SC_MOVED_TEMPORARILY;
 import static org.apache.http.HttpStatus.SC_OK;
 
 /**
  * A client that wraps the Felix OSGi Web Console REST API calls.
+ * @see <a href=http://felix.apache.org/documentation/subprojects/apache-felix-web-console/web-console-restful-api.html>
+ *     Web Console RESTful API</a>
  */
 public class OsgiConsoleClient extends SlingClient {
 
@@ -63,6 +76,12 @@ public class OsgiConsoleClient extends S
      */
     private final String URL_COMPONENTS = CONSOLE_ROOT_URL + "/components";
 
+
+    public static final String JSON_KEY_ID = "id";
+    public static final String JSON_KEY_VERSION = "version";
+    public static final String JSON_KEY_DATA = "data";
+    public static final String JSON_KEY_STATE = "state";
+
     /**
      * Default constructor. Simply calls {@link SlingClient#SlingClient(URI, String, String)}
      *
@@ -141,6 +160,10 @@ public class OsgiConsoleClient extends S
         return new ComponentInfo(JsonUtils.getJsonNodeFromString(resp.getContent()));
     }
 
+    //
+    // OSGi configurations
+    //
+
     /**
      * Returns a map of all properties set for the config referenced by the PID, where the map keys
      * are the property names.
@@ -276,16 +299,282 @@ public class OsgiConsoleClient extends S
      *
      * @param pid pid
      * @param expectedStatus expected response status
+     * @return the sling response
      * @throws ClientException if the response status does not match any of the expectedStatus
      */
-    public void deleteConfiguration(String pid, int... expectedStatus) throws ClientException {
+    public SlingHttpResponse deleteConfiguration(String pid, int... expectedStatus) throws ClientException {
         FormEntityBuilder builder = FormEntityBuilder.create();
         builder.addParameter("apply", "1");
         builder.addParameter("delete", "1");
         // make the request
         SlingHttpResponse resp = this.doPost(URL_CONFIGURATION + "/" + pid, builder.build());
-        // check the returned status         
+        // check the returned status
         HttpUtils.verifyHttpStatus(resp, HttpUtils.getExpectedStatus(200, expectedStatus));
+        return resp;
+    }
+
+    //
+    // Bundles
+    //
+
+    /**
+     * Uninstall a bundle
+     * @param symbolicName
+     * @return the sling response
+     * @throws ClientException
+     */
+    public SlingHttpResponse uninstallBundle(String symbolicName) throws ClientException {
+        final long bundleId = getBundleId(symbolicName);
+        LOG.info("Uninstalling bundle {} with bundleId {}", symbolicName, bundleId);
+        FormEntityBuilder builder = FormEntityBuilder.create();
+        builder.addParameter("action", "uninstall");
+        return this.doPost(getBundlePath(symbolicName), builder.build(), 200);
+    }
+
+    /**
+     * Install a bundle using the Felix webconsole HTTP interface
+     * @param f the bundle file
+     * @param startBundle whether to start the bundle or not
+     * @return the sling response
+     * @throws ClientException
+     */
+    public SlingHttpResponse installBundle(File f, boolean startBundle) throws ClientException {
+        return installBundle(f, startBundle, 0);
+    }
+
+    /**
+     * Install a bundle using the Felix webconsole HTTP interface, with a specific start level
+     * @param f
+     * @param startBundle
+     * @param startLevel
+     * @return the sling response
+     * @throws ClientException
+     */
+    public SlingHttpResponse installBundle(File f, boolean startBundle, int startLevel) throws ClientException {
+        // Setup request for Felix Webconsole bundle install
+        MultipartEntityBuilder builder = MultipartEntityBuilder.create()
+                .addTextBody("action", "install")
+                .addBinaryBody("bundlefile", f);
+        if (startBundle) {
+            builder.addTextBody("bundlestart", "true");
+        }
+        if (startLevel > 0) {
+            builder.addTextBody("bundlestartlevel", String.valueOf(startLevel));
+            LOG.info("Installing bundle {} at start level {}", f.getName(), startLevel);
+        } else {
+            LOG.info("Installing bundle {} at default start level", f.getName());
+        }
+
+        return this.doPost(URL_BUNDLES, builder.build(), 302);
+
+    }
+
+    /**
+     * Install a bundle using the Felix webconsole HTTP interface and wait for it to be installed
+     * @param f the bundle file
+     * @param startBundle whether to start the bundle or not
+     * @param startLevel the start level of the bundle. negative values mean default start level
+     * @param waitTime how long to wait between retries of checking the bundle
+     * @param retries how many times to check for the bundle to be installed, until giving up
+     * @return true if the bundle was successfully installed, false otherwise
+     * @throws ClientException
+     */
+    public boolean installBundleWithRetry(File f, boolean startBundle, int startLevel, int waitTime, int retries)
+            throws ClientException, InterruptedException {
+        installBundle(f, startBundle, startLevel);
+        try {
+            return this.checkBundleInstalled(OsgiConsoleClient.getBundleSymbolicName(f), waitTime, retries);
+        } catch (IOException e) {
+            throw new ClientException("Cannot get bundle symbolic name", e);
+        }
+    }
+
+    /**
+     * Check that specified bundle is installed and retries every {{waitTime}} milliseconds, until the
+     * bundle is installed or the number of retries was reached
+     * @param symbolicName the name of the bundle
+     * @param waitTime How many milliseconds to wait between retries
+     * @param retries the number of retries
+     * @return true if the bundle was installed until the retries stop, false otherwise
+     * @throws InterruptedException
+     */
+    public boolean checkBundleInstalled(String symbolicName, int waitTime, int retries) throws InterruptedException {
+        final String path = getBundlePath(symbolicName, ".json");
+        return new PathPoller(this, path, waitTime, retries).callAndWait();
+    }
+
+    /**
+     * Get the id of the bundle
+     * @param symbolicName
+     * @return
+     * @throws Exception
+     */
+    public long getBundleId(String symbolicName) throws ClientException {
+        final JSONObject bundle = getBundleData(symbolicName);
+        try {
+            return bundle.getLong(JSON_KEY_ID);
+        } catch (JSONException e) {
+            throw new ClientException("Cannot get id from json", e);
+        }
+    }
+
+    /**
+     * Get the version of the bundle
+     * @param symbolicName
+     * @return
+     * @throws ClientException
+     */
+    public String getBundleVersion(String symbolicName) throws ClientException {
+        final JSONObject bundle = getBundleData(symbolicName);
+        try {
+            return bundle.getString(JSON_KEY_VERSION);
+        } catch (JSONException e) {
+            throw new ClientException("Cannot get version from json", e);
+        }
+    }
+
+    /**
+     * Get the state of the bundle
+     * @param symbolicName
+     * @return
+     * @throws Exception
+     */
+    public String getBundleState(String symbolicName) throws ClientException {
+        final JSONObject bundle = getBundleData(symbolicName);
+        try {
+            return bundle.getString(JSON_KEY_STATE);
+        } catch (JSONException e) {
+            throw new ClientException("Cannot get state from json", e);
+        }
+    }
+
+    /**
+     * Starts a bundle
+     * @param symbolicName the name of the bundle
+     * @throws ClientException
+     */
+    public void startBundle(String symbolicName) throws ClientException {
+        // To start the bundle we POST action=start to its URL
+        final String path = getBundlePath(symbolicName);
+        LOG.info("Starting bundle {} via {}", symbolicName, path);
+        this.doPost(path, FormEntityBuilder.create().addParameter("action", "start").build(), SC_OK);
+    }
+
+    /**
+     * Starts a bundle and waits for it to be started
+     * @param symbolicName the name of the bundle
+     * @param waitTime How many milliseconds to wait between retries
+     * @param retries the number of retries
+     * @throws ClientException, InterruptedException
+     */
+    public void startBundlewithWait(String symbolicName, int waitTime, int retries)
+            throws ClientException, InterruptedException {
+        // start a bundle
+        startBundle(symbolicName);
+        // wait for it to be in the started state
+        checkBundleInstalled(symbolicName, waitTime, retries);
+    }
+
+    /**
+     * Calls PackageAdmin.refreshPackages to force re-wiring of all the bundles.
+     * @throws ClientException
+     */
+    public void refreshPackages() throws ClientException {
+        LOG.info("Refreshing packages.");
+        FormEntityBuilder builder = FormEntityBuilder.create();
+        builder.addParameter("action", "refreshPackages");
+        this.doPost(URL_BUNDLES, builder.build(), 200);
+    }
+
+
+    //
+    // private methods
+    //
+
+    private String getBundlePath(String symbolicName, String extension) {
+        return getBundlePath(symbolicName) + extension;
+    }
+
+    private String getBundlePath(String symbolicName) {
+        return URL_BUNDLES + "/" + symbolicName;
+    }
+
+    private JSONObject getBundleData(String symbolicName) throws ClientException {
+        // 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 = this.doGet(path, SC_OK).getContent();
+
+        try {
+            final JSONObject root = new JSONObject(content);
+
+            if (!root.has(JSON_KEY_DATA)) {
+                throw new ClientException(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 ClientException(path + "." + JSON_KEY_DATA + " is empty, JSON content=" + content);
+            }
+
+            final JSONObject bundle = data.getJSONObject(0);
+            if (!bundle.has(JSON_KEY_STATE)) {
+                throw new ClientException(path + ".data[0].state missing, JSON content=" + content);
+            }
+
+            return bundle;
+        } catch (JSONException e) {
+            throw new ClientException("Cannot get json", e);
+        }
+    }
+
+    //
+    // static methods
+    //
+
+    /**
+     * Get the symbolic name from a bundle file
+     * @param bundleFile
+     * @return
+     * @throws IOException
+     */
+    public static String getBundleSymbolicName(File bundleFile) throws IOException {
+        String name = null;
+        final JarInputStream jis = new JarInputStream(new FileInputStream(bundleFile));
+        try {
+            final Manifest m = jis.getManifest();
+            if (m == null) {
+                throw new IOException("Manifest is null in " + bundleFile.getAbsolutePath());
+            }
+            name = m.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME);
+        } finally {
+            jis.close();
+        }
+        return name;
+    }
+
+    /**
+     * Get the version form a bundle file
+     * @param bundleFile
+     * @return
+     * @throws IOException
+     */
+    public static String getBundleVersionFromFile(File bundleFile) throws IOException {
+        String version = null;
+        final JarInputStream jis = new JarInputStream(new FileInputStream(bundleFile));
+        try {
+            final Manifest m = jis.getManifest();
+            if(m == null) {
+                throw new IOException("Manifest is null in " + bundleFile.getAbsolutePath());
+            }
+            version = m.getMainAttributes().getValue(Constants.BUNDLE_VERSION);
+        } finally {
+            jis.close();
+        }
+        return version;
     }
 
 

Added: sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/util/poller/PathPoller.java
URL: http://svn.apache.org/viewvc/sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/util/poller/PathPoller.java?rev=1743676&view=auto
==============================================================================
--- sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/util/poller/PathPoller.java (added)
+++ sling/trunk/testing/http/clients/src/main/java/org/apache/sling/testing/clients/util/poller/PathPoller.java Fri May 13 14:02:03 2016
@@ -0,0 +1,64 @@
+/*******************************************************************************
+ * 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.util.poller;
+
+import org.apache.sling.testing.clients.AbstractSlingClient;
+import org.apache.sling.testing.clients.ClientException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Allows polling for a resource
+ */
+public class PathPoller extends AbstractPoller {
+    private static final Logger LOG = LoggerFactory.getLogger(PathPoller.class);
+    private final AbstractSlingClient client;
+    private final String path;
+    private final int[] expectedStatus;
+    private Exception exception;
+
+    public PathPoller(AbstractSlingClient client, String path, long waitInterval, long waitCount, int... expectedStatus) {
+        super(waitInterval, waitCount);
+        this.client = client;
+        this.path = path;
+        this.expectedStatus = expectedStatus;
+    }
+
+
+    @Override
+    public boolean call() {
+        return true;
+    }
+
+    @Override
+    public boolean condition() {
+        try {
+            client.doGet(path, expectedStatus);
+        } catch (ClientException e) {
+            LOG.warn("Get on {} failed: {}", path, e);
+            this.exception = e;
+            return false;
+        }
+        return true;
+    }
+
+    public Exception getException() {
+        return exception;
+    }
+}