org.eclipse.gemini.blueprint.test.AbstractOnTheFlyBundleCreatorTests.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.gemini.blueprint.test.AbstractOnTheFlyBundleCreatorTests.java

Source

/******************************************************************************
 * Copyright (c) 2006, 2010 VMware Inc.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * and Apache License v2.0 which accompanies this distribution. 
 * The Eclipse Public License is available at 
 * http://www.eclipse.org/legal/epl-v10.html and the Apache License v2.0
 * is available at http://www.opensource.org/licenses/apache2.0.php.
 * You may elect to redistribute this code under either of these licenses. 
 * 
 * Contributors:
 *   VMware Inc.
 *****************************************************************************/

package org.eclipse.gemini.blueprint.test;

import java.io.IOException;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.Manifest;

import org.eclipse.gemini.blueprint.test.internal.util.DependencyVisitor;
import org.eclipse.gemini.blueprint.test.internal.util.jar.JarCreator;
import org.eclipse.gemini.blueprint.util.OsgiStringUtils;
import org.objectweb.asm.ClassReader;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

/**
 * Enhanced subclass of {@link AbstractDependencyManagerTests} that facilitates
 * OSGi testing by creating at runtime, on the fly, a jar using the indicated
 * manifest and resource patterns (by default all files found under the root
 * path).
 * 
 * <p/>The test class can automatically determine the imports required by the
 * test, create the OSGi bundle manifest and pack the test and its resources in
 * a jar that can be installed inside an OSGi platform.
 * 
 * <p/>Additionally, a valid OSGi manifest is automatically created for the
 * resulting test if the user does not provide one. The classes present in the
 * archive are analyzed and based on their byte-code, the required
 * <code>Import-Package</code> entries (for packages not found in the bundle)
 * are created.
 * 
 * Please see the reference documentation for an in-depth explanation and usage
 * examples.
 * 
 * <p/>Note that in more complex scenarios, dedicated packaging tools (such as
 * ant scripts or maven2) should be used.
 * 
 * <p/>It is recommend to extend {@link AbstractConfigurableBundleCreatorTests}
 * rather then this class as the former offers sensible defaults.
 * 
 * @author Costin Leau
 * 
 */
public abstract class AbstractOnTheFlyBundleCreatorTests extends AbstractDependencyManagerTests {

    private static final String META_INF_JAR_LOCATION = "/META-INF/MANIFEST.MF";

    JarCreator jarCreator;

    /** field used for caching jar content */
    private Map jarEntries;
    /** discovered manifest */
    private Manifest manifest;

    public AbstractOnTheFlyBundleCreatorTests() {
        initializeJarCreator();
    }

    public AbstractOnTheFlyBundleCreatorTests(String testName) {
        super(testName);
        initializeJarCreator();
    }

    private void initializeJarCreator() {
        AccessController.doPrivileged(new PrivilegedAction() {

            public Object run() {
                jarCreator = new JarCreator();
                return null;
            }
        });
    }

    /**
     * Returns the root path used for locating the resources that will be packed
     * in the test bundle (the root path does not become part of the jar).
     *
      * <p/>By default, the current threads context ClassLoader is used to locate
      * the root of the classpath. Because unit tests will either be run from Maven
      * or an IDE this will resolve a test classes directory of sorts.
      *
      * <p/>For example when invoked from Maven <code>"file:./target/test-classes"</code>
      * will be resolved and used.
      *
     * @return root path given as a String
     */
    protected String getRootPath() {
        return Thread.currentThread().getContextClassLoader().getResource(".").toString();
    }

    /**
     * Returns the patterns used for identifying the resources added to the jar.
     * The patterns are added to the root path when performing the search. By
     * default, the pattern is <code>*&#42;/*</code>.
     * 
     * <p/>In large test environments, performance can be improved by limiting
     * the resource added to the bundle by selecting only certain packages or
     * classes. This results in a small test bundle which is faster to create,
     * deploy and install.
     * 
     * @return the patterns identifying the resources added to the jar
     */
    protected String[] getBundleContentPattern() {
        return new String[] { JarCreator.EVERYTHING_PATTERN };
    }

    /**
     * Returns the location (in Spring resource style) of the manifest location
     * to be used. By default <code>null</code> is returned, indicating that
     * the manifest should be picked up from the bundle content (if it's
     * available) or be automatically created based on the test class imports.
     * 
     * @return the manifest location
     * @see #getManifest()
     * @see #createDefaultManifest()
     */
    protected String getManifestLocation() {
        return null;
    }

