org.eclipse.acceleo.common.internal.utils.workspace.AcceleoWorkspaceUtil.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.acceleo.common.internal.utils.workspace.AcceleoWorkspaceUtil.java

Source

/*******************************************************************************
 * Copyright (c) 2009, 2012 Obeo.
 * 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:
 *     Obeo - initial API and implementation
 *******************************************************************************/
package org.eclipse.acceleo.common.internal.utils.workspace;

import com.google.common.collect.Sets;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.net.URLDecoder;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
import java.util.Set;

import org.eclipse.acceleo.common.AcceleoCommonMessages;
import org.eclipse.acceleo.common.AcceleoCommonPlugin;
import org.eclipse.acceleo.common.utils.CompactLinkedHashSet;
import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.internal.registry.ExtensionRegistry;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.IncrementalProjectBuilder;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.ContributorFactoryOSGi;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.IContributor;
import org.eclipse.core.runtime.IExtensionRegistry;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.spi.IDynamicExtensionRegistry;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.osgi.service.resolver.BundleDescription;
import org.eclipse.osgi.service.resolver.BundleSpecification;
import org.eclipse.osgi.service.resolver.ExportPackageDescription;
import org.eclipse.osgi.service.resolver.ImportPackageSpecification;
import org.eclipse.pde.core.plugin.IPluginModelBase;
import org.eclipse.pde.core.plugin.PluginRegistry;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.framework.FrameworkEvent;
import org.osgi.framework.FrameworkListener;
import org.osgi.framework.ServiceReference;
import org.osgi.service.packageadmin.PackageAdmin;

/* TODO add model listeners to the workspace installed plugins to check for updates on required bundles
 * or imported packages.
 */
/**
 * Eclipse-specific utilities to handle workspace contributions of Java services, ATL libraries, ...
 * 
 * @author <a href="mailto:laurent.goubet@obeo.fr">Laurent Goubet</a>
 */
public final class AcceleoWorkspaceUtil {

    /** Singleton instance of this utility. */
    public static final AcceleoWorkspaceUtil INSTANCE = new AcceleoWorkspaceUtil();

    /**
     * Time-out used when we are waiting for an OSGI operation done through package admin.
     */
    private static final int OSGI_TIMEOUT = 3000;

    /** Key of the error message that is to be used when we cannot uninstall workspace bundles. */
    private static final String UNINSTALLATION_FAILURE_KEY = "WorkspaceUtil.UninstallationFailure"; //$NON-NLS-1$

    /** We'll use this to add 'empty' contributions for the bundles we dynamically install. */
    private static final String EMPTY_PLUGIN_XML = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><plugin/>"; //$NON-NLS-1$

    /** Prefix we'll use for the dynamic bundles' MANIFEST. */
    private static final String REFERENCE_URI_PREFIX = "reference:"; //$NON-NLS-1$

    /**
     * This will keep references on the workspace's IPluginModelBase which have been changed since the last
     * refresh. This is the trigger of refreshing workspace contributions : if empty, no plugins will ever be
     * installed.
     */
    final Set<IPluginModelBase> changedContributions = new CompactLinkedHashSet<IPluginModelBase>();

    /** Keeps track of all manually loaded workspace bundles. */
    final Map<IPluginModelBase, Bundle> workspaceInstalledBundles = new HashMap<IPluginModelBase, Bundle>();

    /**
     * This will keep track of the classes that have been loaded from workspace bundles. Keys will be the
     * class' qualified name while values will be the containing bundles' symbolic names.
     */
    final Map<String, WorkspaceClassInstance> workspaceLoadedClasses = new HashMap<String, WorkspaceClassInstance>();

    /** This will allow us to react to project additions/removals in the running workspace. */
    private final IResourceChangeListener workspaceListener = new WorkspaceResourcesListener();

    /** This will allow us to only load once the "duplicate project" warnings. */
    private final Set<String> logOnceProjectLoad = Sets.newHashSet();

    /**
     * This class is a singleton. Access instance through {@link #INSTANCE}.
     */
    private AcceleoWorkspaceUtil() {
        // hides constructor
    }

    /**
     * This will search through the workspace for a plugin defined with the given symbolic name and return it
     * if any.
     * 
     * @param bundleName
     *            Symbolic name of the plugin we're searching a workspace project for.
     * @return The workspace project of the given symbolic name, <code>null</code> if none could be found.
     */
    public static IProject getProject(String bundleName) {
        for (IPluginModelBase model : PluginRegistry.getWorkspaceModels()) {
            if (model.getBundleDescription().getSymbolicName().equals(bundleName)) {
                return model.getUnderlyingResource().getProject();
            }
        }
        return null;
    }

