org.eclipse.smarthome.test.SyntheticBundleInstaller.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.smarthome.test.SyntheticBundleInstaller.java

Source

/**
 * 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());
    }
}