/**
* 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.aries.blueprint.compendium.cm;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.jar.JarInputStream;
import de.kalpatec.pojosr.framework.PojoServiceRegistryFactoryImpl;
import de.kalpatec.pojosr.framework.launch.BundleDescriptor;
import de.kalpatec.pojosr.framework.launch.ClasspathScanner;
import de.kalpatec.pojosr.framework.launch.PojoServiceRegistry;
import de.kalpatec.pojosr.framework.launch.PojoServiceRegistryFactory;
import org.ops4j.pax.swissbox.tinybundles.core.TinyBundle;
import org.ops4j.pax.swissbox.tinybundles.core.TinyBundles;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;
import org.osgi.framework.Filter;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.util.tracker.ServiceTracker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
public final class Helper {
public static final long DEFAULT_TIMEOUT = 30000;
public static final String BUNDLE_FILTER = "(Bundle-SymbolicName=*)";
public static final String BUNDLE_VERSION = "1.0.0";
private static final transient Logger LOG = LoggerFactory.getLogger(Helper.class);
private Helper() {
}
public static BundleContext createBundleContext(String name, String descriptors, boolean includeTestBundle) throws Exception {
return createBundleContext(name, descriptors, includeTestBundle, BUNDLE_FILTER, BUNDLE_VERSION);
}
public static BundleContext createBundleContext(String name, String descriptors, boolean includeTestBundle,
String bundleFilter, String testBundleVersion) throws Exception {
TinyBundle bundle = null;
if (includeTestBundle) {
// add ourselves as a bundle
bundle = createTestBundle(name, testBundleVersion, descriptors);
}
return createBundleContext(bundleFilter, new TinyBundle[] { bundle });
}
public static BundleContext createBundleContext(String bundleFilter, TinyBundle[] testBundles) throws Exception {
deleteDirectory("target/bundles");
createDirectory("target/bundles");
// ensure pojosr stores bundles in an unique target directory
System.setProperty("org.osgi.framework.storage", "target/bundles/" + System.currentTimeMillis());
// get the bundles
List<BundleDescriptor> bundles = getBundleDescriptors(bundleFilter);
// Add the test bundles at the beginning of the list so that they get started first.
// The reason is that the bundle tracker used by blueprint does not work well
// with pojosr because it does not support bundle hooks, so events are lost.
if (testBundles != null) {
for (TinyBundle bundle : testBundles) {
File tmp = File.createTempFile("test-", ".jar", new File("target/bundles/"));
tmp.delete();
bundles.add(0, getBundleDescriptor(tmp.getPath(), bundle));
}
}
if (LOG.isDebugEnabled()) {
for (int i = 0; i < bundles.size(); i++) {
BundleDescriptor desc = bundles.get(i);
LOG.debug("Bundle #{} -> {}", i, desc);
}
}
// setup pojosr to use our bundles
Map<String, List<BundleDescriptor>> config = new HashMap<String, List<BundleDescriptor>>();
config.put(PojoServiceRegistryFactory.BUNDLE_DESCRIPTORS, bundles);
// create pojorsr osgi service registry
PojoServiceRegistry reg = new PojoServiceRegistryFactoryImpl().newPojoServiceRegistry(config);
return reg.getBundleContext();
}
public static void disposeBundleContext(BundleContext bundleContext) throws BundleException {
try {
if (bundleContext != null) {
bundleContext.getBundle().stop();
}
} finally {
System.clearProperty("org.osgi.framework.storage");
}
}
public static <T> T getOsgiService(BundleContext bundleContext, Class<T> type, long timeout) {
return getOsgiService(bundleContext, type, null, timeout);
}
public static <T> T getOsgiService(BundleContext bundleContext, Class<T> type) {
return getOsgiService(bundleContext, type, null, DEFAULT_TIMEOUT);
}
public static <T> T getOsgiService(BundleContext bundleContext, Class<T> type, String filter) {
return getOsgiService(bundleContext, type, filter, DEFAULT_TIMEOUT);
}
public static <T> ServiceReference getOsgiServiceReference(BundleContext bundleContext, Class<T> type, String filter, long timeout) {
ServiceTracker tracker = null;
try {
String flt;
if (filter != null) {
if (filter.startsWith("(")) {
flt = "(&(" + Constants.OBJECTCLASS + "=" + type.getName() + ")" + filter + ")";
} else {
flt = "(&(" + Constants.OBJECTCLASS + "=" + type.getName() + ")(" + filter + "))";
}
} else {
flt = "(" + Constants.OBJECTCLASS + "=" + type.getName() + ")";
}
Filter osgiFilter = FrameworkUtil.createFilter(flt);
tracker = new ServiceTracker(bundleContext, osgiFilter, null);
tracker.open(true);
// Note that the tracker is not closed to keep the reference
// This is buggy, as the service reference may change i think
Object svc = tracker.waitForService(timeout);
if (svc == null) {
Dictionary<?, ?> dic = bundleContext.getBundle().getHeaders();
System.err.println("Test bundle headers: " + explode(dic));
for (ServiceReference ref : asCollection(bundleContext.getAllServiceReferences(null, null))) {
System.err.println("ServiceReference: " + ref + ", bundle: " + ref.getBundle() + ", symbolicName: " + ref.getBundle().getSymbolicName());
}
for (ServiceReference ref : asCollection(bundleContext.getAllServiceReferences(null, flt))) {
System.err.println("Filtered ServiceReference: " + ref + ", bundle: " + ref.getBundle() + ", symbolicName: " + ref.getBundle().getSymbolicName());
}
throw new RuntimeException("Gave up waiting for service " + flt);
}
return tracker.getServiceReference();
} catch (InvalidSyntaxException e) {
throw new IllegalArgumentException("Invalid filter", e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
public static <T> T getOsgiService(BundleContext bundleContext, Class<T> type, String filter, long timeout) {
ServiceTracker tracker = null;
try {
String flt;
if (filter != null) {
if (filter.startsWith("(")) {
flt = "(&(" + Constants.OBJECTCLASS + "=" + type.getName() + ")" + filter + ")";
} else {
flt = "(&(" + Constants.OBJECTCLASS + "=" + type.getName() + ")(" + filter + "))";
}
} else {
flt = "(" + Constants.OBJECTCLASS + "=" + type.getName() + ")";
}
Filter osgiFilter = FrameworkUtil.createFilter(flt);
tracker = new ServiceTracker(bundleContext, osgiFilter, null);
tracker.open(true);
// Note that the tracker is not closed to keep the reference
// This is buggy, as the service reference may change i think
Object svc = tracker.waitForService(timeout);
if (svc == null) {
Dictionary<?, ?> dic = bundleContext.getBundle().getHeaders();
System.err.println("Test bundle headers: " + explode(dic));
for (ServiceReference ref : asCollection(bundleContext.getAllServiceReferences(null, null))) {
System.err.println("ServiceReference: " + ref + ", bundle: " + ref.getBundle() + ", symbolicName: " + ref.getBundle().getSymbolicName());
}
for (ServiceReference ref : asCollection(bundleContext.getAllServiceReferences(null, flt))) {
System.err.println("Filtered ServiceReference: " + ref + ", bundle: " + ref.getBundle() + ", symbolicName: " + ref.getBundle().getSymbolicName());
}
throw new RuntimeException("Gave up waiting for service " + flt);
}
return type.cast(svc);
} catch (InvalidSyntaxException e) {
throw new IllegalArgumentException("Invalid filter", e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
protected static TinyBundle createTestBundle(String name, String version, String descriptors) throws FileNotFoundException, MalformedURLException {
TinyBundle bundle = TinyBundles.newBundle();
for (URL url : getBlueprintDescriptors(descriptors)) {
LOG.info("Using Blueprint XML file: " + url.getFile());
bundle.add("OSGI-INF/blueprint/blueprint-" + url.getFile().replace("/", "-"), url);
}
bundle.set("Manifest-Version", "2")
.set("Bundle-ManifestVersion", "2")
.set("Bundle-SymbolicName", name)
.set("Bundle-Version", version);
return bundle;
}
/**
* Explode the dictionary into a <code>,</code> delimited list of <code>key=value</code> pairs.
*/
private static String explode(Dictionary<?, ?> dictionary) {
Enumeration<?> keys = dictionary.keys();
StringBuffer result = new StringBuffer();
while (keys.hasMoreElements()) {
Object key = keys.nextElement();
result.append(String.format("%s=%s", key, dictionary.get(key)));
if (keys.hasMoreElements()) {
result.append(", ");
}
}
return result.toString();
}
/**
* Provides an iterable collection of references, even if the original array is <code>null</code>.
*/
private static Collection<ServiceReference> asCollection(ServiceReference[] references) {
return references == null ? new ArrayList<ServiceReference>(0) : Arrays.asList(references);
}
/**
* Gets list of bundle descriptors.
* @param bundleFilter Filter expression for OSGI bundles.
*
* @return List pointers to OSGi bundles.
* @throws Exception If looking up the bundles fails.
*/
private static List<BundleDescriptor> getBundleDescriptors(final String bundleFilter) throws Exception {
return new ClasspathScanner().scanForBundles(bundleFilter);
}
/**
* Gets the bundle descriptors as {@link URL} resources.
*
* @param descriptors the bundle descriptors, can be separated by comma
* @return the bundle descriptors.
* @throws FileNotFoundException is thrown if a bundle descriptor cannot be found
*/
private static Collection<URL> getBlueprintDescriptors(String descriptors) throws FileNotFoundException, MalformedURLException {
List<URL> answer = new ArrayList<URL>();
String descriptor = descriptors;
if (descriptor != null) {
// there may be more resources separated by comma
Iterator<Object> it = createIterator(descriptor);
while (it.hasNext()) {
String s = (String) it.next();
LOG.trace("Resource descriptor: {}", s);
// remove leading / to be able to load resource from the classpath
s = stripLeadingSeparator(s);
// if there is wildcards for *.xml then we need to find the urls from the package
if (s.endsWith("*.xml")) {
String packageName = s.substring(0, s.length() - 5);
// remove trailing / to be able to load resource from the classpath
Enumeration<URL> urls = loadResourcesAsURL(packageName);
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
File dir = new File(url.getFile());
if (dir.isDirectory()) {
File[] files = dir.listFiles();
if (files != null) {
for (File file : files) {
if (file.isFile() && file.exists() && file.getName().endsWith(".xml")) {
String name = packageName + file.getName();
LOG.debug("Resolving resource: {}", name);
URL xmlUrl = loadResourceAsURL(name);
if (xmlUrl != null) {
answer.add(xmlUrl);
}
}
}
}
}
}
} else {
LOG.debug("Resolving resource: {}", s);
URL url = resolveMandatoryResourceAsUrl(s);
if (url == null) {
throw new FileNotFoundException("Resource " + s + " not found");
}
answer.add(url);
}
}
} else {
throw new IllegalArgumentException("No bundle descriptor configured. Override getBlueprintDescriptor() or getBlueprintDescriptors() method");
}
if (answer.isEmpty()) {
throw new IllegalArgumentException("Cannot find any resources in classpath from descriptor " + descriptors);
}
return answer;
}
private static BundleDescriptor getBundleDescriptor(String path, TinyBundle bundle) throws Exception {
File file = new File(path);
FileOutputStream fos = new FileOutputStream(file, true);
try {
copy(bundle.build(), fos);
} finally {
close(fos);
}
FileInputStream fis = null;
JarInputStream jis = null;
try {
fis = new FileInputStream(file);
jis = new JarInputStream(fis);
Map<String, String> headers = new HashMap<String, String>();
for (Map.Entry<Object, Object> entry : jis.getManifest().getMainAttributes().entrySet()) {
headers.put(entry.getKey().toString(), entry.getValue().toString());
}
return new BundleDescriptor(
bundle.getClass().getClassLoader(),
new URL("jar:" + file.toURI().toString() + "!/"),
headers);
} finally {
close(fis, jis);
}
}
/**
* Closes the given resource if it is available, logging any closing exceptions to the given log.
*
* @param closeable the object to close
* @param name the name of the resource
* @param log the log to use when reporting closure warnings, will use this class's own {@link Logger} if <tt>log == null</tt>
*/
public static void close(Closeable closeable, String name, Logger log) {
if (closeable != null) {
try {
closeable.close();
} catch (IOException e) {
if (log == null) {
// then fallback to use the own Logger
log = LOG;
}
if (name != null) {
log.warn("Cannot close: " + name + ". Reason: " + e.getMessage(), e);
} else {
log.warn("Cannot close. Reason: " + e.getMessage(), e);
}
}
}
}
/**
* Closes the given resource if it is available.
*
* @param closeable the object to close
* @param name the name of the resource
*/
public static void close(Closeable closeable, String name) {
close(closeable, name, LOG);
}
/**
* Closes the given resource if it is available.
*
* @param closeable the object to close
*/
public static void close(Closeable closeable) {
close(closeable, null, LOG);
}
/**
* Closes the given resources if they are available.
*
* @param closeables the objects to close
*/
public static void close(Closeable... closeables) {
for (Closeable closeable : closeables) {
close(closeable);
}
}
private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;
private static final String DEFAULT_DELIMITER = ",";
public static int copy(InputStream input, OutputStream output) throws IOException {
return copy(input, output, DEFAULT_BUFFER_SIZE);
}
public static int copy(final InputStream input, final OutputStream output, int bufferSize) throws IOException {
int avail = input.available();
if (avail > 262144) {
avail = 262144;
}
if (avail > bufferSize) {
bufferSize = avail;
}
final byte[] buffer = new byte[bufferSize];
int n = input.read(buffer);
int total = 0;
while (-1 != n) {
output.write(buffer, 0, n);
total += n;
n = input.read(buffer);
}
output.flush();
return total;
}
/**
* Creates an iterator over the value if the value is a collection, an
* Object[], a String with values separated by comma,
* or a primitive type array; otherwise to simplify the caller's code,
* we just create a singleton collection iterator over a single value
* <p/>
* Will default use comma for String separating String values.
* This method does <b>not</b> allow empty values
*
* @param value the value
* @return the iterator
*/
public static Iterator<Object> createIterator(Object value) {
return createIterator(value, DEFAULT_DELIMITER);
}
/**
* Creates an iterator over the value if the value is a collection, an
* Object[], a String with values separated by the given delimiter,
* or a primitive type array; otherwise to simplify the caller's
* code, we just create a singleton collection iterator over a single value
* <p/>
* This method does <b>not</b> allow empty values
*
* @param value the value
* @param delimiter delimiter for separating String values
* @return the iterator
*/
public static Iterator<Object> createIterator(Object value, String delimiter) {
return createIterator(value, delimiter, false);
}
/**
* Creates an iterator over the value if the value is a collection, an
* Object[], a String with values separated by the given delimiter,
* or a primitive type array; otherwise to simplify the caller's
* code, we just create a singleton collection iterator over a single value
*
* @param value the value
* @param delimiter delimiter for separating String values
* @param allowEmptyValues whether to allow empty values
* @return the iterator
*/
@SuppressWarnings("unchecked")
public static Iterator<Object> createIterator(Object value, String delimiter, final boolean allowEmptyValues) {
if (value == null) {
return Collections.emptyList().iterator();
} else if (value instanceof Iterator) {
return (Iterator<Object>)value;
} else if (value instanceof Iterable) {
return ((Iterable<Object>)value).iterator();
} else if (value.getClass().isArray()) {
// TODO we should handle primitive array types?
List<Object> list = Arrays.asList((Object[])value);
return list.iterator();
} else if (value instanceof NodeList) {
// lets iterate through DOM results after performing XPaths
final NodeList nodeList = (NodeList) value;
return cast(new Iterator<Node>() {
int idx = -1;
public boolean hasNext() {
return (idx + 1) < nodeList.getLength();
}
public Node next() {
idx++;
return nodeList.item(idx);
}
public void remove() {
throw new UnsupportedOperationException();
}
});
} else if (value instanceof String) {
final String s = (String) value;
// this code is optimized to only use a Scanner if needed, eg there is a delimiter
if (delimiter != null && s.contains(delimiter)) {
// use a scanner if it contains the delimiter
Scanner scanner = new Scanner((String)value);
if (DEFAULT_DELIMITER.equals(delimiter)) {
// we use the default delimiter which is a comma, then cater for bean expressions with OGNL
// which may have balanced parentheses pairs as well.
// if the value contains parentheses we need to balance those, to avoid iterating
// in the middle of parentheses pair, so use this regular expression (a bit hard to read)
// the regexp will split by comma, but honor parentheses pair that may include commas
// as well, eg if value = "bean=foo?method=killer(a,b),bean=bar?method=great(a,b)"
// then the regexp will split that into two:
// -> bean=foo?method=killer(a,b)
// -> bean=bar?method=great(a,b)
// http://stackoverflow.com/questions/1516090/splitting-a-title-into-separate-parts
delimiter = ",(?!(?:[^\\(,]|[^\\)],[^\\)])+\\))";
}
scanner.useDelimiter(delimiter);
return cast(scanner);
} else {
// use a plain iterator that returns the value as is as there are only a single value
return cast(new Iterator<String>() {
int idx = -1;
public boolean hasNext() {
return idx + 1 == 0 && (allowEmptyValues || isNotEmpty(s));
}
public String next() {
idx++;
return s;
}
public void remove() {
throw new UnsupportedOperationException();
}
});
}
} else {
return Collections.singletonList(value).iterator();
}
}
/**
* Tests whether the value is <b>not</b> <tt>null</tt> or an empty string.
*
* @param value the value, if its a String it will be tested for text length as well
* @return true if <b>not</b> empty
*/
public static boolean isNotEmpty(Object value) {
if (value == null) {
return false;
} else if (value instanceof String) {
String text = (String) value;
return text.trim().length() > 0;
} else {
return true;
}
}
public static <T> Iterator<T> cast(Iterator<?> p) {
return (Iterator<T>) p;
}
/**
* Strip any leading separators
*/
public static String stripLeadingSeparator(String name) {
if (name == null) {
return null;
}
while (name.startsWith("/") || name.startsWith(File.separator)) {
name = name.substring(1);
}
return name;
}
/**
* Attempts to load the given resources from the given package name using the thread context
* class loader or the class loader used to load this class
*
* @param packageName the name of the package to load its resources
* @return the URLs for the resources or null if it could not be loaded
*/
public static Enumeration<URL> loadResourcesAsURL(String packageName) {
Enumeration<URL> url = null;
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if (contextClassLoader != null) {
try {
url = contextClassLoader.getResources(packageName);
} catch (IOException e) {
// ignore
}
}
if (url == null) {
try {
url = Helper.class.getClassLoader().getResources(packageName);
} catch (IOException e) {
// ignore
}
}
return url;
}
/**
* Attempts to load the given resource as a stream using the thread context
* class loader or the class loader used to load this class
*
* @param name the name of the resource to load
* @return the stream or null if it could not be loaded
*/
public static URL loadResourceAsURL(String name) {
URL url = null;
String resolvedName = resolveUriPath(name);
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if (contextClassLoader != null) {
url = contextClassLoader.getResource(resolvedName);
}
if (url == null) {
url = Helper.class.getClassLoader().getResource(resolvedName);
}
return url;
}
/**
* Helper operation used to remove relative path notation from
* resources. Most critical for resources on the Classpath
* as resource loaders will not resolve the relative paths correctly.
*
* @param name the name of the resource to load
* @return the modified or unmodified string if there were no changes
*/
private static String resolveUriPath(String name) {
String answer = name;
if (answer.indexOf("//") > -1) {
answer = answer.replaceAll("//", "/");
}
if (answer.indexOf("../") > -1) {
answer = answer.replaceAll("[A-Za-z0-9]*/\\.\\./", "");
}
if (answer.indexOf("./") > -1) {
answer = answer.replaceAll("\\./", "");
}
return answer;
}
/**
* Resolves the mandatory resource.
*
* @param uri uri of the resource
* @return the resource as an {@link InputStream}. Remember to close this stream after usage.
* @throws java.io.FileNotFoundException is thrown if the resource file could not be found
* @throws java.net.MalformedURLException if the URI is malformed
*/
public static URL resolveMandatoryResourceAsUrl(String uri) throws FileNotFoundException, MalformedURLException {
if (uri.startsWith("file:")) {
// check if file exists first
String name = after(uri, "file:");
File file = new File(name);
if (!file.exists()) {
throw new FileNotFoundException("File " + file + " not found");
}
return new URL(uri);
} else if (uri.startsWith("http:")) {
return new URL(uri);
} else if (uri.startsWith("classpath:")) {
uri = after(uri, "classpath:");
}
// load from classpath by default
URL url = loadResourceAsURL(uri);
if (url == null) {
throw new FileNotFoundException("Cannot find resource in classpath for URI: " + uri);
} else {
return url;
}
}
public static String after(String text, String after) {
if (!text.contains(after)) {
return null;
}
return text.substring(text.indexOf(after) + after.length());
}
/**
* Recursively delete a directory, useful to zapping test data
*
* @param file the directory to be deleted
* @return <tt>false</tt> if error deleting directory
*/
public static boolean deleteDirectory(String file) {
return deleteDirectory(new File(file));
}
/**
* Recursively delete a directory, useful to zapping test data
*
* @param file the directory to be deleted
* @return <tt>false</tt> if error deleting directory
*/
public static boolean deleteDirectory(File file) {
int tries = 0;
int maxTries = 5;
boolean exists = true;
while (exists && (tries < maxTries)) {
recursivelyDeleteDirectory(file);
tries++;
exists = file.exists();
if (exists) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// Ignore
}
}
}
return !exists;
}
private static void recursivelyDeleteDirectory(File file) {
if (!file.exists()) {
return;
}
if (file.isDirectory()) {
File[] files = file.listFiles();
for (File child : files) {
recursivelyDeleteDirectory(child);
}
}
boolean success = file.delete();
if (!success) {
LOG.warn("Deletion of file: " + file.getAbsolutePath() + " failed");
}
}
/**
* create the directory
*
* @param file the directory to be created
*/
public static void createDirectory(String file) {
File dir = new File(file);
dir.mkdirs();
}
}