/* * 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.SlingClient; import org.apache.sling.testing.clients.SlingClientConfig; import org.apache.sling.testing.clients.SlingHttpResponse; 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.PathPoller; import org.apache.sling.testing.clients.util.poller.Polling; 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.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.TimeoutException; 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. * * @deprecated use {@link #waitGetConfiguration(long, String, int...)} * * @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" */ @Deprecated public Map<String, Object> getConfigurationWithWait(long waitCount, String pid, int... expectedStatus) throws ClientException, InterruptedException { ConfigurationPoller poller = new ConfigurationPoller(pid, expectedStatus); try { poller.poll(500L * waitCount, 500); } catch (TimeoutException e) { throw new ClientException("Cannot retrieve configuration.", e); } return poller.getConfig(); } /** * 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 timeout Maximum time to wait for the configuration to be available, in ms. * @param pid service 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" * @throws TimeoutException if the timeout was reached */ public Map<String, Object> waitGetConfiguration(long timeout, String pid, int... expectedStatus) throws ClientException, InterruptedException, TimeoutException { ConfigurationPoller poller = new ConfigurationPoller(pid, expectedStatus); poller.poll(timeout, 500); 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. * * @deprecated use {@link #waitEditConfiguration(long, String, String, Map, int...)} * * @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" */ @Deprecated 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; } /** * 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 timeout Max time to wait for the configuration to be set, in ms * @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" * @throws TimeoutException if the timeout was reached */ public String waitEditConfiguration(long timeout, String PID, String factoryPID, Map<String, Object> configProperties, int... expectedStatus) throws ClientException, InterruptedException, TimeoutException { String pid = editConfiguration(PID, factoryPID, configProperties, expectedStatus); waitGetConfiguration(timeout, 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 bundle symbolic name * @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 bundle file * @param startBundle whether to start or just install the bundle * @param startLevel start level * @return the sling response * @throws ClientException if the request failed */ 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); } /** * Check that specified bundle is installed and retries every {{waitTime}} milliseconds, until the * bundle is installed or the number of retries was reached * @deprecated does not respect polling practices; use {@link #waitBundleInstalled(String, long, long)} instead * @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 */ @Deprecated 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(); } /** * Install a bundle using the Felix webconsole HTTP interface and wait for it to be installed * @deprecated {@link #waitInstallBundle(File, boolean, int, long, long)} * @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 */ @Deprecated 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); } } /** * 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 timeout how much to wait for the bundle to be installed before throwing a {@code TimeoutException} * @param delay time to wait between checks of the state * @throws ClientException * @throws TimeoutException if the bundle did not install before timeout was reached * @throws InterruptedException */ public void waitInstallBundle(File f, boolean startBundle, int startLevel, long timeout, long delay) throws ClientException, InterruptedException, TimeoutException { installBundle(f, startBundle, startLevel); try { waitBundleInstalled(getBundleSymbolicName(f), timeout, delay); } catch (IOException e) { throw new ClientException("Cannot get bundle symbolic name", e); } } public void waitBundleInstalled(final String symbolicName, final long timeout, final long delay) throws TimeoutException, InterruptedException { final String path = getBundlePath(symbolicName); Polling p = new Polling() { @Override public Boolean call() throws Exception { return exists(path); } @Override protected String message() { return "Bundle " + symbolicName + " did not install in %1$ ms"; } }; p.poll(timeout, delay); } /** * Get the id of the bundle * @param symbolicName bundle symbolic name * @return the id * @throws ClientException if the id cannot be retrieved */ 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 bundle symbolic name * @return bundle version * @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 bundle symbolic name * @return the state of the bundle * @throws ClientException if the state cannot be retrieved */ 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 * @deprecated use {@link #waitStartBundle(String, long, long)} * @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 */ @Deprecated 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); } /** * Starts a bundle and waits for it to be started * @param symbolicName the name of the bundle * @param timeout max time to wait for the bundle to start, in ms * @param delay time to wait between status checks, in ms * @throws ClientException, InterruptedException, TimeoutException */ public void waitStartBundle(String symbolicName, long timeout, long delay) throws ClientException, InterruptedException, TimeoutException { startBundle(symbolicName); // FIXME this should wait for the started state waitBundleInstalled(symbolicName, timeout, delay); } /** * 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; } /** * 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":"" * }] * } */ private JSONObject getBundleData(String symbolicName) throws ClientException { 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 by looking at the manifest * @param bundleFile bundle file * @return the name extracted from the manifest * @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 by looking at the manifest * @param bundleFile bundle file * @return the version * @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 Polling { private final String pid; private final int[] expectedStatus; private Map<String, Object> config; public ConfigurationPoller(String pid, int... expectedStatus) { super(); this.pid = pid; this.expectedStatus = expectedStatus; this.config = null; } @Override public Boolean call() throws Exception { config = getConfiguration(pid, expectedStatus); return config != null; } public Map<String, Object> getConfig() { return config; } } }