This is an automated email from the ASF dual-hosted git repository. kwin pushed a commit to branch feature/wait-for-component in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-testing-clients.git
commit a9aa186c5242c6c38b6b92f56d586aea62a273bb Author: Konrad Windszus <[email protected]> AuthorDate: Sun Dec 10 12:22:15 2017 +0100 SLING-7297 add methods to wait for a component and a service --- pom.xml | 6 + .../testing/clients/osgi/OsgiConsoleClient.java | 153 ++++++++++++++++++++- .../sling/testing/clients/osgi/ServiceInfo.java | 73 ++++++++++ .../sling/testing/clients/osgi/ServicesInfo.java | 142 +++++++++++++++++++ .../sling/testing/clients/osgi/package-info.java | 2 +- .../sling/testing/clients/util/poller/Polling.java | 2 +- .../testing/clients/osgi/ServicesInfoTest.java | 34 +++++ 7 files changed, 403 insertions(+), 9 deletions(-) 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.FileInputStream; 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 @@ public class OsgiConsoleClient extends SlingClient { * 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 @@ public class OsgiConsoleClient extends SlingClient { 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 @@ public class OsgiConsoleClient extends SlingClient { } /** - * 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 @@ public class OsgiConsoleClient extends SlingClient { } /** - * 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 @@ public class OsgiConsoleClient extends SlingClient { @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 @@ public class Polling implements Callable<Boolean> { /** * 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")); + } +} -- To stop receiving notification emails like this one, please contact "[email protected]" <[email protected]>.
