org.apache.sling.testing.clients.osgi.OsgiConsoleClient.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.sling.testing.clients.osgi.OsgiConsoleClient.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with this
 * work for additional information regarding copyright ownership. The ASF
 * licenses this file to You under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package org.apache.sling.testing.clients.osgi;

import org.apache.http.Header;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.sling.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;
import org.apache.sling.testing.clients.util.poller.AbstractPoller;
import org.apache.sling.testing.clients.SlingClient;
import org.apache.sling.testing.clients.SlingClientConfig;
import org.apache.sling.testing.clients.util.FormEntityBuilder;
import org.apache.sling.testing.clients.util.HttpUtils;
import org.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 {

    private static final Logger LOG = LoggerFactory.getLogger(OsgiConsoleClient.class);
    /**
     * All System Console REST API calls go to /system/console and below
     */
    private final String CONSOLE_ROOT_URL = "/system/console";

    /**
     * The URL for configuration requests
     */
    private final String URL_CONFIGURATION = CONSOLE_ROOT_URL + "/configMgr";

    /**
     * The URL for bundle requests
     */
    private final String URL_BUNDLES = CONSOLE_ROOT_URL + "/bundles";

    /**
     * The URL for components requests
     */
    private final String URL_COMPONENTS = CONSOLE_ROOT_URL + "/components";

    public static final String JSON_KEY_ID = "id";
    public static final String JSON_KEY_VERSION = "version";
    public static final String JSON_KEY_DATA = "data";
    public static final String JSON_KEY_STATE = "state";

    /**
     * Default constructor. Simply calls {@link SlingClient#SlingClient(URI, String, String)}
     *
     * @param serverUrl the URL to the server under test
     * @param userName the user name used for authentication
     * @param password the password for this user
     * @throws ClientException if the client cannot be instantiated
     */
    public OsgiConsoleClient(URI serverUrl, String userName, String password) throws ClientException {
        super(serverUrl, userName, password);
    }

    /**
     * Constructor used by adaptTo() and InternalBuilder classes. Should not be called directly in the code
     *
     * @param http http client to be used for requests
     * @param config sling specific configs
     * @throws ClientException if the client cannot be instantiated
     */
    public OsgiConsoleClient(CloseableHttpClient http, SlingClientConfig config) throws ClientException {
        super(http, config);
    }

    /**
     * Returns the wrapper for the bundles info json
     *
     * @param expectedStatus list of accepted statuses of the response
     * @return all the bundles info
     * @throws ClientException if the response status does not match any of the expectedStatus
     */
    public BundlesInfo getBundlesInfo(int... expectedStatus) throws ClientException {
        // request the bundles information
        SlingHttpResponse resp = this.doGet(URL_BUNDLES + ".json",
                HttpUtils.getExpectedStatus(SC_OK, expectedStatus));
        // return the wrapper
        return new BundlesInfo(JsonUtils.getJsonNodeFromString(resp.getContent()));
    }

    /**
     * Returns the wrapper for the bundle info json
     *
     * @param id the id of the bundle
     * @param expectedStatus list of accepted statuses of the response
     * @return the bundle info
     * @throws ClientException if the response status does not match any of the expectedStatus
     */
    public BundleInfo getBundleInfo(String id, int... expectedStatus) throws ClientException {
        SlingHttpResponse resp = this.doGet(URL_BUNDLES + "/" + id + ".json");
        HttpUtils.verifyHttpStatus(resp, HttpUtils.getExpectedStatus(SC_OK, expectedStatus));
        return new BundleInfo(JsonUtils.getJsonNodeFromString(resp.getContent()));
    }

    /**
     * Returns the wrapper for the components info json
     *
     * @param expectedStatus list of accepted statuses of the response
     * @return the components info
     * @throws ClientException if the response status does not match any of the expectedStatus
     */
    public ComponentsInfo getComponentsInfo(int... expectedStatus) throws ClientException {
        SlingHttpResponse resp = this.doGet(URL_COMPONENTS + ".json");
        HttpUtils.verifyHttpStatus(resp, HttpUtils.getExpectedStatus(SC_OK, expectedStatus));
        return new ComponentsInfo(JsonUtils.getJsonNodeFromString(resp.getContent()));
    }

    /**
     * Returns the wrapper for the component info json
     *
     * @param id the id of the component
     * @param expectedStatus list of accepted statuses of the response
     * @return the component info
     * @throws ClientException if the response status does not match any of the expectedStatus
     */
    public ComponentInfo getComponentInfo(String id, int expectedStatus) throws ClientException {
        SlingHttpResponse resp = this.doGet(URL_COMPONENTS + "/" + id + ".json");
        HttpUtils.verifyHttpStatus(resp, HttpUtils.getExpectedStatus(SC_OK, expectedStatus));
        return new ComponentInfo(JsonUtils.getJsonNodeFromString(resp.getContent()));
    }

    //
    // OSGi configurations
    //

    /**
     * Returns a map of all properties set for the config referenced by the PID, where the map keys
     * are the property names.
     *
     * @param pid the pid of the configuration
     * @param expectedStatus list of accepted statuses of the response
     * @return the properties as a map
     * @throws ClientException if the response status does not match any of the expectedStatus
     */
    public Map<String, Object> getConfiguration(String pid, int... expectedStatus) throws ClientException {
        // make the request
        SlingHttpResponse resp = this.doPost(URL_CONFIGURATION + "/" + pid, null);
        // check the returned status
        HttpUtils.verifyHttpStatus(resp, HttpUtils.getExpectedStatus(SC_OK, expectedStatus));
        // get the JSON node
        JsonNode rootNode = JsonUtils.getJsonNodeFromString(resp.getContent());
        // go through the params
        Map<String, Object> props = new HashMap<String, Object>();
        if (rootNode.get("properties") == null)
            return props;
        JsonNode properties = rootNode.get("properties");
        for (Iterator<String> it = properties.getFieldNames(); it.hasNext();) {
            String propName = it.next();
            JsonNode value = properties.get(propName).get("value");
            if (value != null) {
                props.put(propName, value.getValueAsText());
                continue;
            }
            value = properties.get(propName).get("values");
            if (value != null) {
                Iterator<JsonNode> iter = value.getElements();
                List<String> list = new ArrayList<String>();
                while (iter.hasNext()) {
                    list.add(iter.next().getValueAsText());
                }
                props.put(propName, list.toArray(new String[list.size()]));
            }
        }
        return props;
    }

    /**
     * Returns a map of all properties set for the config referenced by the PID, where the map keys
     * are the property names. The method waits until the configuration has been set.
     *
     * @param waitCount The number of maximum wait intervals of 500ms.
     *                  Between each wait interval, the method polls the backend to see if the configuration ahs been set.
     * @param pid pid
     * @param expectedStatus expected response status
     * @return the config properties
     * @throws ClientException if the response status does not match any of the expectedStatus
     * @throws InterruptedException to mark this operation as "waiting"
     */
    public Map<String, Object> getConfigurationWithWait(long waitCount, String pid, int... expectedStatus)
            throws ClientException, InterruptedException {
        ConfigurationPoller poller = new ConfigurationPoller(500L, waitCount, pid, expectedStatus);
        if (!poller.callUntilCondition())
            return getConfiguration(pid, expectedStatus);
        return poller.getConfig();
    }

    /**
     * Sets properties of a config referenced by its PID. the properties to be edited are passed as
     * a map of property name,value pairs.
     *
     * @param PID Persistent identity string
     * @param factoryPID Factory persistent identity string or {@code null}
     * @param configProperties map of properties
     * @param expectedStatus expected response status
     * @return the location of the config
     * @throws ClientException if the response status does not match any of the expectedStatus
     */
    public String editConfiguration(String PID, String factoryPID, Map<String, Object> configProperties,
            int... expectedStatus) throws ClientException {
        FormEntityBuilder builder = FormEntityBuilder.create();
        builder.addParameter("apply", "true");
        builder.addParameter("action", "ajaxConfigManager");
        // send factory PID if set
        if (factoryPID != null) {
            builder.addParameter("factoryPid", factoryPID);
        }
        // add properties to edit
        StringBuilder propertyList = new StringBuilder("");
        for (String propName : configProperties.keySet()) {
            Object o = configProperties.get(propName);
            if (o instanceof String) {
                builder.addParameter(propName, (String) o);
            } else if (o instanceof String[]) {
                for (String s : (String[]) o) {
                    builder.addParameter(propName, s);
                }
            }
            propertyList.append(propName).append(",");
        }
        // cut off the last comma
        builder.addParameter("propertylist", propertyList.substring(0, propertyList.length() - 1));
        // make the request
        SlingHttpResponse resp = this.doPost(URL_CONFIGURATION + "/" + PID, builder.build());
        // check the returned status
        HttpUtils.verifyHttpStatus(resp, HttpUtils.getExpectedStatus(SC_MOVED_TEMPORARILY, expectedStatus));

        Header[] locationHeader = resp.getHeaders("Location");
        if (locationHeader != null && locationHeader.length == 1) {
            return locationHeader[0].getValue().substring(URL_CONFIGURATION.length() + 1);
        } else {
            return null;
        }
    }

    /**
     * Sets properties of a config referenced by its PID. the properties to be edited are passed as
     * a map of property (name,value) pairs. The method waits until the configuration has been set.
     *
     * @param waitCount The number of maximum wait intervals of 500ms.
     *                  Between each wait interval, the method polls the backend to see if the configuration ahs been set.
     * @param PID Persistent identity string
     * @param factoryPID Factory persistent identity string or {@code null}
     * @param configProperties map of properties
     * @param expectedStatus expected response status
     * @return the pid
     * @throws ClientException if the response status does not match any of the expectedStatus
     * @throws InterruptedException to mark this operation as "waiting"
     */
    public String editConfigurationWithWait(int waitCount, String PID, String factoryPID,
            Map<String, Object> configProperties, int... expectedStatus)
            throws ClientException, InterruptedException {
        String pid = editConfiguration(PID, factoryPID, configProperties, expectedStatus);
        getConfigurationWithWait(waitCount, pid);
        return pid;
    }

    /**
     * Delete the config referenced by the PID
     *
     * @param pid pid
     * @param expectedStatus expected response status
     * @return the sling response
     * @throws ClientException if the response status does not match any of the expectedStatus
     */
    public SlingHttpResponse deleteConfiguration(String pid, int... expectedStatus) throws ClientException {
        FormEntityBuilder builder = FormEntityBuilder.create();
        builder.addParameter("apply", "1");
        builder.addParameter("delete", "1");
        // make the request
        SlingHttpResponse resp = this.doPost(URL_CONFIGURATION + "/" + pid, builder.build());
        // check the returned status
        HttpUtils.verifyHttpStatus(resp, HttpUtils.getExpectedStatus(200, expectedStatus));
        return resp;
    }

    //
    // Bundles
    //

    /**
     * Uninstall a bundle
     * @param symbolicName
     * @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;
    }

    class ConfigurationPoller extends AbstractPoller {

        private final String pid;
        int[] expectedStatus;
        public Map<String, Object> config;

        public ConfigurationPoller(long waitInterval, long waitCount, String pid, int... expectedStatus) {
            super(waitInterval, waitCount);
            this.pid = pid;
            this.config = null;
            this.expectedStatus = expectedStatus;
        }

        @Override
        public boolean call() {
            try {
                config = getConfiguration(pid, expectedStatus);
            } catch (ClientException e) {
                LOG.warn("Couldn't get config " + pid, e);
            }
            return true;
        }

        @Override
        public boolean condition() {
            return null != config;
        }

        public Map<String, Object> getConfig() {
            return config;
        }
    }
}