/**
* Copyright (c) 2014-2017 by the respective copyright holders.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.smarthome.test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import org.apache.commons.io.IOUtils;
import org.eclipse.smarthome.config.xml.osgi.AbstractAsyncBundleProcessor;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import com.google.common.collect.ImmutableSet;
/**
* Utility class for creation, installation, update and uninstallation of
* synthetic bundles for the purpose of testing. The synthetic bundles content
* should be stored into separate sub-directories of {@value #bundlePoolPath}
* (which itself is situated in the test bundle's source directory). The
* synthetic bundle is packed as a JAR and installed into the test runtime.
*
* @author Alex Tugarev - Initial contribution
* @author Dennis Nobel - Generalized the mechanism for creation of bundles by list of extensions to include
* @author Simon Kaufmann - Install method returns when the bundle is fully loaded
* @author Stefan Bussweiler - The list of extensions to include is extended with JSON
* @author Andre Fuechsel - Implemented method for adding fragment
* @author Kai Kreuzer - Applied formatting and license to the file
* @author Dimitar Ivanov - The extension to include can be configured or default ones can be used; update method is
* introduced
*
*/
public class SyntheticBundleInstaller {
private static String bundlePoolPath = "/test-bundle-pool";
/**
* A list of default extensions to be included in the synthetic bundle.
*/
private static Set<String> DEFAULT_EXTENSIONS = ImmutableSet.of("*.xml", "*.properties", "*.json", ".keep");
/**
* Install synthetic bundle, denoted by its name, into the test runtime (by using the given bundle context). Only
* the default extensions set
* ({@link #DEFAULT_EXTENSIONS}) will be included into the synthetic bundle
*
* @param bundleContext the bundle context of the test runtime
* @param testBundleName the symbolic name of the sub-directory of {@value #bundlePoolPath}, which contains the
* files for the synthetic bundle
* @return the synthetic bundle representation
* @throws Exception thrown when error occurs while installing or starting the synthetic bundle
*/
public static Bundle install(BundleContext bundleContext, String testBundleName) throws Exception {
return install(bundleContext, testBundleName, DEFAULT_EXTENSIONS);
}
/**
* Install synthetic bundle, denoted by its name, into the test runtime (by using the given bundle context).
*
* @param bundleContext the bundle context of the test runtime
* @param testBundleNamethe symbolic name of the sub-directory of {@value #bundlePoolPath}, which contains the files
* for the synthetic bundle
* @param extensionsToInclude a list of extension to be included into the synthetic bundle. In order to use the list
* of default extensions ({@link #DEFAULT_EXTENSIONS})
*
* @return the synthetic bundle representation
* @throws Exception thrown when error occurs while installing or starting the synthetic bundle
*/
public static Bundle install(BundleContext bundleContext, String testBundleName, Set<String> extensionsToInclude)
throws Exception {
String bundlePath = bundlePoolPath + "/" + testBundleName + "/";
byte[] syntheticBundleBytes = createSyntheticBundle(bundleContext.getBundle(), bundlePath, testBundleName,
extensionsToInclude);
Bundle syntheticBundle = bundleContext.installBundle(testBundleName,
new ByteArrayInputStream(syntheticBundleBytes));
syntheticBundle.start(Bundle.ACTIVE);
waitUntilLoadingFinished(syntheticBundle);
return syntheticBundle;
}
/**
* Install synthetic bundle, denoted by its name, into the test runtime (by using the given bundle context).
*
* @param bundleContext the bundle context of the test runtime
* @param testBundleName the symbolic name of the sub-directory of {@value #bundlePoolPath}, which contains the
* files for the synthetic bundle
* @param extensionsToInclude a list of extension to be included into the synthetic bundle
*
* @return the synthetic bundle representation
* @throws Exception thrown when error occurs while installing or starting the synthetic bundle
*/
public static Bundle install(BundleContext bundleContext, String testBundleName, String... extensionsToInclude)
throws Exception {
Set<String> extensionsSet = new HashSet<>(Arrays.asList(extensionsToInclude));
return install(bundleContext, testBundleName, extensionsSet);
}
/**
* Updates given bundle into the test runtime (the content is changed, but the symbolic name of the bundles remains
* the same) with a new content, prepared in another resources directory.
*
* @param bundleContext the bundle context of the test runtime
* @param bundleToUpdateName the symbolic name of the bundle to be updated
* @param updateDirName the location of the new content, that the target bundle will be updated with
* @return the Bundle representation of the updated bundle
* @throws Exception thrown when error occurs while installing or starting the synthetic bundle
*/
public static Bundle update(BundleContext bundleContext, String bundleToUpdateName, String updateDirName)
throws Exception {
return update(bundleContext, bundleToUpdateName, updateDirName, DEFAULT_EXTENSIONS);
}
/**
* Updates given bundle into the test runtime (the content is changed, but the symbolic name of the bundles remains
* the same) with a new content, prepared in another resources directory.
*
* @param bundleContext the bundle context of the test runtime
* @param bundleToUpdateName the symbolic name of the bundle to be updated
* @param updateDirName the location of the new content, that the target bundle will be updated with
* @param extensionsToInclude a list of extension to be included into the synthetic bundle
* @return the Bundle representation of the updated bundle
* @throws Exception thrown when error occurs while installing or starting the synthetic bundle
*/
public static Bundle update(BundleContext bundleContext, String bundleToUpdateName, String updateDirName,
Set<String> extensionsToInclude) throws Exception {
// Stop the bundle to update first
Bundle[] bundles = bundleContext.getBundles();
for (Bundle bundle : bundles) {
if (bundleToUpdateName.equals(bundle.getSymbolicName())) {
// we have to uninstall the bundle to update its contents
bundle.uninstall();
break;
}
}
// New bytes are taken from the update path
String updatePath = bundlePoolPath + "/" + updateDirName + "/";
byte[] updatedBundleBytes = createSyntheticBundle(bundleContext.getBundle(), updatePath, bundleToUpdateName,
extensionsToInclude);
// The updated bytes are installed with the same name
Bundle syntheticBundle = bundleContext.installBundle(bundleToUpdateName,
new ByteArrayInputStream(updatedBundleBytes));
// Starting the bundle
syntheticBundle.start(Bundle.ACTIVE);
waitUntilLoadingFinished(syntheticBundle);
return syntheticBundle;
}
/**
* Updates given bundle into the test runtime (the content is changed, but the symbolic name of the bundles remains
* the same) with a new content, prepared in another resources directory.
*
* @param bundleContextthe bundle context of the test runtime
* @param bundleToUpdateName the symbolic name of the bundle to be updated
* @param updateDirName the location of the new content, that the target bundle will be updated with
* @param extensionsToInclude a list of extension to be included into the synthetic bundle
* @return the Bundle representation of the updated bundle
* @throws Exception thrown when error occurs while installing or starting the synthetic bundle
*/
public static Bundle update(BundleContext bundleContext, String bundleToUpdateName, String updateDirName,
String... extensionsToInclude) throws Exception {
Set<String> extensionsSet = new HashSet<>(Arrays.asList(extensionsToInclude));
return update(bundleContext, bundleToUpdateName, updateDirName, extensionsSet);
}
/**
* Install synthetic bundle fragment, denoted by its name, into the test
* runtime (by using the given bundle context). Only the default extensions
* set ({@link #DEFAULT_EXTENSIONS}) will be included into the synthetic
* bundle fragment.
*
* @param bundleContext the bundle context of the test runtime
* @param testBundleName the name of the sub-directory of {@value #bundlePoolPath}, which contains the files for the
* synthetic bundle
* @param extensionsToInclude a list of extension to be included into the synthetic bundle fragment. In order to use
* the list of default extensions ({@link #DEFAULT_EXTENSIONS})
* @return the synthetic bundle representation
* @throws Exception thrown when error occurs while installing or starting the synthetic bundle fragment
*/
public static Bundle installFragment(BundleContext bundleContext, String testBundleName) throws Exception {
return installFragment(bundleContext, testBundleName, DEFAULT_EXTENSIONS);
}
/**
* Install synthetic bundle fragment, denoted by its name, into the test runtime (by using the given bundle
* context). Only the default extensions set ({@link #DEFAULT_EXTENSIONS}) will be included into the synthetic
* bundle fragment.
*
* @param bundleContext the bundle context of the test runtime
* @param testBundleName the name of the sub-directory of {@value #bundlePoolPath}, which contains the files for the
* synthetic bundle
* @return the synthetic bundle representation
* @throws Exception thrown when error occurs while installing or starting the synthetic bundle fragment
*/
public static Bundle installFragment(BundleContext bundleContext, String testBundleName,
Set<String> extensionsToInclude) throws Exception {
String bundlePath = bundlePoolPath + "/" + testBundleName + "/";
byte[] syntheticBundleBytes = createSyntheticBundle(bundleContext.getBundle(), bundlePath, testBundleName,
extensionsToInclude);
Bundle syntheticBundle = bundleContext.installBundle(testBundleName,
new ByteArrayInputStream(syntheticBundleBytes));
waitUntilLoadingFinished(syntheticBundle);
return syntheticBundle;
}
/**
* Explicitly wait for the given bundle to finish its loading
*
* @param bundle the bundle object representation
*/
public static void waitUntilLoadingFinished(Bundle bundle) {
while (!AbstractAsyncBundleProcessor.isBundleFinishedLoading(bundle)) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
/**
* Uninstalls the synthetic bundle (or bundle fragment), denoted by its name, from the test runtime.
*
* @param bundleContext the bundle context of the test runtime
* @param testBundleName the name of the test bundle to be uninstalled
* @throws BundleException if error is met during the bundle uninstall
*/
public static void uninstall(BundleContext bundleContext, String testBundleName) throws BundleException {
Bundle[] bundles = bundleContext.getBundles();
for (Bundle bundle : bundles) {
if (testBundleName.equals(bundle.getSymbolicName())) {
bundle.uninstall();
}
}
}
private static byte[] createSyntheticBundle(Bundle bundle, String bundlePath, String bundleName,
Set<String> extensionsToInclude) throws Exception {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
Manifest manifest = getManifest(bundle, bundlePath);
JarOutputStream jarOutputStream = manifest != null ? new JarOutputStream(outputStream, manifest)
: new JarOutputStream(outputStream);
List<String> files = collectFilesFrom(bundle, bundlePath, bundleName, extensionsToInclude);
for (String file : files) {
addFileToArchive(bundle, bundlePath, file, jarOutputStream);
}
jarOutputStream.close();
return outputStream.toByteArray();
}
private static void addFileToArchive(Bundle bundle, String bundlePath, String fileInBundle,
JarOutputStream jarOutputStream) throws IOException {
String filePath = bundlePath + "/" + fileInBundle;
URL resource = bundle.getResource(filePath);
if (resource == null) {
return;
}
ZipEntry zipEntry = new ZipEntry(fileInBundle);
jarOutputStream.putNextEntry(zipEntry);
IOUtils.copy(resource.openStream(), jarOutputStream);
jarOutputStream.closeEntry();
}
private static List<String> collectFilesFrom(Bundle bundle, String bundlePath, String bundleName,
Set<String> extensionsToInclude) throws Exception {
List<String> result = new ArrayList<>();
URL url = getBaseURL(bundle, bundleName);
if (url != null) {
String path = url.getPath();
URI baseURI = url.toURI();
List<URL> list = collectEntries(bundle, path, extensionsToInclude);
for (URL entryURL : list) {
String fileEntry = convertToFileEntry(baseURI, entryURL);
result.add(fileEntry);
}
}
return result;
}
private static URL getBaseURL(Bundle bundle, String bundleName) {
Enumeration<URL> entries = bundle.findEntries("/", bundleName, true);
return entries != null ? entries.nextElement() : null;
}
private static List<URL> collectEntries(Bundle bundle, String path, Set<String> extensionsToInclude) {
List<URL> result = new ArrayList<>();
for (String filePattern : extensionsToInclude) {
Enumeration<URL> entries = bundle.findEntries(path, filePattern, true);
if (entries != null) {
result.addAll(Collections.list(entries));
}
}
return result;
}
private static String convertToFileEntry(URI baseURI, URL entryURL) throws URISyntaxException {
URI entryURI = entryURL.toURI();
URI relativeURI = baseURI.relativize(entryURI);
String fileEntry = relativeURI.toString();
return fileEntry;
}
private static Manifest getManifest(Bundle bundle, String bundlePath) throws IOException {
String filePath = bundlePath + "/" + "META-INF/MANIFEST.MF";
URL resource = bundle.getResource(filePath);
if (resource == null) {
return null;
}
return new Manifest(resource.openStream());
}
}