    /**
     * This will try and find a resource of the given name using the bundle from which was originally loaded
     * the given class so as to try and detect if it is jarred. If <code>clazz</code> hasn't been loaded from
     * a bundle class loader, we'll resort to the default class loader mechanism. This will only return
     * <code>null</code> in the case where the resource at <code>resourcePath</code> cannot be located at all.
     * 
     * @param clazz
     *            Class which class loader will be used to try and locate the resource.
     * @param resourcePath
     *            Path of the resource we seek, relative to the class.
     * @return The URL of the resource as we could locate it.
     * @throws IOException
     *             This will be thrown if we fail to convert bundle-scheme URIs into file-scheme URIs.
     */
    public static URL getResourceURL(Class<?> clazz, String resourcePath) throws IOException {
        BundleContext context = AcceleoCommonPlugin.getDefault().getContext();
        ServiceReference packageAdminReference = context.getServiceReference(PackageAdmin.class.getName());
        PackageAdmin packageAdmin = null;
        if (packageAdminReference != null) {
            packageAdmin = (PackageAdmin) context.getService(packageAdminReference);
        }

        URL resourceURL = null;
        if (packageAdmin != null) {
            Bundle bundle = packageAdmin.getBundle(clazz);
            if (bundle != null) {
                final String pathSeparator = "/"; //$NON-NLS-1$
                // We found the appropriate bundle. We'll now try and determine whether the emtl is jarred
                resourceURL = getBundleResourceURL(bundle, pathSeparator, resourcePath);
            }
        }
        /*
         * We couldn't locate either the bundle which loaded the class or the resource. Resort to the class
         * loader and return null if it cannot locate the resource either.
         */
        if (resourceURL == null) {
            resourceURL = clazz.getResource(resourcePath);
        }

        if (packageAdminReference != null) {
            context.ungetService(packageAdminReference);
        }
        return resourceURL;
    }

    /**
     * Returns the URL of the resource at the given path in the given bundle.
     * 
     * @param bundle
     *            The bundle in which we will look for the resource
     * @param pathSeparator
     *            The path separator
     * @param resourcePath
     *            The path of the resource in the bundle
     * @return the URL of the resource at the given path in the given bundle
     * @throws IOException
     *             This will be thrown if we fail to convert bundle-scheme URIs into file-scheme URIs.
     */
    private static URL getBundleResourceURL(Bundle bundle, String pathSeparator, String resourcePath)
            throws IOException {
        URL resourceURL = null;
        Enumeration<?> emtlFiles = bundle.findEntries(pathSeparator, resourcePath, true);
        if (emtlFiles != null && emtlFiles.hasMoreElements()) {
            resourceURL = (URL) emtlFiles.nextElement();
        }

        // We haven't found the URL of the resource, let's check if we have an absolute URL instead of the
        // name of the resource. Fix for [336109] and [325351].
        if (resourceURL == null) {
            Enumeration<?> resources = bundle.getResources(resourcePath);
            if (resources != null && resources.hasMoreElements()) {
                resourceURL = (URL) resources.nextElement();
            }
        }

        // This can only be a bundle-scheme URL if we found the URL. Convert it to file or jar scheme
        if (resourceURL != null) {
            resourceURL = FileLocator.resolve(resourceURL);
        }
        return resourceURL;
    }

    /**
     * Return the bundle of the given class.
     * 
     * @param clazz
     *            The class
     * @return The bundle of the given class
     * @since 3.1
     */
    public static Bundle getBundle(Class<?> clazz) {
        Bundle bundle = null;
        BundleContext context = AcceleoCommonPlugin.getDefault().getContext();
        ServiceReference packageAdminReference = context.getServiceReference(PackageAdmin.class.getName());
        PackageAdmin packageAdmin = null;
        if (packageAdminReference != null) {
            packageAdmin = (PackageAdmin) context.getService(packageAdminReference);
        }

        if (packageAdmin != null) {
            bundle = packageAdmin.getBundle(clazz);
        }
        if (packageAdminReference != null) {
            context.ungetService(packageAdminReference);
        }
        return bundle;
    }

    /**
     * Returns the bundles with the given name.
     * 
     * @param bundleName
     *            The bundle name.
     * @return The bundles with the given name.
     */
    public static Bundle[] getBundles(String bundleName) {
        Bundle[] bundle = null;
        BundleContext context = AcceleoCommonPlugin.getDefault().getContext();
        ServiceReference packageAdminReference = context.getServiceReference(PackageAdmin.class.getName());
        PackageAdmin packageAdmin = null;
        if (packageAdminReference != null) {
            packageAdmin = (PackageAdmin) context.getService(packageAdminReference);
        }

        if (packageAdmin != null) {
            bundle = packageAdmin.getBundles(bundleName, null);
        }
        if (packageAdminReference != null) {
            context.ungetService(packageAdminReference);
        }
        return bundle;
    }

    /**
     * This will try and resolve the given {@link java.io.File} within the workspace and return it if found.
     * 
     * @param file
     *            The file we wish to find in the workspace.
     * @return The resolved IFile if any, <code>null</code> otherwise.
     */
    public static IFile getWorkspaceFile(File file) {
        return ResourcesPlugin.getWorkspace().getRoot().getFileForLocation(new Path(file.getAbsolutePath()));
    }

