SLING-7297 add methods to wait for a component and a service
diff --git a/pom.xml b/pom.xml
index ee4d472..17c4562 100644
--- a/pom.xml
+++ b/pom.xml
@@ -141,6 +141,12 @@
<scope>test</scope>
</dependency>
<dependency>
+ <groupId>org.hamcrest</groupId>
+ <artifactId>hamcrest-library</artifactId>
+ <version>1.3</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
<groupId>org.apache.sling</groupId>
<artifactId>org.apache.sling.hapi.client</artifactId>
<version>1.0.0</version>
diff --git a/src/main/java/org/apache/sling/testing/clients/osgi/OsgiConsoleClient.java b/src/main/java/org/apache/sling/testing/clients/osgi/OsgiConsoleClient.java
index 55bdb25..1e8fac4 100644
--- a/src/main/java/org/apache/sling/testing/clients/osgi/OsgiConsoleClient.java
+++ b/src/main/java/org/apache/sling/testing/clients/osgi/OsgiConsoleClient.java
@@ -39,6 +39,7 @@
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@@ -77,6 +78,11 @@
* The URL for components requests
*/
private final String URL_COMPONENTS = CONSOLE_ROOT_URL + "/components";
+
+ /**
+ * The URL for service requests
+ */
+ private final String URL_SERVICES = CONSOLE_ROOT_URL + "/services";
public static final String JSON_KEY_ID = "id";
@@ -162,6 +168,105 @@
return new ComponentInfo(JsonUtils.getJsonNodeFromString(resp.getContent()));
}
+ /**
+ * Returns the wrapper for the component info json
+ *
+ * @param id the id of the component
+ * @return the component info or {@code null} if the component with that name is not found
+ */
+ private ComponentInfo getComponentInfo(String name) throws ClientException {
+ SlingHttpResponse resp = this.doGet(URL_COMPONENTS + "/" + name + ".json");
+ if (HttpUtils.getHttpStatus(resp) == SC_OK) {
+ return new ComponentInfo(JsonUtils.getJsonNodeFromString(resp.getContent()));
+ }
+ return null;
+ }
+
+ /**
+ * Returns the service info wrapper for all services implementing the given type.
+ *
+ * @param name the type of the service
+ * @return the service infos or {@code null} if no service for the given type is registered
+ */
+ private Collection<ServiceInfo> getServiceInfos(String type) throws ClientException {
+ SlingHttpResponse resp = this.doGet(URL_SERVICES + ".json");
+ if (HttpUtils.getHttpStatus(resp) == SC_OK) {
+ return new ServicesInfo(JsonUtils.getJsonNodeFromString(resp.getContent())).forType(type);
+ }
+ return null;
+ }
+
+ /**
+ * Wait until the component with the given name is registered. This means the component must be either in state "Registered" or "Active".
+ * @param componentName the component's name
+ * @param timeout how long to wait for the component to become registered before throwing a {@code TimeoutException} in milliseconds
+ * @param delay time to wait between checks of the state in milliseconds
+ * @throws TimeoutException if the component did not become registered before timeout was reached
+ * @throws InterruptedException if interrupted
+ * @see "OSGi Comp. R6, §112.5 Component Life Cycle"
+ */
+ public void waitComponentRegistered(final String componentName, final long timeout, final long delay) throws TimeoutException, InterruptedException {
+ Polling p = new Polling() {
+ @Override
+ public Boolean call() throws Exception {
+ ComponentInfo info = getComponentInfo(componentName);
+ if (info != null) {
+ return ((info.getStatus() == Component.Status.REGISTERED) || (info.getStatus() == Component.Status.ACTIVE));
+ } else {
+ LOG.debug("Could not get component info for component name {}", componentName);
+ }
+ return false;
+ }
+
+ @Override
+ protected String message() {
+ return "Component " + componentName + " was not registered in %1$d ms";
+ }
+ };
+ p.poll(timeout, delay);
+ }
+
+ /**
+ * Wait until the service with the given name is registered. This means the component must be either in state "Registered" or "Active".
+ * @param type the type of the service (usually the name of a Java interface)
+ * @param bundleSymbolicName the symbolic name of the bundle supposed to register that service.
+ * May be {@code null} in which case this method just waits for any service with the requested type being registered (independent of the registering bundle).
+ * @param timeout how long to wait for the component to become registered before throwing a {@code TimeoutException} in milliseconds
+ * @param delay time to wait between checks of the state in milliseconds
+ * @throws TimeoutException if the component did not become registered before timeout was reached
+ * @throws InterruptedException if interrupted
+ */
+ public void waitServiceRegistered(final String type, final String bundleSymbolicName , final long timeout, final long delay) throws TimeoutException, InterruptedException {
+ Polling p = new Polling() {
+ @Override
+ public Boolean call() throws Exception {
+ Collection<ServiceInfo> infos = getServiceInfos(type);
+ if (infos != null) {
+ if (bundleSymbolicName != null) {
+ for (ServiceInfo info : infos) {
+ if (bundleSymbolicName.equals(info.getBundleSymbolicName())) {
+ return true;
+ }
+ }
+ LOG.debug("Could not find service info for service type {} provided by bundle {}", type, bundleSymbolicName);
+ return false;
+ } else {
+ return !infos.isEmpty();
+ }
+ } else {
+ LOG.debug("Could not find any service info for service type {}", type);
+ }
+ return false;
+ }
+
+ @Override
+ protected String message() {
+ return "Service with type " + type + " was not registered in %1$d ms";
+ }
+ };
+ p.poll(timeout, delay);
+ }
+
//
// OSGi configurations
//
@@ -462,12 +567,12 @@
}
/**
- * Install a bundle using the Felix webconsole HTTP interface and wait for it to be installed
+ * 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
+ * @param timeout how long to wait for the bundle to be installed before throwing a {@code TimeoutException} in milliseconds
+ * @param delay time to wait between checks of the state in milliseconds
* @throws ClientException if the request failed
* @throws TimeoutException if the bundle did not install before timeout was reached
* @throws InterruptedException if interrupted
@@ -484,12 +589,13 @@
}
/**
- * Wait until the bundle is installed
+ * Wait until the bundle is installed.
* @param symbolicName symbolic name of bundle
- * @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
+ * @param timeout how long to wait for the bundle to be installed before throwing a {@code TimeoutException} in milliseconds
+ * @param delay time to wait between checks of the state in milliseconds
* @throws TimeoutException if the bundle did not install before timeout was reached
* @throws InterruptedException if interrupted
+ * @see "OSGi Core R6, §4.4.2 Bundle State"
*/
public void waitBundleInstalled(final String symbolicName, final long timeout, final long delay)
throws TimeoutException, InterruptedException {
@@ -503,7 +609,40 @@
@Override
protected String message() {
- return "Bundle " + symbolicName + " did not install in %1$ ms";
+ return "Bundle " + symbolicName + " did not install in %1$d ms";
+ }
+ };
+
+ p.poll(timeout, delay);
+ }
+
+ /**
+ * Wait until the bundle is started
+ * @param symbolicName symbolic name of bundle
+ * @param timeout how long to wait for the bundle to be installed before throwing a {@code TimeoutException} in milliseconds.
+ * @param delay time to wait between checks of the state in milliseconds.
+ * @throws TimeoutException if the bundle did not install before timeout was reached
+ * @throws InterruptedException if interrupted
+ * @see "OSGi Core R6, §4.4.2 Bundle State"
+ */
+ public void waitBundleStarted(final String symbolicName, final long timeout, final long delay)
+ throws TimeoutException, InterruptedException {
+
+ Polling p = new Polling() {
+ @Override
+ public Boolean call() throws Exception {
+ try {
+ BundleInfo bundleInfo = getBundleInfo(symbolicName, 200);
+ return (bundleInfo.getStatus() == Bundle.Status.ACTIVE);
+ } catch (ClientException e) {
+ LOG.debug("Could not get bundle state for {}: {}", symbolicName, e.getLocalizedMessage(), e);
+ return false;
+ }
+ }
+
+ @Override
+ protected String message() {
+ return "Bundle " + symbolicName + " did not start in %1$d ms";
}
};
diff --git a/src/main/java/org/apache/sling/testing/clients/osgi/ServiceInfo.java b/src/main/java/org/apache/sling/testing/clients/osgi/ServiceInfo.java
new file mode 100644
index 0000000..8df6463
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/osgi/ServiceInfo.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.apache.sling.testing.clients.osgi;
+
+import java.util.List;
+
+import org.apache.sling.testing.clients.ClientException;
+import org.codehaus.jackson.JsonNode;
+
+public class ServiceInfo {
+
+ private JsonNode service;
+
+ public ServiceInfo(JsonNode root) throws ClientException {
+ if(root.get("id") != null) {
+ service = root;
+ } else {
+ if(root.get("data") == null && root.get("data").size() < 1) {
+ throw new ClientException("No service info returned");
+ }
+ service = root.get("data").get(0);
+ }
+ }
+
+ /**
+ * @return the service identifier
+ */
+ public int getId() {
+ return service.get("id").getIntValue();
+ }
+
+ /**
+ * @return the service types name
+ */
+ public List<String> getTypes() {
+ // this is not a proper JSON array (https://issues.apache.org/jira/browse/FELIX-5762)
+ return ServicesInfo.splitPseudoJsonValueArray(service.get("types").getTextValue());
+ }
+
+ public String getPid() {
+ return service.get("pid").getTextValue();
+ }
+
+ /**
+ * @return the bundle id of the bundle exposing the service
+ */
+ public int getBundleId() {
+ return service.get("bundleId").getIntValue();
+ }
+
+ /**
+ * @return the bundle symbolic name of bundle implementing the service
+ */
+ public String getBundleSymbolicName() {
+ return service.get("bundleSymbolicName").getTextValue();
+ }
+
+}
diff --git a/src/main/java/org/apache/sling/testing/clients/osgi/ServicesInfo.java b/src/main/java/org/apache/sling/testing/clients/osgi/ServicesInfo.java
new file mode 100644
index 0000000..8ec3bbd
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/osgi/ServicesInfo.java
@@ -0,0 +1,142 @@
+/*
+ * 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.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * A simple Wrapper around the returned JSON when requesting the status of /system/console/services
+ */
+public class ServicesInfo {
+
+ private JsonNode root = 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 ServicesInfo(JsonNode root) throws ClientException {
+ this.root = root;
+ // some simple sanity checks
+ if(root.get("status") == null)
+ throw new ClientException("No Status returned!");
+ if(root.get("serviceCount") == null)
+ throw new ClientException("No serviceCount returned!");
+ }
+
+ /**
+ * @return total number of bundles.
+ */
+ public int getTotalNumOfServices() {
+ return root.get("serviceCount").getIntValue();
+ }
+
+ /**
+ * Return service info for a service with given id
+ *
+ * @param id the id of the service
+ * @return the BundleInfo
+ * @throws ClientException if the info could not be retrieved
+ */
+ public ServiceInfo forId(String id) throws ClientException {
+ JsonNode serviceInfo = findBy("id", id);
+ return (serviceInfo != null) ? new ServiceInfo(serviceInfo) : null;
+ }
+
+ /**
+ * Return service infos for a bundle with name {@code name}
+ *
+ * @param type the type of the service
+ * @return a Collection of {@link ServiceInfo}s of all services with the given type. Might be empty, never {@code null}
+ * @throws ClientException if the info cannot be retrieved
+ */
+ public Collection<ServiceInfo> forType(String type) throws ClientException {
+ List<ServiceInfo> results = new LinkedList<>();
+ List<JsonNode> serviceInfoNodes = findAllContainingValueInArray("types", type);
+ for (JsonNode serviceInfoNode : serviceInfoNodes) {
+ results.add(new ServiceInfo(serviceInfoNode));
+ }
+ return results;
+ }
+
+ private JsonNode findBy(String key, String value) {
+ List<JsonNode> result = findBy(key, value, true, false);
+ if (result.isEmpty()) {
+ return null;
+ } else {
+ return result.get(0);
+ }
+ }
+
+ private List<JsonNode> findAllContainingValueInArray(String key, String value) {
+ return findBy(key, value, false, true);
+ }
+
+ private List<JsonNode> findBy(String key, String value, boolean onlyReturnFirstMatch, boolean arrayContainingMatch) {
+ Iterator<JsonNode> nodes = root.get("data").getElements();
+ List<JsonNode> results = new LinkedList<>();
+ while(nodes.hasNext()) {
+ JsonNode node = nodes.next();
+ if ((null != node.get(key)) && (node.get(key).isValueNode())) {
+ final String valueNode = node.get(key).getTextValue();
+ if (arrayContainingMatch) {
+ if (splitPseudoJsonValueArray(valueNode).contains(value)) {
+ results.add(node);
+ }
+ } else {
+ if (valueNode.equals(value)) {
+ results.add(node);
+ }
+ }
+ }
+ }
+ return results;
+ }
+
+ /**
+ * Array values are not returned as proper JSON array for Apache Felix.
+ * Therefore we need this dedicated split method, which extracts the individual values from this "pseudo" JSON array.
+ * Example value:
+ * <pre>
+ * [java.lang.Runnable, org.apache.sling.event.impl.jobs.queues.QueueManager, org.osgi.service.event.EventHandler]
+ * </pre>
+ * @param value the value to split
+ * @return the list of the individual values in the given array.
+ * @see <a href="https://issues.apache.org/jira/browse/FELIX-5762">FELIX-5762</a>
+ */
+ static final List<String> splitPseudoJsonValueArray(String value) {
+ // is this an array?
+ if (value.startsWith("[") && value.length() >= 2) {
+ // strip of first and last character
+ String pureArrayValues = value.substring(1, value.length() - 1);
+ String[] resultArray = pureArrayValues.split(", |,");
+ return Arrays.asList(resultArray);
+ }
+ return Collections.singletonList(value);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/testing/clients/osgi/package-info.java b/src/main/java/org/apache/sling/testing/clients/osgi/package-info.java
index 2b92f60..fa38949 100644
--- a/src/main/java/org/apache/sling/testing/clients/osgi/package-info.java
+++ b/src/main/java/org/apache/sling/testing/clients/osgi/package-info.java
@@ -19,7 +19,7 @@
/**
* OSGI testing tools.
*/
-@Version("1.3.0")
+@Version("1.4.0")
package org.apache.sling.testing.clients.osgi;
import org.osgi.annotation.versioning.Version;
diff --git a/src/main/java/org/apache/sling/testing/clients/util/poller/Polling.java b/src/main/java/org/apache/sling/testing/clients/util/poller/Polling.java
index c57018d..064bbcc 100644
--- a/src/main/java/org/apache/sling/testing/clients/util/poller/Polling.java
+++ b/src/main/java/org/apache/sling/testing/clients/util/poller/Polling.java
@@ -121,7 +121,7 @@
/**
* Returns the string to be used in the {@code TimeoutException}, if needed.
* The string is passed to {@code String.format(message(), timeout, delay)}, so it can be a format
- * including {@code %1$} and {@code %2$}. The field {@code lastException} is also available for logging
+ * including {@code %1$d} and {@code %2$d}. The field {@code lastException} is also available for logging
*
* @return the format string
*/
diff --git a/src/test/java/org/apache/sling/testing/clients/osgi/ServicesInfoTest.java b/src/test/java/org/apache/sling/testing/clients/osgi/ServicesInfoTest.java
new file mode 100644
index 0000000..b74bb31
--- /dev/null
+++ b/src/test/java/org/apache/sling/testing/clients/osgi/ServicesInfoTest.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.sling.testing.clients.osgi;
+
+import org.hamcrest.Matchers;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class ServicesInfoTest {
+
+ @Test
+ public void testSplitPseudoJsonValueArray() {
+ Assert.assertThat(ServicesInfo.splitPseudoJsonValueArray("test"), Matchers.contains("test"));
+ Assert.assertThat(ServicesInfo.splitPseudoJsonValueArray("[]"), Matchers.contains(""));
+ Assert.assertThat(ServicesInfo.splitPseudoJsonValueArray("[one, two]"), Matchers.contains("one", "two"));
+ Assert.assertThat(ServicesInfo.splitPseudoJsonValueArray("[one,two]"), Matchers.contains("one", "two"));
+ Assert.assertThat(ServicesInfo.splitPseudoJsonValueArray("[java.lang.Runnable, org.apache.sling.event.impl.jobs.queues.QueueManager, org.osgi.service.event.EventHandler]"),
+ Matchers.contains("java.lang.Runnable", "org.apache.sling.event.impl.jobs.queues.QueueManager", "org.osgi.service.event.EventHandler"));
+ }
+}