    /**
     * Returns the current test bundle manifest. The method tries to read the
     * manifest from the given location; in case the location is
     * <code>null</code> (default), it will search for
     * <code>META-INF/MANIFEST.MF</code> file in jar content (as specified
     * through the patterns) and, if it cannot find the file,
     * <em>automatically</em> create a <code>Manifest</code> object
     * containing default entries.
     * 
     * <p/> Subclasses can override this method to enhance the returned
     * Manifest.
     * 
     * @return Manifest used for this test suite.
     * 
     * @see #createDefaultManifest()
     */
    protected Manifest getManifest() {
        // return cached manifest
        if (manifest != null)
            return manifest;

        String manifestLocation = getManifestLocation();
        if (StringUtils.hasText(manifestLocation)) {
            logger.info("Using Manifest from specified location=[" + getManifestLocation() + "]");
            DefaultResourceLoader loader = new DefaultResourceLoader();
            manifest = createManifestFrom(loader.getResource(manifestLocation));
        }

        else {
            // set root path
            jarCreator.setRootPath(getRootPath());
            // add the content pattern
            jarCreator.setContentPattern(getBundleContentPattern());

            // see if the manifest already exists in the classpath
            // to resolve the patterns
            jarEntries = jarCreator.resolveContent();

            for (Iterator iterator = jarEntries.entrySet().iterator(); iterator.hasNext();) {
                Map.Entry entry = (Map.Entry) iterator.next();
                if (META_INF_JAR_LOCATION.equals(entry.getKey())) {
                    logger.info("Using Manifest from the test bundle content=[/META-INF/MANIFEST.MF]");
                    manifest = createManifestFrom((Resource) entry.getValue());
                }
            }
            // fallback to default manifest creation

            if (manifest == null) {
                logger.info("Automatically creating Manifest for the test bundle");
                manifest = createDefaultManifest();
            }
        }

        return manifest;
    }

    /**
     * Indicates if the automatic manifest creation should consider only the
     * test class (<code>true</code>) or all classes included in the test
     * bundle(<code>false</code>). The latter should be used when the test
     * bundle contains additional classes that help with the test case.
     * 
     * <p/> By default, this method returns <code>true</code>, meaning that
     * only the test class will be searched for dependencies.
     * 
     * @return true if only the test hierarchy is searched for dependencies or
     *         false if all classes discovered in the test archive need to be
     *         parsed.
     */
    protected boolean createManifestOnlyFromTestClass() {
        return true;
    }

    private Manifest createManifestFrom(Resource resource) {
        Assert.notNull(resource, "unable to create manifest for empty resources");
        try {
            return new Manifest(resource.getInputStream());
        } catch (IOException ex) {
            throw (RuntimeException) new IllegalArgumentException("cannot create manifest from " + resource)
                    .initCause(ex);
        }
    }

    /**
     * Creates the default manifest in case none if found on the disk. By
     * default, the imports are synthetised based on the test class bytecode.
     * 
     * @return default manifest for the jar created on the fly
     */
    protected Manifest createDefaultManifest() {
        Manifest manifest = new Manifest();
        Attributes attrs = manifest.getMainAttributes();

        // manifest versions
        attrs.put(Attributes.Name.MANIFEST_VERSION, "1.0");
        attrs.putValue(Constants.BUNDLE_MANIFESTVERSION, "2");

        String description = getName() + "-" + getClass().getName();
        // name/description
        attrs.putValue(Constants.BUNDLE_NAME, "TestBundle-" + description);
        attrs.putValue(Constants.BUNDLE_SYMBOLICNAME, "TestBundle-" + description);
        attrs.putValue(Constants.BUNDLE_DESCRIPTION, "on-the-fly test bundle");

        // activator
        attrs.putValue(Constants.BUNDLE_ACTIVATOR, JUnitTestActivator.class.getName());

        // add Import-Package entry
        addImportPackage(manifest);

        if (logger.isDebugEnabled())
            logger.debug("Created manifest:" + manifest.getMainAttributes().entrySet());
        return manifest;
    }

    private void addImportPackage(Manifest manifest) {
        String[] rawImports = determineImports();

        boolean trace = logger.isTraceEnabled();

        if (trace)
            logger.trace("Discovered raw imports " + ObjectUtils.nullSafeToString(rawImports));

        Collection specialImportsOut = eliminateSpecialPackages(rawImports);
        Collection imports = eliminatePackagesAvailableInTheJar(specialImportsOut);

        if (trace)
            logger.trace("Filtered imports are " + imports);

        manifest.getMainAttributes().putValue(Constants.IMPORT_PACKAGE,
                StringUtils.collectionToCommaDelimitedString(imports));
    }

