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