/* * Copyright (c) 2006-2011 Nuxeo SA (http://nuxeo.com/) and others. * * 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 * * Contributors: * Nuxeo - initial API and implementation * $Id$ */ package org.eclipse.ecr.testlib; import java.io.File; import java.io.IOException; import java.lang.reflect.Method; import java.net.URI; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.jar.Attributes; import java.util.jar.JarFile; import java.util.jar.Manifest; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jmock.MockObjectTestCase; import org.nuxeo.common.Environment; import org.nuxeo.common.utils.FileUtils; import org.nuxeo.osgi.BundleFile; import org.nuxeo.osgi.BundleImpl; import org.nuxeo.osgi.DirectoryBundleFile; import org.nuxeo.osgi.JarBundleFile; import org.nuxeo.osgi.OSGiAdapter; import org.nuxeo.osgi.application.StandaloneBundleLoader; import org.eclipse.ecr.runtime.AbstractRuntimeService; import org.eclipse.ecr.runtime.RuntimeService; import org.eclipse.ecr.runtime.api.Framework; import org.eclipse.ecr.runtime.api.ServiceManager; import org.eclipse.ecr.runtime.model.RuntimeContext; import org.eclipse.ecr.runtime.osgi.OSGiRuntimeContext; import org.eclipse.ecr.runtime.osgi.OSGiRuntimeService; import org.eclipse.ecr.testlib.runner.RuntimeHarness; import org.osgi.framework.Bundle; import org.osgi.framework.FrameworkEvent; /** * Abstract base class for test cases that require a test runtime service. * <p> * The runtime service itself is conveniently available as the * <code>runtime</code> instance variable in derived classes. * * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a> */ // Make sure this class is kept in sync with with RuntimeHarness public class NXRuntimeTestCase extends MockObjectTestCase implements RuntimeHarness { static { // jul to jcl redirection may pose problems (infinite loops) in some // environment // where slf4j to jul, and jcl over slf4j is deployed System.setProperty(AbstractRuntimeService.REDIRECT_JUL, "false"); } private static final Log log = LogFactory.getLog(NXRuntimeTestCase.class); protected RuntimeService runtime; protected URL[] urls; // classpath urls, used for bundles lookup protected File workingDir; private static int counter = 0; protected StandaloneBundleLoader bundleLoader; private Set<URI> readUris; protected Map<String, BundleFile> bundles; protected boolean restart = false; protected OSGiAdapter osgi; protected Bundle runtimeBundle; protected final List<WorkingDirectoryConfigurator> wdConfigs = new ArrayList<WorkingDirectoryConfigurator>(); public NXRuntimeTestCase() { } public NXRuntimeTestCase(String name) { super(name); } @Override public void addWorkingDirectoryConfigurator( WorkingDirectoryConfigurator config) { wdConfigs.add(config); } @Override public File getWorkingDir() { return workingDir; } /** * Restarts the runtime and preserve homes directory. */ protected void restart() throws Exception { restart = true; try { tearDown(); setUp(); } finally { restart = false; } } @Override public void start() throws Exception { setUp(); } @Override public void setUp() throws Exception { System.setProperty("org.eclipse.ecr.runtime.testing", "true"); super.setUp(); wipeRuntime(); initUrls(); if (urls == null) { initTestRuntime(); } else { initOsgiRuntime(); } } /** * Fire the event {@code FrameworkEvent.STARTED}. */ @Override public void fireFrameworkStarted() throws Exception { osgi.fireFrameworkEvent(new FrameworkEvent(FrameworkEvent.STARTED, runtimeBundle, null)); } @Override public void tearDown() throws Exception { wipeRuntime(); if (workingDir != null) { if (!restart) { FileUtils.deleteTree(workingDir); workingDir = null; } } readUris = null; bundles = null; ServiceManager.getInstance().reset(); super.tearDown(); } @Override public void stop() throws Exception { tearDown(); } @Override public boolean isStarted() { return runtime != null; } private static synchronized String generateId() { long stamp = System.currentTimeMillis(); counter++; return Long.toHexString(stamp) + '-' + System.identityHashCode(System.class) + '.' + counter; } protected void initOsgiRuntime() throws Exception { try { if (!restart) { Environment.setDefault(null); workingDir = File.createTempFile("NXOSGITestFramework", generateId()); workingDir.delete(); } } catch (IOException e) { log.error("Could not init working directory", e); throw e; } osgi = new OSGiAdapter(workingDir); bundleLoader = new StandaloneBundleLoader(osgi, NXRuntimeTestCase.class.getClassLoader()); Thread.currentThread().setContextClassLoader( bundleLoader.getSharedClassLoader().getLoader()); for (WorkingDirectoryConfigurator cfg : wdConfigs) { cfg.configure(this, workingDir); } bundleLoader.setScanForNestedJARs(false); // for now bundleLoader.setExtractNestedJARs(false); BundleFile bundleFile = lookupBundle("org.eclipse.ecr.runtime"); runtimeBundle = new RootRuntimeBundle(osgi, bundleFile, bundleLoader.getClass().getClassLoader(), true); runtimeBundle.start(); runtime = Framework.getRuntime(); assertNotNull(runtime); // avoid Streaming and Remoting services: useless and can't work deployContrib(bundleFile, "OSGI-INF/DeploymentService.xml"); deployContrib(bundleFile, "OSGI-INF/LoginComponent.xml"); deployContrib(bundleFile, "OSGI-INF/ServiceManagement.xml"); deployContrib(bundleFile, "OSGI-INF/EventService.xml"); deployContrib(bundleFile, "OSGI-INF/ResourceService.xml"); deployContrib(bundleFile, "OSGI-INF/DefaultJBossBindings.xml"); deployContrib(bundleFile, "OSGI-INF/ContributionPersistence.xml"); } protected void initTestRuntime() throws Exception { runtime = new TestRuntime(); Framework.initialize(runtime); deployContrib("org.nuxeo.runtime.test", "EventService.xml"); deployContrib("org.nuxeo.runtime.test", "DeploymentService.xml"); } protected void initUrls() throws Exception { ClassLoader classLoader = NXRuntimeTestCase.class.getClassLoader(); if (classLoader instanceof URLClassLoader) { urls = ((URLClassLoader) classLoader).getURLs(); } else if (classLoader.getClass().getName().equals( "org.apache.tools.ant.AntClassLoader")) { Method method = classLoader.getClass().getMethod("getClasspath"); String cp = (String) method.invoke(classLoader); String[] paths = cp.split(File.pathSeparator); urls = new URL[paths.length]; for (int i = 0; i < paths.length; i++) { urls[i] = new URL("file:" + paths[i]); } } else { log.warn("Unknow classloader type: " + classLoader.getClass().getName() + "\nWon't be able to load OSGI bundles"); return; } // special case for maven surefire with useManifestOnlyJar if (urls.length == 1) { try { URI uri = urls[0].toURI(); if (uri.getScheme().equals("file") && uri.getPath().contains("surefirebooter")) { JarFile jar = new JarFile(new File(uri)); try { String cp = jar.getManifest().getMainAttributes().getValue( Attributes.Name.CLASS_PATH); if (cp != null) { String[] cpe = cp.split(" "); URL[] newUrls = new URL[cpe.length]; for (int i = 0; i < cpe.length; i++) { // Don't need to add 'file:' with maven surefire // >= 2.4.2 String newUrl = cpe[i].startsWith("file:") ? cpe[i] : "file:" + cpe[i]; newUrls[i] = new URL(newUrl); } urls = newUrls; } } finally { jar.close(); } } } catch (Exception e) { // skip } } StringBuilder sb = new StringBuilder(); sb.append("URLs on the classpath: "); for (URL url : urls) { sb.append(url.toString()); sb.append('\n'); } log.debug(sb.toString()); readUris = new HashSet<URI>(); bundles = new HashMap<String, BundleFile>(); } /** * Makes sure there is no previous runtime hanging around. * <p> * This happens for instance if a previous test had errors in its * <code>setUp()</code>, because <code>tearDown()</code> has not been * called. */ protected void wipeRuntime() throws Exception { // Make sure there is no active runtime (this might happen if an // exception is raised during a previous setUp -> tearDown is not called // afterwards). runtime = null; if (Framework.getRuntime() != null) { Framework.shutdown(); } } /** @deprecated use {@link #getResource(String, String)} instead */ @Deprecated public static URL getResource(String resource) { return Thread.currentThread().getContextClassLoader().getResource( resource); } public URL getResource(String bundleName, String resource) throws Exception { return lookupBundle(bundleName).getEntry(resource); } /** * @deprecated use <code>deployContrib()</code> instead */ @Override @Deprecated public void deploy(String contrib) { deployContrib(contrib); } @Deprecated protected void deployContrib(URL url) { assertEquals(runtime, Framework.getRuntime()); log.info("Deploying contribution from " + url.toString()); try { runtime.getContext().deploy(url); } catch (Exception e) { log.error(e); fail("Failed to deploy contrib " + url.toString()); } } /** * Deploys a contribution file by looking for it in the class loader. * <p> * The first contribution file found by the class loader will be used. You * have no guarantee in case of name collisions. * * @deprecated use the less ambiguous * {@link #deployContrib(BundleFile,String)} * @param contrib the relative path to the contribution file */ @Override @Deprecated public void deployContrib(String contrib) { URL url = getResource(contrib); assertNotNull("Test contribution not found: " + contrib, url); deployContrib(url); } protected void deployContrib(BundleFile bundleFile, String contrib) { URL url = bundleFile.getEntry(contrib); if (url == null) { fail(String.format("Could not find entry %s in bundle '%s", contrib, bundleFile.getURL())); } log.info("Deploying contribution from " + url.toString()); Bundle bundle = new BundleImpl(getOSGiAdapter(), bundleFile, getClass().getClassLoader()); try { RuntimeContext context = ((OSGiRuntimeService) runtime).createContext(bundle); context.deploy(url); } catch (Exception e) { log.error(e); fail("Failed to deploy contrib " + url.toString()); } } /** * Deploys a contribution from a given bundle. * <p> * The path will be relative to the bundle root. Example: <code> * deployContrib("org.nuxeo.ecm.core", "OSGI-INF/CoreExtensions.xml") * </code> * <p> * For compatibility reasons the name of the bundle may be a jar name, but * this use is discouraged and deprecated. * * @param bundle the name of the bundle to peek the contrib in * @param contrib the path to contrib in the bundle. */ @Override public void deployContrib(String bundle, String contrib) throws Exception { deployContrib(lookupBundle(bundle), contrib); } /** * Deploy an XML contribution from outside a bundle. * <p> * This should be used by tests wiling to deploy test contribution as part * of a real bundle. * <p> * The bundle owner is important since the contribution may depend on * resources deployed in that bundle. * <p> * Note that the owner bundle MUST be an already deployed bundle. * * @param bundle the bundle that becomes the contribution owner * @param contrib the contribution to deploy as part of the given bundle */ @Override public RuntimeContext deployTestContrib(String bundle, String contrib) throws Exception { Bundle b = bundleLoader.getOSGi().getRegistry().getBundle(bundle); if (b != null) { OSGiRuntimeContext ctx = new OSGiRuntimeContext(runtime, b); ctx.deploy(contrib); return ctx; } else { throw new IllegalArgumentException("Bundle not deployed " + bundle); } } @Override public RuntimeContext deployTestContrib(String bundle, URL contrib) throws Exception { Bundle b = bundleLoader.getOSGi().getRegistry().getBundle(bundle); if (b != null) { OSGiRuntimeContext ctx = new OSGiRuntimeContext(runtime, b); ctx.deploy(contrib); return ctx; } else { throw new IllegalArgumentException("Bundle not deployed " + bundle); } } /** * @deprecated use {@link #undeployContrib(String, String)} instead */ @Override @Deprecated public void undeploy(String contrib) { undeployContrib(contrib); } /** * @deprecated use {@link #undeployContrib(String, String)} instead */ @Override @Deprecated public void undeployContrib(String contrib) { URL url = getResource(contrib); assertNotNull("Test contribution not found: " + contrib, url); deployContrib(url); } /** * Undeploys a contribution from a given bundle. * <p> * The path will be relative to the bundle root. Example: <code> * undeployContrib("org.nuxeo.ecm.core", "OSGI-INF/CoreExtensions.xml") * </code> * * @param bundle the bundle * @param contrib the contribution */ @Override public void undeployContrib(String bundle, String contrib) throws Exception { BundleFile b = lookupBundle(bundle); URL url = b.getEntry(contrib); if (url == null) { fail(String.format("Could not find entry %s in bundle '%s'", contrib, b.getURL())); } Bundle bundleImpl = new BundleImpl(getOSGiAdapter(), b, getClass().getClassLoader()); RuntimeContext context = ((OSGiRuntimeService) runtime).getContext(bundleImpl); context.undeploy(url); } // TODO: Never used. Remove? @Deprecated protected void undeployContrib(URL url, String contrib) { assertEquals(runtime, Framework.getRuntime()); log.info("Undeploying contribution from " + url.toString()); try { runtime.getContext().undeploy(url); } catch (Exception e) { log.error(e); fail("Failed to undeploy contrib " + url.toString()); } } protected static boolean isVersionSuffix(String s) { if (s.length() == 0) { return true; } return s.matches("-(\\d+\\.?)+(-SNAPSHOT)?(\\.\\w+)?"); } /** * Resolves an URL for bundle deployment code. * <p> * TODO: Implementation could be finer... * * @return the resolved url */ protected URL lookupBundleUrl(String bundle) { for (URL url : urls) { String[] pathElts = url.getPath().split("/"); for (int i = 0; i < pathElts.length; i++) { if (pathElts[i].startsWith(bundle) && isVersionSuffix(pathElts[i].substring(bundle.length()))) { // we want the main version of the bundle boolean isTestVersion = false; for (int j = i + 1; j < pathElts.length; j++) { // ok for Eclipse (/test) and Maven (/test-classes) if (pathElts[j].startsWith("test")) { isTestVersion = true; break; } } if (!isTestVersion) { log.info("Resolved " + bundle + " as " + url.toString()); return url; } } } } throw new RuntimeException("Could not resolve bundle " + bundle); } /** * Deploys a whole OSGI bundle. * <p> * The lookup is first done on symbolic name, as set in * <code>MANIFEST.MF</code> and then falls back to the bundle url (e.g., * <code>nuxeo-platform-search-api</code>) for backwards compatibility. * * @param bundle the symbolic name */ @Override public void deployBundle(String bundle) throws Exception { // install only if not yet installed if (bundleLoader.getOSGi().getRegistry().getBundle(bundle) == null) { BundleFile bundleFile = lookupBundle(bundle); bundleLoader.loadBundle(bundleFile); bundleLoader.installBundle(bundleFile); } } protected String readSymbolicName(BundleFile bf) { Manifest manifest = bf.getManifest(); if (manifest == null) { return null; } Attributes attrs = manifest.getMainAttributes(); String name = attrs.getValue("Bundle-SymbolicName"); if (name == null) { return null; } String[] sp = name.split(";", 2); return sp[0]; } public BundleFile lookupBundle(String bundleName) throws Exception { BundleFile bundleFile = bundles.get(bundleName); if (bundleFile != null) { return bundleFile; } for (URL url : urls) { URI uri = url.toURI(); if (readUris.contains(uri)) { continue; } File file = new File(uri); readUris.add(uri); try { if (file.isDirectory()) { bundleFile = new DirectoryBundleFile(file); } else { bundleFile = new JarBundleFile(file); } } catch (IOException e) { // no manifest => not a bundle continue; } String symbolicName = readSymbolicName(bundleFile); if (symbolicName != null) { log.info(String.format("Bundle '%s' has URL %s", symbolicName, url)); bundles.put(symbolicName, bundleFile); } if (bundleName.equals(symbolicName)) { return bundleFile; } } log.warn(String.format( "No bundle with symbolic name '%s'; Falling back to deprecated url lookup scheme", bundleName)); return oldLookupBundle(bundleName); } @Deprecated protected BundleFile oldLookupBundle(String bundle) throws Exception { URL url = lookupBundleUrl(bundle); File file = new File(url.toURI()); BundleFile bundleFile; if (file.isDirectory()) { bundleFile = new DirectoryBundleFile(file); } else { bundleFile = new JarBundleFile(file); } log.warn(String.format( "URL-based bundle lookup is deprecated. Please use the symbolic name from MANIFEST (%s) instead", readSymbolicName(bundleFile))); return bundleFile; } @Override public void deployFolder(File folder, ClassLoader loader) throws Exception { DirectoryBundleFile bf = new DirectoryBundleFile(folder); BundleImpl bundle = new BundleImpl(osgi, bf, loader); osgi.install(bundle); } @Override public Properties getProperties() { return runtime.getProperties(); } @Override public RuntimeContext getContext() { return runtime.getContext(); } @Override public OSGiAdapter getOSGiAdapter() { return osgi; } }