    /**
     * This will try and resolve the given path against a workspace file. The path can either be relative to
     * the workpace, an absolute path towards a workspace file or represent an uri of platform scheme.
     * 
     * @param path
     *            The path we seek a file for. Cannot be <code>null</code>.
     * @return The resolved file.
     * @throws IOException
     *             This will be throw if we cannot resolve a "platform://plugin" URI to an existing file.
     */
    public static File getWorkspaceFile(String path) throws IOException {
        final String platformResourcePrefix = "platform:/resource/"; //$NON-NLS-1$
        final String platformPluginPrefix = "platform:/plugin/"; //$NON-NLS-1$

        final File soughtFile;
        if (path.startsWith(platformResourcePrefix)) {
            final IPath relativePath = new Path(path.substring(platformResourcePrefix.length()));
            final IFile soughtIFile = ResourcesPlugin.getWorkspace().getRoot().getFile(relativePath);
            soughtFile = soughtIFile.getLocation().toFile();
        } else if (path.startsWith(platformPluginPrefix)) {
            final int bundleNameEnd = path.indexOf('/', platformPluginPrefix.length() + 1);
            final String bundleName = path.substring(platformPluginPrefix.length(), bundleNameEnd);
            Bundle bundle = Platform.getBundle(bundleName);
            if (bundle != null) {
                final URL bundleFileURL = bundle.getEntry(path.substring(bundleNameEnd));
                final URL fileURL = FileLocator.toFileURL(bundleFileURL);
                soughtFile = new File(fileURL.getFile());
            } else {
                /*
                 * Being here means that the bundle id is different than the bundle name. We could try and
                 * find the corresponding bundle in the PluginRegistry.getActiveModels(false) list, but is it
                 * worth the trouble? Most cases should be handled by the previous code and going through such
                 * a loop would be extremely CPU intensive.
                 */
                soughtFile = null;
            }
        } else {
            final IPath workspaceLocation = ResourcesPlugin.getWorkspace().getRoot().getLocation();
            IPath fullPath = new Path(path);
            if (workspaceLocation.isPrefixOf(fullPath)) {
                fullPath = fullPath.removeFirstSegments(workspaceLocation.segmentCount());
            }
            final IFile soughtIFile = ResourcesPlugin.getWorkspace().getRoot().getFile(fullPath);
            soughtFile = soughtIFile.getLocation().toFile();
        }
        return soughtFile;
    }

    /**
     * This can be used to convert a file-scheme URI to a "platform:/plugin" scheme URI if it can be resolved
     * in the installed plugins.
     * 
     * @param filePath
     *            File scheme URI that is to be converted.
     * @return The converted URI if the file could be resolved in the installed plugins, <code>null</code>
     *         otherwise.
     */
    public static String resolveAsPlatformPlugin(String filePath) {
        BundleURLConverter converter = new BundleURLConverter(filePath);
        return converter.resolveAsPlatformPlugin();
    }

    /**
     * This can be used to convert a file-protocol URI to a native protocol (jar, file, http...) URI if it can
     * be resolved in the installed plugins.
     * 
     * @param filePath
     *            File protocol URI that is to be converted.
     * @return The converted URI if the file could be resolved in the installed plugins, <code>null</code>
     *         otherwise.
     */
    public static String resolveInBundles(String filePath) {
        BundleURLConverter converter = new BundleURLConverter(filePath);
        return converter.resolveAsNativeProtocolURL();
    }

    /**
     * Returns the bundle corresponding to the given location if any.
     * 
     * @param pluginLocation
     *            The location of the bundle we seek.
     * @return The bundle corresponding to the given location if any, <code>null</code> otherwise.
     */
    private static Bundle getBundle(String pluginLocation) {
        Bundle[] bundles = AcceleoCommonPlugin.getDefault().getContext().getBundles();
        for (int i = 0; i < bundles.length; i++) {
            if (pluginLocation.equals(bundles[i].getLocation())) {
                return bundles[i];
            }
        }
        return null;
    }