    /**
     * Eliminate 'special' packages (java.*, test framework internal and the
     * class declaring package)
     * 
     * @param rawImports
     * @return
     */
    private Collection eliminateSpecialPackages(String[] rawImports) {
        String currentPckg = ClassUtils.classPackageAsResourcePath(getClass()).replace('/', '.');

        Set filteredImports = new LinkedHashSet(rawImports.length);
        Set eliminatedImports = new LinkedHashSet(4);

        for (int i = 0; i < rawImports.length; i++) {
            String pckg = rawImports[i];

            if (!(pckg.startsWith("java.") || pckg.startsWith("org.eclipse.gemini.blueprint.test.internal")
                    || pckg.equals(currentPckg)))
                filteredImports.add(pckg);
            else
                eliminatedImports.add(pckg);
        }

        if (!eliminatedImports.isEmpty() && logger.isTraceEnabled())
            logger.trace("Eliminated special packages " + eliminatedImports);

        return filteredImports;
    }

    /**
     * Eliminates imports for packages already included in the bundle. Works
     * only if the jar content is known (variable 'jarEntries' set).
     * 
     * @param imports
     * @return
     */
    private Collection eliminatePackagesAvailableInTheJar(Collection imports) {
        // no jar entry present, bail out.
        if (jarEntries == null || jarEntries.isEmpty())
            return imports;

        Set filteredImports = new LinkedHashSet(imports.size());
        Collection eliminatedImports = new LinkedHashSet(2);

        Collection jarPackages = jarCreator.getContainedPackages();
        for (Iterator iterator = imports.iterator(); iterator.hasNext();) {
            String pckg = (String) iterator.next();
            if (jarPackages.contains(pckg))
                eliminatedImports.add(pckg);
            else
                filteredImports.add(pckg);
        }
        if (!eliminatedImports.isEmpty() && logger.isTraceEnabled())
            logger.trace("Eliminated packages already present in the bundle " + eliminatedImports);

        return filteredImports;
    }

    /**
     * Determine imports for the given bundle. Based on the user settings, this
     * method will consider only the the test hierarchy until the testing
     * framework is found or all classes available inside the test bundle. <p/>
     * Note that split packages are not supported.
     * 
     * @return
     */
    private String[] determineImports() {

        boolean useTestClassOnly = false;

        // no jar entry present, bail out.
        if (jarEntries == null || jarEntries.isEmpty()) {
            logger.debug("No test jar content detected, generating bundle imports from the test class");
            useTestClassOnly = true;
        }

        else if (createManifestOnlyFromTestClass()) {
            logger.info("Using the test class for generating bundle imports");
            useTestClassOnly = true;
        } else
            logger.info("Using all classes in the jar for the generation of bundle imports");

        // className, class resource
        Map entries;

        if (useTestClassOnly) {

            entries = new LinkedHashMap(4);

            // get current class (test class that bootstraps the OSGi infrastructure)
            Class<?> clazz = getClass();
            String clazzPackage = null;
            String endPackage = AbstractOnTheFlyBundleCreatorTests.class.getPackage().getName();

            do {

                // consider inner classes as well
                List classes = new ArrayList(4);
                classes.add(clazz);
                CollectionUtils.mergeArrayIntoCollection(clazz.getDeclaredClasses(), classes);

                for (Iterator iterator = classes.iterator(); iterator.hasNext();) {
                    Class<?> classToInspect = (Class) iterator.next();

                    Package pkg = classToInspect.getPackage();
                    if (pkg != null) {
                        clazzPackage = pkg.getName();
                        String classFile = ClassUtils.getClassFileName(classToInspect);
                        entries.put(classToInspect.getName().replace('.', '/').concat(ClassUtils.CLASS_FILE_SUFFIX),
                                new InputStreamResource(classToInspect.getResourceAsStream(classFile)));
                    }
                    // handle default package
                    else {
                        logger.warn("Could not find package for class " + classToInspect + "; ignoring...");
                    }
                }

                clazz = clazz.getSuperclass();

            } while (!endPackage.equals(clazzPackage));
        } else
            entries = jarEntries;

        return determineImportsFor(entries);

    }

