org.eclipse.ecr.testlib.NXRuntimeTestCase.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.ecr.testlib.NXRuntimeTestCase.java

Source

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

}