blob: d6f78ee4d932416a23f63b3ebe88ca6eb4f92cc0 [file] [log] [blame]
/******************************************************************************
* 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");
logger.debug(Constants.FRAMEWORK_BOOTDELEGATION + " = " + context.getProperty(Constants.FRAMEWORK_BOOTDELEGATION));
}
bundle.start();
if (debug)
logger.debug("Test bundle [" + bundleString + "] successfully started");
}
}