    /**
     * This will return the set of output folders name for the given (java) project.
     * <p>
     * For example, if a project has a source folder "src" with its output folder set as "bin" and a source
     * folder "src-gen" with its output folder set as "bin-gen", this will return a LinkedHashSet containing
     * both "bin" and "bin-gen".
     * </p>
     * 
     * @param project
     *            The project we seek the output folders of.
     * @return The set of output folders name for the given (java) project.
     */
    private static Set<String> getOutputFolders(IProject project) {
        final Set<String> classpathEntries = new CompactLinkedHashSet<String>();
        final IJavaProject javaProject = JavaCore.create(project);
        try {
            for (IClasspathEntry entry : javaProject.getResolvedClasspath(true)) {
                if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE) {
                    final IPath output = entry.getOutputLocation();
                    if (output != null) {
                        classpathEntries.add(output.removeFirstSegments(1).toString());
                    }
                }
            }
            /*
             * Add the default output location to the classpath anyway since source folders are not required
             * to have their own
             */
            final IPath output = javaProject.getOutputLocation();
            classpathEntries.add(output.removeFirstSegments(1).toString());
        } catch (JavaModelException e) {
            AcceleoCommonPlugin.log(e, false);
        }
        return classpathEntries;
    }

    /**
     * This will iterate over the "Export-Package" manifest header of the given bundle and search a bundle
     * corresponding to the given qualified class name.
     * <p>
     * For example, if the qualified name we're given is "org.eclipse.acceleo.sample.Test", we'll search for a
     * bundle exporting the package "org.eclipse.acceleo.sample".
     * </p>
     * 
     * @param model
     *            The bundle model that is to be checked.
     * @param qualifiedName
     *            Qualified name of the class we search the exported package of.
     * @return <code>true</code> iff <code>model</code> has an entry for a package corresponding to
     *         <code>qualifiedName</code>.
     */
    private static boolean hasCorrespondingExportPackage(IPluginModelBase model, String qualifiedName) {
        String packageName = ""; //$NON-NLS-1$
        final int end = qualifiedName.lastIndexOf('.');
        if (end != -1) {
            packageName = qualifiedName.substring(0, end);
        }

        for (ExportPackageDescription exported : model.getBundleDescription().getExportPackages()) {
            if (packageName.startsWith(exported.getName())) {
                return true;
            }
        }

        return false;
    }

    /**
     * Adds a given project to the set of projects that are to be dynamically installed.
     * 
     * @param project
     *            The project that is to be dynamically installed when Acceleo searches for workspace
     *            contributions.
     */
    public synchronized void addWorkspaceContribution(IProject project) {
        final IPluginModelBase model = PluginRegistry.findModel(project);
        if (model != null && !workspaceInstalledBundles.containsKey(model)) {
            changedContributions.add(model);
        }
    }

    /**
     * This can be used to uninstall all manually loaded bundles from the registry and remove all listeners.
     * It will be called on plugin stopping and is not intended to be called by clients.
     * 
     * @noreference This method is not intended to be referenced by clients.
     */
    public synchronized void dispose() {
        changedContributions.clear();
        workspaceLoadedClasses.clear();
        ResourcesPlugin.getWorkspace().removeResourceChangeListener(workspaceListener);

        for (Map.Entry<IPluginModelBase, Bundle> entry : workspaceInstalledBundles.entrySet()) {
            final Bundle bundle = entry.getValue();

            try {
                uninstallBundle(bundle);
            } catch (BundleException e) {
                AcceleoCommonPlugin.log(new Status(IStatus.ERROR, AcceleoCommonPlugin.PLUGIN_ID,
                        AcceleoCommonMessages.getString(UNINSTALLATION_FAILURE_KEY, bundle.getSymbolicName()), e));
            }
        }
        workspaceInstalledBundles.clear();
    }

    /**
     * This will refresh the workspace contributions if needed, then search through the workspace loaded
     * bundles for a class corresponding to <code>qualifiedName</code>.
     * 
     * @param qualifiedName
     *            The qualified name of the class we seek to load.
     * @param honorOSGiVisibility
     *            If <code>true</code>, this will only search through exported packages for the class
     *            <code>qualifiedName</code>. Otherwise we'll search through all bundles by simply trying to
     *            load the class and catching the {@link ClassNotFoundException} if it isn't loadable.
     * @return The class <code>qualifiedName</code> if it could be found in the workspace bundles,
     *         <code>null</code> otherwise.
     */
    public synchronized Class<?> getClass(String qualifiedName, boolean honorOSGiVisibility) {
        if (changedContributions.size() > 0) {
            refreshContributions();
        }

        // Has an instance of this class already been loaded?
        Class<?> clazz = null;
        final WorkspaceClassInstance workspaceInstance = workspaceLoadedClasses.get(qualifiedName);
        if (workspaceInstance != null) {
            if (workspaceInstance.isStale()) {
                for (Map.Entry<IPluginModelBase, Bundle> entry : workspaceInstalledBundles.entrySet()) {
                    final IPluginModelBase model = entry.getKey();
                    if (workspaceInstance.getBundle().equals(model.getBundleDescription().getSymbolicName())) {
                        clazz = internalLoadClass(entry.getValue(), qualifiedName);
                        workspaceInstance.setStale(false);
                        workspaceInstance.setClass(clazz);
                        break;
                    }
                }
            } else {
                clazz = workspaceInstance.getInstance().getClass();
            }
        }
        if (clazz != null) {
            return clazz;
        }

        // The class hasn't been instantiated yet ; search for the class without instantiating it
        Iterator<Map.Entry<IPluginModelBase, Bundle>> iterator = workspaceInstalledBundles.entrySet().iterator();
        while (clazz == null && iterator.hasNext()) {
            Map.Entry<IPluginModelBase, Bundle> entry = iterator.next();
            /*
             * If we're asked to honor OSGi package visibility, we'll first check the "Export-Package" header
             * of this bundle's MANIFEST.
             */
            if (!honorOSGiVisibility || hasCorrespondingExportPackage(entry.getKey(), qualifiedName)) {
                try {
                    clazz = entry.getValue().loadClass(qualifiedName);
                } catch (ClassNotFoundException e) {
                    // Swallow this ; we'll log the issue later on if we cannot find the class at all
                }
            }
        }

        if (clazz == null) {
            AcceleoCommonPlugin.log(AcceleoCommonMessages.getString("BundleClassLookupFailure", //$NON-NLS-1$
                    qualifiedName), false);
        }

        return clazz;
    }

    /**
     * This will install or refresh the given workspace contribution if needed, then search through it for a
     * class corresponding to <code>qualifiedName</code>.
     * 
     * @param project
     *            The project that is to be dynamically installed when Acceleo searches for workspace
     *            contributions.
     * @param qualifiedName
     *            The qualified name of the class we seek to load.
     * @return An instance of the class <code>qualifiedName</code> if it could be found in the workspace
     *         bundles, <code>null</code> otherwise.
     */
    public synchronized Class<?> getClass(IProject project, String qualifiedName) {
        Class<?> instance = null;
        addWorkspaceContribution(project);
        refreshContributions();
        final IPluginModelBase model = PluginRegistry.findModel(project);
        final Bundle installedBundle = workspaceInstalledBundles.get(model);
        if (installedBundle != null) {
            instance = internalLoadClass(installedBundle, qualifiedName);
        }
        return instance;
    }

    /**
     * This will try and load a ResourceBundle for the given qualified name.
     * 
     * @param qualifiedName
     *            Name if the resource bundle we need to load.
     * @return The loaded resource bundle.
     */
    public synchronized ResourceBundle getResourceBundle(String qualifiedName) {
        MissingResourceException originalException = null;

        // shortcut evaluation in case this properties file can be found in the current classloader.
        try {
            ResourceBundle bundle = ResourceBundle.getBundle(qualifiedName);
            return bundle;
        } catch (MissingResourceException e) {
            originalException = e;
        }

        if (changedContributions.size() > 0) {
            refreshContributions();
        }

        /*
         * We'll iterate over the bundles that have been installed from the workspace, search for one that
         * exports a package of the name we're looking for, then try and load the properties file from this
         * bundle's class loader (bundle.getResource()). If the resource couldn't be found in that bundle, we
         * loop over to the next.
         */
        for (Map.Entry<IPluginModelBase, Bundle> entry : workspaceInstalledBundles.entrySet()) {
            final Bundle bundle = entry.getValue();
            URL propertiesResource = bundle.getResource(qualifiedName.replace('.', '/') + ".properties"); //$NON-NLS-1$
            if (propertiesResource != null) {
                InputStream stream = null;
                try {
                    stream = propertiesResource.openStream();
                    // make sure this stream is buffered
                    stream = new BufferedInputStream(stream);
                    return new PropertyResourceBundle(stream);
                } catch (IOException e) {
                    // Swallow this, we'll throw the original MissingResourceException
                } finally {
                    try {
                        if (stream != null) {
                            stream.close();
                        }
                    } catch (IOException e) {
                        // Swallow this, we'll throw the original MissingResourceException
                    }
                }
            }
        }
        throw originalException;
    }

    /**
     * Retrieves the singleton instance of the given service class after refreshing it if needed.
     * 
     * @param serviceClass
     *            The service class we need an instance of.
     * @return The singleton instance of the given service class if any.
     */
    public synchronized Object getServiceInstance(Class<?> serviceClass) {
        String qualifiedName = serviceClass.getName();
        for (Map.Entry<String, WorkspaceClassInstance> workspaceClass : workspaceLoadedClasses.entrySet()) {
            if (workspaceClass.getKey().equals(qualifiedName)) {
                WorkspaceClassInstance workspaceInstance = workspaceClass.getValue();
                if (workspaceInstance.isStale()) {
                    for (Map.Entry<IPluginModelBase, Bundle> entry : workspaceInstalledBundles.entrySet()) {
                        final IPluginModelBase model = entry.getKey();
                        if (workspaceInstance.getBundle().equals(model.getBundleDescription().getSymbolicName())) {
                            Class<?> clazz = internalLoadClass(entry.getValue(), qualifiedName);
                            workspaceInstance.setStale(false);
                            workspaceInstance.setClass(clazz);
                            break;
                        }
                    }
                }
                return workspaceInstance.getInstance();
            }
        }
        return null;
    }

    /**
     * Adds model listeners to all workspace-defined bundles. This will be called at plugin starting and is
     * not intended to be called by clients.
     * 
     * @noreference This method is not intended to be referenced by clients.
     */
    public void initialize() {
        final int interestingEvents = IResourceChangeEvent.PRE_CLOSE | IResourceChangeEvent.PRE_DELETE
                | IResourceChangeEvent.POST_CHANGE | IResourceChangeEvent.POST_BUILD;
        ResourcesPlugin.getWorkspace().addResourceChangeListener(workspaceListener, interestingEvents);
    }

    /**
     * This can be used to check whether the given class is located in a dynamically installed bundle.
     * 
     * @param clazz
     *            The class of which we need to determine the originating bundle.
     * @return <code>true</code> if the given class has been loaded from a dynamic bundle, <code>false</code>
     *         otherwise.
     */
    public boolean isInDynamicBundle(Class<?> clazz) {
        BundleContext context = AcceleoCommonPlugin.getDefault().getContext();
        ServiceReference packageAdminReference = context.getServiceReference(PackageAdmin.class.getName());
        PackageAdmin packageAdmin = null;
        if (packageAdminReference != null) {
            packageAdmin = (PackageAdmin) context.getService(packageAdminReference);
        }

        if (packageAdmin != null) {
            Bundle bundle = packageAdmin.getBundle(clazz);
            if (workspaceInstalledBundles.values().contains(bundle)) {
                return true;
            }
        }

        if (packageAdminReference != null) {
            context.ungetService(packageAdminReference);
        }

        return false;
    }

    /**
     * This will refresh the list of contributions to the registry by either installing the given plugins in
     * the current running Eclipse or refresh their packages. Keep this synchronized as it will be called by
     * each of the utilities and these calls can come from multiple threads.
     * 
     * @noreference This method is not intended to be referenced by clients.
     */
    public synchronized void refreshContributions() {
        if (changedContributions.size() == 0) {
            return;
        }

        for (IPluginModelBase candidate : changedContributions) {
            installBundle(candidate);
        }
        changedContributions.clear();
    }

    /**
     * This will be used internally to reset the workspace utility to its initialized state. Not intended to
     * be called by clients.
     */
    public synchronized void reset() {
        changedContributions.clear();
        workspaceLoadedClasses.clear();

        for (Map.Entry<IPluginModelBase, Bundle> entry : workspaceInstalledBundles.entrySet()) {
            final Bundle bundle = entry.getValue();

            try {
                if (bundle.getState() != Bundle.UNINSTALLED) {
                    uninstallBundle(bundle);
                }
            } catch (BundleException e) {
                AcceleoCommonPlugin.log(new Status(IStatus.ERROR, AcceleoCommonPlugin.PLUGIN_ID,
                        AcceleoCommonMessages.getString(UNINSTALLATION_FAILURE_KEY, bundle.getSymbolicName()), e));
            }
        }
        workspaceInstalledBundles.clear();
    }

    /**
     * Refreshes all exported packages of the given bundles. This must be called after installing the bundle.
     * 
     * @param bundles
     *            Bundles which exported packages are to be refreshed.
     */
    @SuppressWarnings("restriction")
    void refreshPackages(Bundle[] bundles) {
        BundleContext context = AcceleoCommonPlugin.getDefault().getContext();
        ServiceReference packageAdminReference = context.getServiceReference(PackageAdmin.class.getName());
        PackageAdmin packageAdmin = null;
        if (packageAdminReference != null) {
            packageAdmin = (PackageAdmin) context.getService(packageAdminReference);
        }

        if (packageAdmin != null) {
            final boolean[] flag = new boolean[] { false, };
            FrameworkListener listener = new FrameworkListener() {
                public void frameworkEvent(FrameworkEvent event) {
                    if (event.getType() == FrameworkEvent.PACKAGES_REFRESHED) {
                        synchronized (flag) {
                            flag[0] = true;
                            flag.notifyAll();
                        }
                    }
                }
            };

            /*
             * Hack-ish : Make sure the contributions from this bundle won't be parsed. When installing a
             * bundle, the EclipseBundleListener will _always_ parse the plugin.xml and notify the extension
             * registry (and its listeners) of the new contributions. Problem is : some bundles listen to new
             * contributions, but ignore the event of contribution removals (when we uninstall the bundle). We
             * would thus end with "invalid registry object" exceptions thrown ... with no way to prevent or
             * fix them whatsoever. Equinox does not provide us with an API to disable the contributions from
             * the bundle we're installing, temporarily disable the extension listeners... or any other way to
             * workaround this registry issue. We'll then make use of the fact that the EclipseBundleListener
             * does not add contributions from a contributor which has already been added: we'll add the
             * contributor beforehand with an empty plugin.xml, disabling all potential contributions this
             * bundle would have made otherwise.
             */

            if (bundles != null && Platform.getExtensionRegistry() instanceof IDynamicExtensionRegistry) {
                IExtensionRegistry registry = Platform.getExtensionRegistry();
                for (Bundle bundle : bundles) {
                    IContributor contributor = ContributorFactoryOSGi.createContributor(bundle);
                    if (!((IDynamicExtensionRegistry) registry).hasContributor(contributor)) {
                        registry.addContribution(new ByteArrayInputStream(EMPTY_PLUGIN_XML.getBytes()), contributor,
                                false, null, null, ((ExtensionRegistry) registry).getTemporaryUserToken());
                    }
                }
            }

            context.addFrameworkListener(listener);
            packageAdmin.refreshPackages(bundles);
            synchronized (flag) {
                while (!flag[0]) {
                    try {
                        flag.wait(OSGI_TIMEOUT);
                    } catch (InterruptedException e) {
                        // discard
                        break;
                    }
                }
            }
            context.removeFrameworkListener(listener);
            if (packageAdminReference != null) {
                context.ungetService(packageAdminReference);
            }
        }
    }

    /**
     * Uninstalls the given bundle from the context.
     * 
     * @param target
     *            The bundle that is to be uninstalled.
     * @throws BundleException
     *             Thrown if a lifecycle issue arises.
     */
    void uninstallBundle(Bundle target) throws BundleException {
        target.uninstall();
        refreshPackages(null);
    }

    /**
     * This will check the indirect dependencies of <code>model</code> and install the necessary workspace
     * plugins if we need to import some of their packages.
     * 
     * @param model
     *            The model of which we wish the dependencies checked.
     */
    private void checkImportPackagesDependencies(IPluginModelBase model) {
        final BundleDescription desc = model.getBundleDescription();
        if (desc == null) {
            return;
        }
        for (ImportPackageSpecification importPackage : desc.getImportPackages()) {
            for (IPluginModelBase workspaceModel : PluginRegistry.getWorkspaceModels()) {
                if (workspaceModel != null && workspaceModel.getBundleDescription() != null) {
                    for (ExportPackageDescription export : workspaceModel.getBundleDescription()
                            .getExportPackages()) {
                        if (importPackage.isSatisfiedBy(export)) {
                            installBundle(workspaceModel);
                            break;
                        }
                    }
                }
            }
        }
    }

    /**
     * This will check through the dependencies of <code>model</code> and install the necessary workspace
     * plugins if they are required.
     * 
     * @param model
     *            The model of which we wish the dependencies checked.
     */
    private void checkRequireBundleDependencies(IPluginModelBase model) {
        final BundleDescription desc = model.getBundleDescription();
        if (desc == null) {
            return;
        }
        for (BundleSpecification requiredBundle : desc.getRequiredBundles()) {
            for (IPluginModelBase workspaceModel : PluginRegistry.getWorkspaceModels()) {
                if (requiredBundle.isSatisfiedBy(workspaceModel.getBundleDescription())) {
                    installBundle(workspaceModel);
                    break;
                }
            }
        }
    }

    /**
     * Installs the bundle corresponding to the model.
     * 
     * @param model
     *            Model of the bundle to be installed.
     */
    private void installBundle(IPluginModelBase model) {
        try {
            final IResource candidateManifest = model.getUnderlyingResource();
            final IProject project = candidateManifest.getProject();

            URL url = null;
            try {
                url = project.getLocationURI().toURL();
            } catch (MalformedURLException e) {
                // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=354360
                try {
                    URI uri = project.getLocationURI();
                    IFileStore store = EFS.getStore(uri);
                    File file = store.toLocalFile(0, null);
                    if (file != null) {
                        url = file.toURI().toURL();
                    }
                } catch (CoreException ex) {
                    // Logging both exceptions just to be sure
                    AcceleoCommonPlugin.log(e, false);
                    AcceleoCommonPlugin.log(ex, false);
                }
            }

            if (url != null) {
                final String candidateLocationReference = REFERENCE_URI_PREFIX
                        + URLDecoder.decode(url.toExternalForm(), System.getProperty("file.encoding")); //$NON-NLS-1$

                Bundle bundle = getBundle(candidateLocationReference);

                /*
                 * Install the bundle if needed. Note that we'll check bundle dependencies in two phases as
                 * even if there cannot be cyclic dependencies through the "require-bundle" header, there
                 * could be through the "import package" header.
                 */
                if (bundle == null) {
                    checkRequireBundleDependencies(model);
                    bundle = installBundle(candidateLocationReference);
                    setBundleClasspath(project, bundle);
                    workspaceInstalledBundles.put(model, bundle);
                    checkImportPackagesDependencies(model);
                }
                refreshPackages(new Bundle[] { bundle, });
            }
        } catch (BundleException e) {
            String bundleName = model.getBundleDescription().getName();
            if (!logOnceProjectLoad.contains(bundleName)) {
                logOnceProjectLoad.add(bundleName);
                AcceleoCommonPlugin.log(new Status(IStatus.WARNING, AcceleoCommonPlugin.PLUGIN_ID,
                        AcceleoCommonMessages.getString("WorkspaceUtil.InstallationFailure", //$NON-NLS-1$
                                bundleName, e.getMessage()),
                        e));
            }
        } catch (MalformedURLException e) {
            AcceleoCommonPlugin.log(e, false);
        } catch (UnsupportedEncodingException e) {
            AcceleoCommonPlugin.log(e, false);
        }
    }

    /**
     * Installs the bundle corresponding to the given location. This will fail if the location doesn't point
     * to a valid bundle.
     * 
     * @param pluginLocation
     *            Location of the bundle to be installed.
     * @return The installed bundle.
     * @throws BundleException
     *             Thrown if the Bundle isn't valid.
     * @throws IllegalStateException
     *             Thrown if the bundle couldn't be installed properly.
     */
    private Bundle installBundle(String pluginLocation) throws BundleException, IllegalStateException {
        Bundle target = AcceleoCommonPlugin.getDefault().getContext().installBundle(pluginLocation);
        int state = target.getState();
        if (state != Bundle.INSTALLED) {
            throw new IllegalStateException(AcceleoCommonMessages.getString("WorkspaceUtil.IllegalBundleState", //$NON-NLS-1$
                    target, Integer.valueOf(state)));
        }
        return target;
    }

    /**
     * Loads the class <code>qualifiedName</code> from the specified <code>bundle</code> if possible.
     * 
     * @param bundle
     *            The bundle from which to load the sought class.
     * @param qualifiedName
     *            Qualified name of the class that is to be loaded.
     * @return An instance of the class if it could be loaded, <code>null</code> otherwise.
     */
    private Class<?> internalLoadClass(Bundle bundle, String qualifiedName) {
        try {
            WorkspaceClassInstance workspaceInstance = workspaceLoadedClasses.get(qualifiedName);
            final Class<?> clazz;
            if (workspaceInstance == null) {
                clazz = bundle.loadClass(qualifiedName);
                workspaceLoadedClasses.put(qualifiedName,
                        new WorkspaceClassInstance(clazz, bundle.getSymbolicName()));
            } else if (workspaceInstance.isStale()) {
                clazz = bundle.loadClass(qualifiedName);
                workspaceInstance.setStale(false);
                workspaceInstance.setClass(clazz);
            } else {
                clazz = workspaceInstance.getClassInstance();
            }

            return clazz;
        } catch (ClassNotFoundException e) {
            e.fillInStackTrace();
            AcceleoCommonPlugin.log(AcceleoCommonMessages.getString("BundleClassLookupFailure", //$NON-NLS-1$
                    qualifiedName, bundle.getSymbolicName()), e, false);
        }
        return null;
    }

    /**
     * This will set the equinox classpath of <code>bundle</code> to reflect the eclipse classpath of
     * <code>plugin</code>.
     * 
     * @param plugin
     *            The eclipse plugin which classpath is to be set for its corresponding equinox bundle.
     * @param bundle
     *            The equinox bundle which classpath is to reflect an eclipse development plugin.
     */
    @SuppressWarnings("restriction")
    /*
     * This methods sports a number of restricted class/method usage, yet there is no workaround and the
     * equinox team has no plan to open it. We're suppressing the warning until plans change. Details in bug
     * 271761.
     */
    private void setBundleClasspath(IProject plugin, Bundle bundle) {
        final Set<String> classpathEntries = getOutputFolders(plugin);
        if (classpathEntries.size() > 0) {
            final org.eclipse.osgi.baseadaptor.BaseData bundleData = (org.eclipse.osgi.baseadaptor.BaseData) ((org.eclipse.osgi.framework.internal.core.AbstractBundle) bundle)
                    .getBundleData();
            final StringBuilder classpath = new StringBuilder();
            classpath.append(bundleData.getClassPathString()).append(',');
            final Iterator<String> entryIterator = classpathEntries.iterator();
            while (entryIterator.hasNext()) {
                classpath.append(entryIterator.next());
                if (entryIterator.hasNext()) {
                    classpath.append(',');
                }
            }
            bundleData.setClassPathString(classpath.toString());
        }
    }

    /**
     * Allows us to react to changes in the workspace and install/uninstall listeners as needed.
     * 
     * @author <a href="mailto:laurent.goubet@obeo.fr">Laurent Goubet</a>
     */
    class WorkspaceResourcesListener implements IResourceChangeListener {
        /**
         * {@inheritDoc}
         * 
         * @see org.eclipse.core.resources.IResourceChangeListener#resourceChanged(org.eclipse.core.resources.IResourceChangeEvent)
         */
        public void resourceChanged(IResourceChangeEvent event) {
            switch (event.getType()) {
            /*
             * Closing and deleting projects trigger the same actions : we must remove the model listener and
             * uninstall the bundle.
             */
            case IResourceChangeEvent.PRE_CLOSE:
            case IResourceChangeEvent.PRE_DELETE:
                if (event.getResource() instanceof IProject) {
                    final IProject project = (IProject) event.getResource();
                    final IPluginModelBase model = PluginRegistry.findModel(project);
                    if (model != null) {
                        final Bundle bundle = workspaceInstalledBundles.remove(model);
                        changedContributions.remove(model);
                        if (bundle != null) {
                            try {
                                uninstallBundle(bundle);
                            } catch (BundleException e) {
                                AcceleoCommonPlugin.log(new Status(
                                        IStatus.ERROR, AcceleoCommonPlugin.PLUGIN_ID, AcceleoCommonMessages
                                                .getString(UNINSTALLATION_FAILURE_KEY, bundle.getSymbolicName()),
                                        e));
                            }
                        }
                    }
                }
                break;
            case IResourceChangeEvent.POST_BUILD:
                processBuildEvent(event);
                break;
            case IResourceChangeEvent.POST_CHANGE:
            default:
                // no default action
            }
        }

        /**
         * This will process IResourceChangeEvent.POST_BUILD events so that we can react to builds of our
         * workspace loaded services.
         * 
         * @param event
         *            The event that is to be processed. Assumes that
         *            <code>event.getType() == IResourceChangeEvent.POST_BUILD</code>.
         */
        private void processBuildEvent(IResourceChangeEvent event) {
            final IResourceDelta delta = event.getDelta();
            switch (event.getBuildKind()) {
            case IncrementalProjectBuilder.AUTO_BUILD:
                // Nothing built in such cases
                if (!ResourcesPlugin.getWorkspace().isAutoBuilding()) {
                    break;
                }
                // Fall through to the incremental build handling otherwise
                //$FALL-THROUGH$
            case IncrementalProjectBuilder.INCREMENTAL_BUILD:
                final AcceleoDeltaVisitor visitor = new AcceleoDeltaVisitor();
                try {
                    delta.accept(visitor);
                } catch (CoreException e) {
                    AcceleoCommonPlugin.log(e, false);
                }
                for (IProject changed : visitor.getChangedProjects()) {
                    IPluginModelBase model = PluginRegistry.findModel(changed);
                    if (model != null && workspaceInstalledBundles.keySet().contains(model)) {
                        changedContributions.add(model);
                    }
                }
                for (String changedClass : visitor.getChangedClasses()) {
                    final WorkspaceClassInstance workspaceInstance = workspaceLoadedClasses.get(changedClass);
                    if (workspaceInstance != null) {
                        workspaceInstance.setStale(true);
                    }
                }
                break;
            case IncrementalProjectBuilder.FULL_BUILD:
                for (Map.Entry<IPluginModelBase, Bundle> entry : workspaceInstalledBundles.entrySet()) {
                    IPluginModelBase model = entry.getKey();
                    if (model != null) {
                        changedContributions.add(model);
                    }
                }
                for (WorkspaceClassInstance workspaceInstance : workspaceLoadedClasses.values()) {
                    workspaceInstance.setStale(true);
                }
                break;
            case IncrementalProjectBuilder.CLEAN_BUILD:
                // workspace has been cleaned. Unload every service until next they're built
                final Iterator<Map.Entry<IPluginModelBase, Bundle>> workspaceBundleIterator = workspaceInstalledBundles
                        .entrySet().iterator();
                while (workspaceBundleIterator.hasNext()) {
                    Map.Entry<IPluginModelBase, Bundle> entry = workspaceBundleIterator.next();
                    final Bundle bundle = entry.getValue();

                    try {
                        uninstallBundle(bundle);
                    } catch (BundleException e) {
                        AcceleoCommonPlugin
                                .log(new Status(
                                        IStatus.ERROR, AcceleoCommonPlugin.PLUGIN_ID, AcceleoCommonMessages
                                                .getString(UNINSTALLATION_FAILURE_KEY, bundle.getSymbolicName()),
                                        e));
                    }

                    workspaceBundleIterator.remove();
                }
                for (WorkspaceClassInstance workspaceInstance : workspaceLoadedClasses.values()) {
                    workspaceInstance.setStale(true);
                }
                break;
            default:
                // no default action
            }
        }
    }
}