    private String[] determineImportsFor(Map entries) {
        // get contained packages to do matching on the test hierarchy
        Collection containedPackages = jarCreator.getContainedPackages();
        Set cumulatedPackages = new LinkedHashSet();

        // make sure the collection package is valid
        boolean validPackageCollection = !containedPackages.isEmpty();

        boolean trace = logger.isTraceEnabled();

        for (Iterator iterator = entries.entrySet().iterator(); iterator.hasNext();) {
            Map.Entry entry = (Map.Entry) iterator.next();
            String resourceName = (String) entry.getKey();

            // filter out the test hierarchy
            if (resourceName.endsWith(ClassUtils.CLASS_FILE_SUFFIX)) {
                if (trace)
                    logger.trace("Analyze imports for test bundle resource " + resourceName);
                String classFileName = StringUtils.getFilename(resourceName);
                String className = classFileName.substring(0,
                        classFileName.length() - ClassUtils.CLASS_FILE_SUFFIX.length());
                String classPkg = resourceName.substring(0, resourceName.length() - classFileName.length())
                        .replace('/', '.');

                if (classPkg.startsWith("."))
                    classPkg = classPkg.substring(1);

                if (classPkg.endsWith("."))
                    classPkg = classPkg.substring(0, classPkg.length() - 1);

                // if we don't have the package, add it
                if (validPackageCollection && StringUtils.hasText(classPkg)
                        && !containedPackages.contains(classPkg)) {
                    logger.trace(
                            "Package [" + classPkg + "] is NOT part of the test archive; adding an import for it");
                    cumulatedPackages.add(classPkg);
                }

                // otherwise parse the class byte-code
                else {
                    if (trace)
                        logger.trace("Package [" + classPkg + "] is part of the test archive; parsing " + className
                                + " bytecode to determine imports...");
                    cumulatedPackages.addAll(determineImportsForClass(className, (Resource) entry.getValue()));
                }
            }
        }

        return (String[]) cumulatedPackages.toArray(new String[cumulatedPackages.size()]);
    }

    /**
     * Determine imports for a class given as a String resource. This method
     * doesn't do any search for the enclosing/inner classes as it considers
     * that these should be handled at a higher level.
     * 
     * The returned set contains the packages in string format (i.e. java.io)
     * 
     * @param className
     * @param resource
     * @return
     */
    private Set determineImportsForClass(String className, Resource resource) {
        Assert.notNull(resource, "a not-null class is required");
        DependencyVisitor visitor = new DependencyVisitor();

        boolean trace = logger.isTraceEnabled();

        ClassReader reader;

        try {
            if (trace)
                logger.trace("Visiting class " + className);
            reader = new ClassReader(resource.getInputStream());
        } catch (Exception ex) {
            throw (RuntimeException) new IllegalArgumentException("Cannot read class " + className).initCause(ex);
        }
        reader.accept(visitor, false);

        // convert from / to . format
        Set originalPackages = visitor.getPackages();
        Set pkgs = new LinkedHashSet(originalPackages.size());

        for (Iterator iterator = originalPackages.iterator(); iterator.hasNext();) {
            String pkg = (String) iterator.next();
            pkgs.add(pkg.replace('/', '.'));
        }
        return pkgs;
    }

    protected void postProcessBundleContext(BundleContext context) throws Exception {
        logger.debug("Post processing: creating test bundle");

        Resource jar;

        Manifest mf = getManifest();

        // if the jar content hasn't been discovered yet (while creating the manifest)
        // do so now
        if (jarEntries == null) {
            // set root path
            jarCreator.setRootPath(getRootPath());
            // add the content pattern
            jarCreator.setContentPattern(getBundleContentPattern());

            // use jar creator for pattern discovery
            jar = jarCreator.createJar(mf);
        }

        // otherwise use the cached resources
        else {
            jar = jarCreator.createJar(mf, jarEntries);
        }

        try {
            installAndStartBundle(context, jar);
        } catch (Exception e) {
            IllegalStateException ise = new IllegalStateException(
                    "Unable to dynamically start generated unit test bundle");
            ise.initCause(e);
            throw ise;
        }

        // now do the delegation
        super.postProcessBundleContext(context);
    }

    private void installAndStartBundle(BundleContext context, Resource resource) throws Exception {
        // install & start
        Bundle bundle = context.installBundle(
                "[onTheFly-test-bundle]" + ClassUtils.getShortName(getClass()) + "[" + hashCode() + "]",
                resource.getInputStream());

        String bundleString = OsgiStringUtils.nullSafeNameAndSymName(bundle);
        boolean debug = logger.isDebugEnabled();

        if (debug)
            logger.debug("Test bundle [" + bundleString + "] successfully installed");
        bundle.start();
        if (debug)
            logger.debug("Test bundle [" + bundleString + "] successfully started");
    }

}