diff options
| author | cbrun | 2015-03-02 16:30:09 +0000 |
|---|---|---|
| committer | Maxime Porhel | 2015-03-31 13:09:02 +0000 |
| commit | 69539959f35957701e6c936541ecef9fff1885b4 (patch) | |
| tree | ba8a6b93d9220c1e37effe154feeb67b642f3464 | |
| parent | cac09fa3ac9d886c3c08dedda94441cb2c408bc5 (diff) | |
| download | org.eclipse.sirius-69539959f35957701e6c936541ecef9fff1885b4.tar.gz org.eclipse.sirius-69539959f35957701e6c936541ecef9fff1885b4.tar.xz org.eclipse.sirius-69539959f35957701e6c936541ecef9fff1885b4.zip | |
[460947] Utilities to support invocation of Java from the workspace
Introduce utility classes and APIs abstracting ClassLoading capabilities
used by query interpreters.
An Interpreter registers callbacks on an instance of
JavaExtensionsManager.
These callbacks will be triggered when a class has been loaded,
unloaded, notfound or gone missing.
The JavaExtensionsManager maintains its state in sync with the current
list of imports, the scope (projects, plugins) and the workspace
notifications.
By using the same mechanisms, the JavaExtensionsManager computes and
maintain the list of EPackages declarations which are visibles in the
current classpath.
The JavaExtensionsManager is designed in a way which makes an instance
shareable among different interpreters if one wants to avoid the cost of
search/loading several times for the same class.
It relies on a ClassLoading implementation (BundleClassLoading by
default) which searches for classes in the current Bundles. This
implementation is overriden by WorkspaceClassLoading when the
org.eclipse.sirius.editor plugin is
installed.
This implementation also searches in workspace classes and uses the PDE
to collect dependencies.
Bug: 460947
Change-Id: I5ce7454f1d7bf168b5a408612ce73afd79154119
Signed-off-by: Cedric Brun <cedric.brun@obeo.fr>
15 files changed, 1883 insertions, 4 deletions
diff --git a/plugins/org.eclipse.sirius.common/META-INF/MANIFEST.MF b/plugins/org.eclipse.sirius.common/META-INF/MANIFEST.MF index a50ab5c9f0..add45247cd 100644 --- a/plugins/org.eclipse.sirius.common/META-INF/MANIFEST.MF +++ b/plugins/org.eclipse.sirius.common/META-INF/MANIFEST.MF @@ -12,7 +12,7 @@ Export-Package: org.eclipse.sirius.common.tools;version="2.0.4", org.eclipse.sirius.common.tools.api.contentassist;version="2.0.4", org.eclipse.sirius.common.tools.api.editing;version="2.0.4", org.eclipse.sirius.common.tools.api.find;version="2.0.4", - org.eclipse.sirius.common.tools.api.interpreter;version="2.0.4", + org.eclipse.sirius.common.tools.api.interpreter;version="2.1.0", org.eclipse.sirius.common.tools.api.listener;version="2.0.4", org.eclipse.sirius.common.tools.api.profiler;version="2.0.4", org.eclipse.sirius.common.tools.api.query;version="2.0.4", @@ -21,7 +21,7 @@ Export-Package: org.eclipse.sirius.common.tools;version="2.0.4", org.eclipse.sirius.common.tools.internal.assist;x-internal:=true;version="2.0.4", org.eclipse.sirius.common.tools.internal.ecore;x-internal:=true;version="2.0.4", org.eclipse.sirius.common.tools.internal.editing;x-internal:=true;version="2.0.4", - org.eclipse.sirius.common.tools.internal.interpreter;x-internal:=true;version="2.0.4", + org.eclipse.sirius.common.tools.internal.interpreter;x-internal:=true;version="2.1.0", org.eclipse.sirius.common.tools.internal.resource;x-internal:=true;version="2.0.4" Bundle-Activator: org.eclipse.sirius.common.tools.DslCommonPlugin Bundle-ActivationPolicy: lazy diff --git a/plugins/org.eclipse.sirius.common/plugin.xml b/plugins/org.eclipse.sirius.common/plugin.xml index c59cee24df..13e67de89a 100644 --- a/plugins/org.eclipse.sirius.common/plugin.xml +++ b/plugins/org.eclipse.sirius.common/plugin.xml @@ -19,6 +19,7 @@ <extension-point id="dynamic_package" name="Forked Ecore Package Registry for Dynamic Packages" schema="schema/dynamic_package.exsd"/> <extension-point id="fileModificationValidator" name="File Modification Validator" schema="schema/fileModificationValidator.exsd"/> <extension-point id="resourceSetFactory" name="org.eclipse.sirius.common.resourceSetFactory" schema="schema/resourceSetFactory.exsd"/> + <extension-point id="classloading_override" name="org.eclipse.sirius.common.classloading_override" schema="schema/classloading_override.exsd"/> <extension id="org.eclipse.sirius.common.variableInterpreter" point="org.eclipse.sirius.common.expressionInterpreter"> <expressionInterpreterProvider interpreterProviderClass="org.eclipse.sirius.common.tools.internal.interpreter.VariableInterpreter" /> @@ -29,4 +30,5 @@ <extension id="org.eclipse.sirius.common.serviceInterpreter" point="org.eclipse.sirius.common.expressionInterpreter"> <expressionInterpreterProvider interpreterProviderClass="org.eclipse.sirius.common.tools.internal.interpreter.ServiceInterpreter" /> </extension> + </plugin> diff --git a/plugins/org.eclipse.sirius.common/schema/classloading_override.exsd b/plugins/org.eclipse.sirius.common/schema/classloading_override.exsd new file mode 100644 index 0000000000..6cbfd268b6 --- /dev/null +++ b/plugins/org.eclipse.sirius.common/schema/classloading_override.exsd @@ -0,0 +1,92 @@ +<?xml version='1.0' encoding='UTF-8'?> +<!-- Schema file written by PDE --> +<schema targetNamespace="org.eclipse.sirius.common" xmlns="http://www.w3.org/2001/XMLSchema"> +<annotation> + <appInfo> + <meta.schema plugin="org.eclipse.sirius.common" id="classloading_override" name="org.eclipse.sirius.common.classloading_override"/> + </appInfo> + <documentation> + This extension point allows the redefinition of the instance responsible for classloading intepreter Java extensions. + +Only one override will be used for a given installation. + </documentation> + </annotation> + + <element name="extension"> + <annotation> + <appInfo> + <meta.element /> + </appInfo> + </annotation> + <complexType> + <sequence> + <element ref="override"/> + </sequence> + <attribute name="point" type="string" use="required"> + <annotation> + <documentation> + + </documentation> + </annotation> + </attribute> + <attribute name="id" type="string"> + <annotation> + <documentation> + + </documentation> + </annotation> + </attribute> + <attribute name="name" type="string"> + <annotation> + <documentation> + + </documentation> + <appInfo> + <meta.attribute translatable="true"/> + </appInfo> + </annotation> + </attribute> + </complexType> + </element> + + <element name="override"> + <complexType> + <attribute name="class" type="string" use="required"> + <annotation> + <documentation> + + </documentation> + <appInfo> + <meta.attribute kind="java" basedOn=":org.eclipse.sirius.common.tools.api.interpreter.ClassLoading"/> + </appInfo> + </annotation> + </attribute> + </complexType> + </element> + + <annotation> + <appInfo> + <meta.section type="since"/> + </appInfo> + <documentation> + 3.0.0 + </documentation> + </annotation> + + + + + <annotation> + <appInfo> + <meta.section type="copyright"/> + </appInfo> + <documentation> + Copyright (c) 2015 Obeo<br> +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 +<a href="http://www.eclipse.org/legal/epl-v10.html">http://www.eclipse.org/legal/epl-v10.html</a> + </documentation> + </annotation> + +</schema> diff --git a/plugins/org.eclipse.sirius.common/src/org/eclipse/sirius/common/tools/api/interpreter/ClassLoading.java b/plugins/org.eclipse.sirius.common/src/org/eclipse/sirius/common/tools/api/interpreter/ClassLoading.java new file mode 100644 index 0000000000..c600d69f96 --- /dev/null +++ b/plugins/org.eclipse.sirius.common/src/org/eclipse/sirius/common/tools/api/interpreter/ClassLoading.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * Copyright (c) 2015 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.sirius.common.tools.api.interpreter; + +import java.util.Collection; +import java.util.Set; + +/** + * An instance which is responsible for finding classes given a qualified name + * and a set of project names. + * + * When something changes in the data which has been used for doing the class + * loading, this instance has to notify the {@link ClasspathChangeCallback} + * instance which could have been set using the method + * setClasspathChangeCallback. + * + * @author Cedric Brun <cedric.brun@obeo.fr> + */ +public interface ClassLoading { + /** + * Search a class using the given project names and the Java qualified name. + * + * @param projects + * the projects to search in. + * @param plugins + * the plugins to search in. + * @param qualifiedName + * the qualified name of the class to find. + * @return the class if found, null if not found. + */ + Class<?> findClass(Set<String> projects, Set<String> plugins, String qualifiedName); + + /** + * Invoked by the callee when this class loading utility is no longer + * needed. + */ + void dispose(); + + /** + * Set a {@link ClasspathChangeCallback} which should be notified by the + * instance if something in the environment changed and means the loaded + * classes might not be valid anymore. + * + * @param listener + * the listener to notify when a change is detected. + */ + void setClasspathChangeCallback(ClasspathChangeCallback listener); + + /** + * Return the list of Ecore models which are accessibles in the classpath of + * the given projects and plugins. + * + * @param projects + * a set of project IDs. + * @param plugins + * a set of plugin IDs + * @return a list of descriptors for each Ecore package which is declared in + * the project/plugins or their dependencies. + */ + Collection<EPackageLoadingCallback.EPackageDeclarationSource> findEcoreDeclarations(Set<String> projects, Set<String> plugins); + +} diff --git a/plugins/org.eclipse.sirius.common/src/org/eclipse/sirius/common/tools/api/interpreter/ClassLoadingCallback.java b/plugins/org.eclipse.sirius.common/src/org/eclipse/sirius/common/tools/api/interpreter/ClassLoadingCallback.java new file mode 100644 index 0000000000..870096002a --- /dev/null +++ b/plugins/org.eclipse.sirius.common/src/org/eclipse/sirius/common/tools/api/interpreter/ClassLoadingCallback.java @@ -0,0 +1,68 @@ +/******************************************************************************* + * Copyright (c) 2015 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.sirius.common.tools.api.interpreter; + +/** + * Callback one can use to trigger some behavior when a Java extension is being + * searched. + * + * @author Cedric Brun <cedric.brun@obeo.fr> + */ +public interface ClassLoadingCallback { + /** + * Called at the end of a search or update of class for the corresponding + * qualified name. + * + * This method will be called: + * <ul> + * <li>when the {@link JavaExtensionsManager} instance initialize a new + * {@link Class} (a new import has been added for instance)</li> + * <li>when the {@link JavaExtensionsManager} instances did detect that an + * update is necessary (the class has been modified for instance) and did + * reload it.</li> + * </ul> + * If the clazz has not been found, then the method "notFound" is called. + * + * + * @param qualifiedName + * the Java qualified name if the class which was searched and/or + * reloaded. + * @param clazz + * the corresponding clazz instance if it has been found. + */ + void loaded(String qualifiedName, Class<?> clazz); + + /** + * Called when the manager did search for a class based on a qualified name + * but found nothing. + * + * @param qualifiedName + * the Java qualified name which has not been found in the + * current scope. + */ + void notFound(String qualifiedName); + + /** + * Called by the manager when a class is being unloaded. This might happens: + * <ul> + * <li>When something in the current scope (plugin, project) has changed its + * content, hence the class will be unloaded and loaded again.</li> + * <li>When a name which was previously imported is not anymore.</li> + * </ul> + * + * @param qualifiedName + * the Java qualified name which has been unloaded. + * @param clazz + * the class which has been unloaded. + */ + void unloaded(String qualifiedName, Class<?> clazz); + +} diff --git a/plugins/org.eclipse.sirius.common/src/org/eclipse/sirius/common/tools/api/interpreter/ClasspathChangeCallback.java b/plugins/org.eclipse.sirius.common/src/org/eclipse/sirius/common/tools/api/interpreter/ClasspathChangeCallback.java new file mode 100644 index 0000000000..430c3e4d40 --- /dev/null +++ b/plugins/org.eclipse.sirius.common/src/org/eclipse/sirius/common/tools/api/interpreter/ClasspathChangeCallback.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * Copyright (c) 2015 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.sirius.common.tools.api.interpreter; + +import java.util.Set; + +/** + * A callback which is being notified if the classpath of some project did + * change. + * + * @author Cedric Brun <cedric.brun@obeo.fr> + */ + +public interface ClasspathChangeCallback { + /** + * Called by the {@link ClassLoading} when some projects have been + * updated in a way which might the classpath might have changed. + * + * @param updatedProjects + * a set of project names which have been changed. + */ + void classpathChanged(Set<String> updatedProjects); +} diff --git a/plugins/org.eclipse.sirius.common/src/org/eclipse/sirius/common/tools/api/interpreter/EPackageLoadingCallback.java b/plugins/org.eclipse.sirius.common/src/org/eclipse/sirius/common/tools/api/interpreter/EPackageLoadingCallback.java new file mode 100644 index 0000000000..f3086250c0 --- /dev/null +++ b/plugins/org.eclipse.sirius.common/src/org/eclipse/sirius/common/tools/api/interpreter/EPackageLoadingCallback.java @@ -0,0 +1,135 @@ +/******************************************************************************* + * Copyright (c) 2015 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.sirius.common.tools.api.interpreter; + +import java.util.Collection; + +import org.eclipse.emf.ecore.EPackage; + +/** + * Callback one can use to trigger some behavior when an EPackage extension is + * being searched and loaded. + * + * @author Cedric Brun <cedric.brun@obeo.fr> + */ +public interface EPackageLoadingCallback { + /** + * Called at the end of a search or update of an EPackage with the + * corresponding nsURI. + * + * @param nsURI + * the EPackage nsURI. + * + * @param pak + * the EPackage instance. + * */ + void loaded(String nsURI, EPackage pak); + + /** + * Called at the end of a search or update of an EPackage with the + * corresponding nsURI. + * + * @param nsURI + * the EPackage nsURI. + * @param pak + * the EPackage instance. + * */ + void unloaded(String nsURI, EPackage pak); + + /** + * A value class acting as a descriptor for an EPackage declaration. + * + * @author cedric + * + */ + class EPackageDeclaration { + + private String nsURI; + + private String className; + + private String genModelPath; + + public EPackageDeclaration(String nsURI, String className, String genModelPath) { + super(); + this.nsURI = nsURI; + this.className = className; + this.genModelPath = genModelPath; + } + + public String getNsURI() { + return nsURI; + } + + public String getClassName() { + return className; + } + + /** + * return the genmodel path if specified by the declaration. Null + * otherwhise. + * + * @return the genmodel path if specified by the declaration. Null + * otherwhise. + */ + public String getGenModelPath() { + return genModelPath; + } + + } + + /** + * A source of EPackage declaration. Might be a bundle or a workspace + * project. + * + * @author cedric + * + */ + class EPackageDeclarationSource { + private String symbolicName; + + private Collection<EPackageDeclaration> ePackages; + + private boolean isBundle; + + public EPackageDeclarationSource(String symbolicName, Collection<EPackageDeclaration> epackages, boolean isBundle) { + this.symbolicName = symbolicName; + this.ePackages = epackages; + this.isBundle = isBundle; + } + + public String getSymbolicName() { + return symbolicName; + } + + public Collection<EPackageDeclaration> getEPackageDeclarations() { + return ePackages; + } + + public boolean isBundle() { + return isBundle; + } + + @Override + public String toString() { + String result = ""; + if (isBundle) { + result += "bundle :"; + } else { + result += "project :"; + } + result += symbolicName; + return result; + } + + } + +} diff --git a/plugins/org.eclipse.sirius.common/src/org/eclipse/sirius/common/tools/api/interpreter/JavaExtensionsManager.java b/plugins/org.eclipse.sirius.common/src/org/eclipse/sirius/common/tools/api/interpreter/JavaExtensionsManager.java new file mode 100644 index 0000000000..ab81c30dd3 --- /dev/null +++ b/plugins/org.eclipse.sirius.common/src/org/eclipse/sirius/common/tools/api/interpreter/JavaExtensionsManager.java @@ -0,0 +1,624 @@ +/******************************************************************************* + * Copyright (c) 2013, 2014 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.sirius.common.tools.api.interpreter; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.FileLocator; +import org.eclipse.core.runtime.Path; +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.EPackage; +import org.eclipse.emf.ecore.plugin.EcorePlugin; +import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl; +import org.eclipse.sirius.common.tools.api.interpreter.EPackageLoadingCallback.EPackageDeclaration; +import org.eclipse.sirius.common.tools.api.interpreter.EPackageLoadingCallback.EPackageDeclarationSource; +import org.eclipse.sirius.common.tools.api.util.StringUtil; +import org.eclipse.sirius.common.tools.internal.interpreter.BundleClassLoading; +import org.eclipse.sirius.common.tools.internal.interpreter.ClassLoadingService; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; +import com.google.common.collect.Sets; + +/** + * The {@link JavaExtensionsManager} load and maintains {@link Class} instances + * based on the current search scope (of projects and/or plugins) and the + * imported qualified names. + * + * @author Cedric Brun <cedric.brun@obeo.fr> + */ +public final class JavaExtensionsManager { + + private static final String WORKSPACE_SEPARATOR = "/"; + + /** + * This will be updated with the list of accessible viewpoint plugins, if + * any. + */ + private Set<String> viewpointPlugins = Sets.newLinkedHashSet(); + + /** + * This will be updated with the list of accessible viewpoint projects + * present in the workspace, if any. + */ + private Set<String> viewpointProjects = Sets.newLinkedHashSet(); + + private final Set<String> imports = new LinkedHashSet<String>(); + + /** + * These are the imports which are registered as + * "not having been loaded so far", waiting for a change of scope or a + * recompilation which would make them loadable. + */ + private final Set<String> couldNotBeLoaded = new LinkedHashSet<String>(); + + private final Map<String, Class<?>> loadedClasses = Maps.newLinkedHashMap(); + + private ClassLoading classLoading; + + private List<ClassLoadingCallback> callbacks = Lists.newArrayList(); + + private List<EPackageLoadingCallback> ePackageCallbacks = Lists.newArrayList(); + + private ClasspathChangeCallback onWorkspaceChange = new ClasspathChangeCallback() { + + public void classpathChanged(Set<String> updatedProjects) { + /* + * we get a notification if something in the classpath we used so + * far has changed. + */ + if (viewpointPlugins.size() > 0 && viewpointProjects.size() > 0) { + reloadEPackages(); + reloadJavaExtensions(); + } + } + }; + + private Multimap<String, EPackage> lastDeclarerIDsToEPackages = HashMultimap.create(); + + /** + * through this field we keep track fo the EPackage declarers which were + * identified as bundles, hence don't need to be reloaded if they are still + * bundles when a reload is requested. + * + */ + private Set<String> lastDeclarerIDsInBundles; + + /** + * Create a new JavaExtensionManager. + */ + public JavaExtensionsManager() { + classLoading = new BundleClassLoading(); + } + + /** + * Set (or replace) a new classloading override. If a previous one was + * already set, then it will be disposed. + * + * @param override + * the instance to override the class loading process. + */ + public void setClassLoadingOverride(ClassLoading override) { + if (classLoading != null) { + classLoading.dispose(); + } + override.setClasspathChangeCallback(onWorkspaceChange); + this.classLoading = override; + + } + + /** + * Add a new callback which, from now one, will be notified when a class has + * been loaded/unloaded/searched but not found. + * + * @param callback + * the callback to register. + */ + public void addClassLoadingCallBack(ClassLoadingCallback callback) { + this.callbacks.add(callback); + } + + /** + * Remove a previously registered callback. + * + * @param callback + * the callback to remove. + */ + public void removeClassLoadingCallBack(ClassLoadingCallback callback) { + this.callbacks.remove(callback); + } + + /** + * Add a new callback which, from now one, will be notified when a EPackage + * has been loaded/unloaded/searched but not found. + * + * @param callback + * the callback to register. + */ + public void addEPackageCallBack(EPackageLoadingCallback callback) { + this.ePackageCallbacks.add(callback); + } + + /** + * Remove a previously registered callback. + * + * @param callback + * the callback to remove. + */ + public void removeEPackageCallBack(EPackageLoadingCallback callback) { + this.ePackageCallbacks.remove(callback); + } + + /** + * Clear the resources used by the {@link JavaExtensionsManager}. + */ + public void dispose() { + classLoading.dispose(); + clearImports(); + this.viewpointPlugins.clear(); + this.viewpointProjects.clear(); + this.couldNotBeLoaded.clear(); + } + + /** + * Update the search scope for Classes. Only if the newly passed scope + * differs to the previous one then a reload of the classes will be + * triggered. + * + * @param plugins + * a set of bundle IDs to search in. + * @param project + * a set of project names to search in. + */ + public void updateScope(Set<String> plugins, Set<String> project) { + boolean removedAddedAtLeastOnePlugin = this.viewpointPlugins.retainAll(plugins) || this.viewpointPlugins.addAll(plugins); + + boolean removedAddedAtLeastOnProject = this.viewpointProjects.retainAll(project) || this.viewpointProjects.addAll(project); + this.viewpointPlugins = plugins; + this.viewpointProjects = project; + /* + * something changed in the scope, we have to reload the Java + * extensions. + */ + if (couldNotBeLoaded.size() > 0 || removedAddedAtLeastOnePlugin || removedAddedAtLeastOnProject) { + if (this.viewpointPlugins.size() > 0 && this.viewpointProjects.size() > 0) { + reloadEPackages(); + reloadJavaExtensions(); + } + } + + } + + private void reloadEPackages() { + Multimap<String, EPackage> newDeclarations = HashMultimap.create(); + Set<String> newDeclarersAsBundles = Sets.newLinkedHashSet(); + Collection<EPackageDeclarationSource> ecoreDeclarationSources = this.classLoading.findEcoreDeclarations(this.viewpointProjects, this.viewpointPlugins); + Collection<EPackageDeclarationSource> workspaceDeclarations = Lists.newArrayList(); + for (EPackageLoadingCallback.EPackageDeclarationSource declarer : ecoreDeclarationSources) { + if (declarer.isBundle()) { + newDeclarersAsBundles.add(declarer.getSymbolicName()); + for (EPackageDeclaration ePackageDeclaration : declarer.getEPackageDeclarations()) { + /* + * the EPackage definition comes from a deployed plugin, we + * retrieve the EPackage instance to use by getting it from + * the global registry. + */ + EPackage pak = EPackage.Registry.INSTANCE.getEPackage(ePackageDeclaration.getNsURI()); + if (pak != null) { + newDeclarations.put(declarer.getSymbolicName(), pak); + } + } + + } else { + /* + * we keep that for later as we need to initialize a specific + * resourceset which will be used by all the subsequent + * loadings. + */ + workspaceDeclarations.add(declarer); + } + } + if (workspaceDeclarations.size() > 0) { + /* + * this resourceset is being used to load the genmodel instances + * from the workspace. It is setup with uri mappings so that other + * Ecore residing in the workspace are shadowing the ones from the + * targetplatform. + */ + ResourceSetImpl set = new ResourceSetImpl(); + + computePlatformURIMap(set); + + /* + * the EPackage definition comes from a workspace project, right now + * we don't explicitely and fully support this use case where the + * Ecore model lives in the workspace next to the .odesign + * specification. To properly support this use case we would have to + * load the corresponding genmodel and register it, making sure we + * clean all the + */ + for (EPackageDeclarationSource workspaceSource : workspaceDeclarations) { + Map<String, EPackage> ecorePackages = Maps.newLinkedHashMap(); + /* + * a first iteration to populate the map of loaded Ecore + * packages. + */ + loadAndFindEPackages(set, workspaceSource, ecorePackages); + /* + * a second iteration to declare the EPackages + */ + for (EPackageDeclaration declaration : workspaceSource.getEPackageDeclarations()) { + String nsURI = declaration.getNsURI(); + if (!StringUtil.isEmpty(nsURI)) { + EPackage loaded = ecorePackages.get(nsURI); + if (loaded != null) { + newDeclarations.put(nsURI, loaded); + } + } + } + } + + } + + /* + * cleaning up previously registered EPackage which are not accessible + * any more. + */ + boolean firstRun = lastDeclarerIDsInBundles == null; + if (!firstRun) { + for (Entry<String, EPackage> entry : lastDeclarerIDsToEPackages.entries()) { + boolean changedType = lastDeclarerIDsInBundles.contains(entry.getKey()) != newDeclarersAsBundles.contains(entry.getKey()); + if (changedType) { + unloadedEPackage(entry.getValue()); + } + } + } + for (Entry<String, EPackage> entry : newDeclarations.entries()) { + boolean changedType = firstRun || lastDeclarerIDsInBundles.contains(entry.getKey()) != newDeclarersAsBundles.contains(entry.getKey()); + if (changedType) { + loadedEPackage(entry.getValue()); + } + } + + this.lastDeclarerIDsToEPackages = newDeclarations; + this.lastDeclarerIDsInBundles = newDeclarersAsBundles; + + } + + private void computePlatformURIMap(ResourceSetImpl set) { + Map<URI, URI> result = null; + /* + * We invoke computePlatformURIMap by reflection to keep being + * compatible with EMF 2.8 and still leverage the new capabilities + * regarding target platforms introduced in EMF 2.9. + */ + try { + Method computePlatformURIMap = EcorePlugin.class.getMethod("computePlatformURIMap", Boolean.TYPE); + result = (Map<URI, URI>) computePlatformURIMap.invoke(null, true); + } catch (NoSuchMethodException e) { + /* + * result is still null, we'll call the old method. + */ + } catch (IllegalAccessException e) { + /* + * result is still null, we'll call the old method. + */ + } catch (IllegalArgumentException e) { + /* + * result is still null, we'll call the old method. + */ + } catch (InvocationTargetException e) { + /* + * result is still null, we'll call the old method. + */ + } + if (result == null) { + result = EcorePlugin.computePlatformURIMap(); + } + if (result != null) { + set.getURIConverter().getURIMap().putAll(result); + } + } + + private void loadAndFindEPackages(ResourceSetImpl set, EPackageDeclarationSource workspaceSource, Map<String, EPackage> ecorePackages) { + for (EPackageDeclaration declaration : workspaceSource.getEPackageDeclarations()) { + String genmodelPath = declaration.getGenModelPath(); + if (!StringUtil.isEmpty(genmodelPath)) { + URI genModelURI = URI.createPlatformResourceURI(WORKSPACE_SEPARATOR + workspaceSource.getSymbolicName() + WORKSPACE_SEPARATOR + genmodelPath, true); + /* + * the uri might have a fragment already, for instance in the + * Xcore case, if it is not the case then the genmodel is + * supposed to be the root element. + */ + if (!genModelURI.hasFragment()) { + genModelURI = genModelURI.appendFragment("/"); + } + EObject genModel = set.getEObject(genModelURI, true); + if (genModel != null && genModel.eClass().getEPackage() != null && "GenModel".equals(genModel.eClass().getName()) && "genmodel".equals(genModel.eClass().getEPackage().getName())) { + Collection<EObject> genPackages = (Collection<EObject>) genModel.eGet(genModel.eClass().getEStructuralFeature("genPackages")); + collectEPackages(ecorePackages, genPackages); + } + + } + } + } + + private void collectEPackages(Map<String, EPackage> ecorePackages, Collection<EObject> genPackages) { + for (EObject genPackage : genPackages) { + Object ePak = genPackage.eGet(genPackage.eClass().getEStructuralFeature("ecorePackage")); + if (ePak instanceof EPackage && !StringUtil.isEmpty(((EPackage) ePak).getNsURI())) { + ecorePackages.put(((EPackage) ePak).getNsURI(), (EPackage) ePak); + } + Collection<EObject> subGenPackages = (Collection<EObject>) genPackage.eGet(genPackage.eClass().getEStructuralFeature("nestedGenPackages")); + collectEPackages(ecorePackages, subGenPackages); + } + } + + private void unloadedEPackage(EPackage removed) { + for (EPackageLoadingCallback ePackageCallBack : this.ePackageCallbacks) { + try { + ePackageCallBack.unloaded(removed.getNsURI(), removed); + // CHECKSTYLE:OFF + } catch (Throwable e) { + // CHECKSTYLE:ON + /* + * It's the callback responsability to log or manage the errors, + * we should not prevent another callback to process the event + * if another one failed for some reason. + */ + } + } + } + + private void loadedEPackage(EPackage ePackage) { + for (EPackageLoadingCallback ePackageCallBack : this.ePackageCallbacks) { + try { + ePackageCallBack.loaded(ePackage.getNsURI(), ePackage); + // CHECKSTYLE:OFF + } catch (Throwable e) { + // CHECKSTYLE:ON + /* + * It's the callback responsability to log or manage the errors, + * we should not prevent another callback to process the event + * if another one failed for some reason. + */ + } + } + } + + /** + * Add a new Java qualified name to consider as an Import. + * + * @param classQualifiedName + * the Java qualified name of a class to consider as a Java + * Extension. + */ + public void addImport(String classQualifiedName) { + if (couldNotBeLoaded.contains(classQualifiedName)) { + loadJavaExtensions(Sets.newHashSet(classQualifiedName)); + } + if (classQualifiedName != null && classQualifiedName.contains(".") && !imports.contains(classQualifiedName)) { + imports.add(classQualifiedName); + loadJavaExtensions(Collections.singleton(classQualifiedName)); + } + } + + /** + * Remove a JavaExtension in the current manager. + * + * @param classQualifiedName + * the Java qualified name of a class to remove as a Java + * Extension. + */ + public void removeImport(String classQualifiedName) { + if (this.imports.contains(classQualifiedName)) { + if (couldNotBeLoaded.contains(classQualifiedName)) { + couldNotBeLoaded.remove(classQualifiedName); + } + Set<String> removedImport = Sets.newLinkedHashSet(); + removedImport.add(classQualifiedName); + this.imports.remove(classQualifiedName); + unloadJavaExtensions(removedImport); + } + } + + /** + * the current list of imported Java Extensions. + * + * @return the current list of class qualified name used as Java Extensions. + */ + public Collection<String> getImports() { + return ImmutableList.copyOf(this.imports); + } + + /** + * Unload the already known Java Extensions. + */ + public void clearImports() { + unloadJavaExtensions(this.imports); + this.imports.clear(); + this.couldNotBeLoaded.clear(); + } + + private void reloadJavaExtensions() { + unloadJavaExtensions(this.imports); + loadJavaExtensions(this.imports); + } + + private void loadJavaExtensions(Set<String> addedImports) { + for (String qualifiedName : addedImports) { + Class<?> found = classLoading.findClass(viewpointProjects, viewpointPlugins, qualifiedName); + processResult(qualifiedName, found); + } + } + + private void processResult(String qualifiedName, Class<?> found) { + if (found != null) { + this.couldNotBeLoaded.remove(qualifiedName); + Class<?> alreadyHere = loadedClasses.get(qualifiedName); + if (alreadyHere != null) { + unloaded(qualifiedName, alreadyHere); + } + loadedClasses.put(qualifiedName, found); + + loaded(qualifiedName, found); + } else { + this.couldNotBeLoaded.add(qualifiedName); + notFound(qualifiedName); + } + } + + private void notFound(String qualifiedName) { + for (ClassLoadingCallback callback : this.callbacks) { + try { + callback.notFound(qualifiedName); + // CHECKSTYLE:OFF + } catch (Throwable e) { + // CHECKSTYLE:ON + /* + * It's the callback responsability to log or manage the errors, + * we should not prevent another callback to process the event + * if another one failed for some reason. + */ + } + } + + } + + private void loaded(String qualifiedName, Class<?> clazz) { + for (ClassLoadingCallback callback : this.callbacks) { + try { + callback.loaded(qualifiedName, clazz); + // CHECKSTYLE:OFF + } catch (Throwable e) { + // CHECKSTYLE:ON + /* + * It's the callback responsability to log or manage the errors, + * we should not prevent another callback to process the event + * if another one failed for some reason. + */ + } + } + + } + + private void unloaded(String qualifiedName, Class<?> clazz) { + for (ClassLoadingCallback callback : this.callbacks) { + try { + callback.unloaded(qualifiedName, clazz); + // CHECKSTYLE:OFF + } catch (Throwable e) { + // CHECKSTYLE:ON + /* + * It's the callback responsability to log or manage the errors, + * we should not prevent another callback to process the event + * if another one failed for some reason. + */ + } + } + + } + + private void unloadJavaExtensions(Set<String> removedImports) { + for (String qualifiedName : removedImports) { + Class<?> alreadyHere = loadedClasses.get(qualifiedName); + if (alreadyHere != null) { + unloaded(qualifiedName, alreadyHere); + } + } + } + + /** + * Takes a pure Object as value to update the scope as sent to the + * {@link IInterpreter} instances through the setProperty() method with + * IInterpreter.FILES key. + * + * @param value + * can be null, or a list of String each being the identifier of + * a project which can be in the workspace or not. + */ + public void updateScope(Collection<String> value) { + Set<String> prjs = Sets.newLinkedHashSet(); + Set<String> plugins = Sets.newLinkedHashSet(); + if (value != null) { + for (final String odesignPath : value) { + final URI workspaceCandidate = URI.createPlatformResourceURI(odesignPath, true); + final URI pluginCandidate = URI.createPlatformPluginURI(odesignPath, true); + if (existsInWorkspace(workspaceCandidate.toPlatformString(true))) { + prjs.add(workspaceCandidate.segment(1)); + } else if (existsInPlugins(URI.decode(pluginCandidate.toString()))) { + plugins.add(pluginCandidate.segment(1)); + } + } + } + updateScope(plugins, prjs); + } + + /** + * Checks whether the given path exists in the plugins. + * + * @param path + * The path we need to check. + * @return <code>true</code> if <em>path</em> denotes an existing plugin + * resource, <code>false</code> otherwise. + */ + private static boolean existsInPlugins(String path) { + try { + URL url = new URL(path); + return FileLocator.find(url) != null; + } catch (MalformedURLException e) { + return false; + } + } + + /** + * Checks whether the given path exists in the workspace. + * + * @param path + * The path we need to check. + * @return <code>true</code> if <em>path</em> denotes an existing workspace + * resource, <code>false</code> otherwise. + */ + private static boolean existsInWorkspace(String path) { + if (path == null || path.length() == 0 || EcorePlugin.getWorkspaceRoot() == null) { + return false; + } + return ResourcesPlugin.getWorkspace().getRoot().exists(new Path(path)); + } + + /** + * Create and setup a {@link JavaExtensionsManager} which might have been + * extended with a specific support for workspace class loading. + * + * @return the created {@link JavaExtensionsManager} + */ + public static JavaExtensionsManager createManagerWithOverride() { + JavaExtensionsManager result = new JavaExtensionsManager(); + result.setClassLoadingOverride(ClassLoadingService.getClassLoading()); + return result; + } + +} diff --git a/plugins/org.eclipse.sirius.common/src/org/eclipse/sirius/common/tools/internal/interpreter/BundleClassLoading.java b/plugins/org.eclipse.sirius.common/src/org/eclipse/sirius/common/tools/internal/interpreter/BundleClassLoading.java new file mode 100644 index 0000000000..c589be6a6b --- /dev/null +++ b/plugins/org.eclipse.sirius.common/src/org/eclipse/sirius/common/tools/internal/interpreter/BundleClassLoading.java @@ -0,0 +1,214 @@ +/******************************************************************************* + * Copyright (c) 2015 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.sirius.common.tools.internal.interpreter; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Set; + +import org.eclipse.core.runtime.IConfigurationElement; +import org.eclipse.core.runtime.IExtension; +import org.eclipse.core.runtime.IExtensionPoint; +import org.eclipse.core.runtime.IExtensionRegistry; +import org.eclipse.core.runtime.Platform; +import org.eclipse.emf.common.EMFPlugin; +import org.eclipse.sirius.common.tools.DslCommonPlugin; +import org.eclipse.sirius.common.tools.api.interpreter.ClassLoading; +import org.eclipse.sirius.common.tools.api.interpreter.ClasspathChangeCallback; +import org.eclipse.sirius.common.tools.api.interpreter.EPackageLoadingCallback; +import org.eclipse.sirius.common.tools.api.interpreter.EPackageLoadingCallback.EPackageDeclaration; +import org.osgi.framework.Bundle; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.framework.wiring.BundleWire; +import org.osgi.framework.wiring.BundleWiring; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Multimap; +import com.google.common.collect.Sets; + +/** + * A {@link ClassLoading} implementation which look for a class in a list of + * OSGi bundles. + * + * @author Cedric Brun <cedric.brun@obeo.fr + * + */ +public class BundleClassLoading implements ClassLoading { + /** + * The extension point id used to contribute generated EPackages to the EMF + * runtime. + */ + protected static final String EMF_GENERATED_PACKAGE_EXTENSIONPOINT = "org.eclipse.emf.ecore.generated_package"; + + @Override + public void setClasspathChangeCallback(ClasspathChangeCallback listener) { + /* + * we only consider plugins by default and are not detecting any change + * in it. + */ + + } + + @Override + public Class<?> findClass(Set<String> projectsToSearchIn, Set<String> plugins, String qualifiedName) { + /* + * By default we don't search in projects, only in plugins. + */ + Class<?> found = null; + Iterator<String> it = plugins.iterator(); + while (found == null && it.hasNext()) { + String bundleID = it.next(); + found = loadClassInBundle(bundleID, qualifiedName); + } + return found; + } + + @Override + public void dispose() { + /* + * no internal state, nothing to dispose of. + */ + + } + + private Class<?> loadClassInBundle(String bundleID, String qualifiedName) { + Bundle requiredBundle = Platform.getBundle(bundleID); + if (requiredBundle != null) { + return loadClassInBundle(requiredBundle, qualifiedName); + } + return null; + + } + + private Class<?> loadClassInBundle(Bundle bundle, String qualifiedName) { + try { + return bundle.loadClass(qualifiedName); + } catch (ClassNotFoundException e) { + /* + * nothing to report, move along to the next bundle. + */ + } catch (NoClassDefFoundError e) { + /* + * nothing to report, move along to the next bundle. + */ + } + return null; + } + + @Override + public Collection<EPackageLoadingCallback.EPackageDeclarationSource> findEcoreDeclarations(Set<String> projects, Set<String> plugins) { + Set<String> analyzed = Sets.newLinkedHashSet(); + Set<String> bundlesIDependOn = Sets.newLinkedHashSet(); + + for (String currentBundle : Iterables.concat(plugins, projects)) { + addDependencies(currentBundle, analyzed, bundlesIDependOn); + } + + return getEPackagesDeclaredInBundles(bundlesIDependOn); + } + + /** + * return the list of EPackage declared in the given list of bundles. + * + * @param bundles + * a collection of bundles. + * @return the list of EPackage declarations made through the plugin.xml of + * those bundles. + */ + protected Collection<EPackageLoadingCallback.EPackageDeclarationSource> getEPackagesDeclaredInBundles(Collection<String> bundles) { + Collection<EPackageLoadingCallback.EPackageDeclarationSource> result = Lists.newArrayList(); + if (EMFPlugin.IS_ECLIPSE_RUNNING) { + final IExtensionRegistry reg = Platform.getExtensionRegistry(); + Multimap<String, EPackageDeclaration> contributions = HashMultimap.create(); + final IExtensionPoint ep = reg.getExtensionPoint(EMF_GENERATED_PACKAGE_EXTENSIONPOINT); + for (final IExtension ext : ep.getExtensions()) { + final IConfigurationElement[] ce = ext.getConfigurationElements(); + String contributorName = ext.getContributor().getName(); + if (bundles.contains(contributorName)) { + for (IConfigurationElement element : ce) { + + String nsURI = element.getAttribute("uri"); + String className = element.getAttribute("class"); + String genModel = element.getAttribute("genModel"); + + if (nsURI != null && className != null) { + contributions.put(contributorName, new EPackageDeclaration(nsURI, className, genModel)); + } else { + DslCommonPlugin.getDefault().warning("An EPackage declaration in project " + contributorName + " has been ignored because of missing informations.", + new IllegalArgumentException()); + } + } + + } + } + + for (String contributor : contributions.keySet()) { + Collection<EPackageDeclaration> declarations = contributions.get(contributor); + if (declarations.size() > 0) { + result.add(new EPackageLoadingCallback.EPackageDeclarationSource(contributor, declarations, true)); + } + } + } + return result; + } + + /** + * Recursively add all the dependencies + * + * @param bundleSymbolicName + * the symbolic name of the bundle to analyze. + * @param analyzed + * the set of bundle which have been analyzed already. This set + * is used as a safeguard to avoid infinite cycles and will be + * updated by the method. + * @param collectedDependencies + * the set containing the collected dependencies. + */ + private void addDependencies(String bundleSymbolicName, Set<String> analyzed, Set<String> collectedDependencies) { + if (!analyzed.contains(bundleSymbolicName)) { + analyzed.add(bundleSymbolicName); + Set<String> dependencies = getBundleDependencies(bundleSymbolicName); + collectedDependencies.addAll(dependencies); + for (String dependency : dependencies) { + addDependencies(dependency, analyzed, collectedDependencies); + } + } + + } + + /** + * Retrieve the set of dependencies for a given bundle symbolic name. + * + * + * @param bundleSymbolicName + * the bundle symbolic name. + * @return a set of bundle symbolic names which are dependencies. An empty + * set if the bundle has not been found. + */ + protected Set<String> getBundleDependencies(String bundleSymbolicName) { + Set<String> dependencies = Sets.newLinkedHashSet(); + Bundle currentBundle = Platform.getBundle(bundleSymbolicName); + if (currentBundle != null) { + BundleWiring wiring = currentBundle.adapt(BundleWiring.class); + if (wiring != null) { + for (BundleWire wire : wiring.getRequiredWires(BundleRevision.BUNDLE_NAMESPACE)) { + if (wire.getProvider() != null && wire.getProvider().getBundle() != null) { + dependencies.add(wire.getProvider().getBundle().getSymbolicName()); + } + } + } + } + return dependencies; + } + +} diff --git a/plugins/org.eclipse.sirius.common/src/org/eclipse/sirius/common/tools/internal/interpreter/ClassLoadingService.java b/plugins/org.eclipse.sirius.common/src/org/eclipse/sirius/common/tools/internal/interpreter/ClassLoadingService.java new file mode 100644 index 0000000000..707c469482 --- /dev/null +++ b/plugins/org.eclipse.sirius.common/src/org/eclipse/sirius/common/tools/internal/interpreter/ClassLoadingService.java @@ -0,0 +1,74 @@ +/******************************************************************************* + * Copyright (c) 2015 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.sirius.common.tools.internal.interpreter; + +import java.util.Iterator; +import java.util.List; + +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.sirius.common.tools.DslCommonPlugin; +import org.eclipse.sirius.common.tools.api.interpreter.ClassLoading; +import org.eclipse.sirius.common.tools.api.util.EclipseUtil; + +/** + * Runtime-wide configuration for ClassLoading used by Interpreter instances. + * instances. + * + * @author Cedric Brun <cedric.brun@obeo.fr> + * + */ +public final class ClassLoadingService { + + /** + * A default class loading utility which is suitable for deployed modeler. + */ + public static final ClassLoading DEFAULT = new BundleClassLoading(); + + /** + * ClassLoading service extension point ID. + */ + private static final String ID = "org.eclipse.sirius.common.classloading_override"; + + /** + * Extension point attribute to get the {@link ClassLoading} class. + */ + private static final String CLASS_ATTRIBUTE = "class"; + + private ClassLoadingService() { + + } + + /** + * return the class loading utility to use. + * + * @return the class loading utility to use. + */ + public static ClassLoading getClassLoading() { + final List<ClassLoading> providedClassLoadings = EclipseUtil.getExtensionPlugins(ClassLoading.class, ID, CLASS_ATTRIBUTE); + Iterator<ClassLoading> it = providedClassLoadings.iterator(); + ClassLoading picked = null; + while (it.hasNext()) { + if (picked == null) { + picked = it.next(); + } else { + final IStatus status = new Status(IStatus.WARNING, DslCommonPlugin.PLUGIN_ID, "Several overrides are contributed for the class loading override, " + it.next().getClass().getName() + + " will be ignored"); + DslCommonPlugin.getDefault().getLog().log(status); + } + } + if (picked == null) { + picked = DEFAULT; + } + return picked; + } + +} diff --git a/plugins/org.eclipse.sirius.doc/doc/Release Notes.html b/plugins/org.eclipse.sirius.doc/doc/Release Notes.html index 70d6617d37..119aafc5f3 100644 --- a/plugins/org.eclipse.sirius.doc/doc/Release Notes.html +++ b/plugins/org.eclipse.sirius.doc/doc/Release Notes.html @@ -239,6 +239,9 @@ <code>org.eclipse.core.runtime.Platform.getExtensionRegistry().getConfigurationElementsFor()</code> and check <code>Platform.isRunning()</code> before. </li> + <li>A new class + <code>org.eclipse.sirius.common.tools.api.interpreter.JavaExtensionsManager</code> can now be used by langages interpreters to benefit from a consistent handling of Java extensions and support for loading Java services from the workspace. + </li> </ul> <h4 id="Changesinorg.eclipse.sirius.ecore.extender">Changes in <code>org.eclipse.sirius.ecore.extender</code> diff --git a/plugins/org.eclipse.sirius.doc/doc/Release Notes.textile b/plugins/org.eclipse.sirius.doc/doc/Release Notes.textile index 594e44c29e..03f12641ea 100644 --- a/plugins/org.eclipse.sirius.doc/doc/Release Notes.textile +++ b/plugins/org.eclipse.sirius.doc/doc/Release Notes.textile @@ -67,6 +67,7 @@ h4. Changes in @org.eclipse.sirius.common@ * The @org.eclipse.sirius.common.tools.api.resource.ImageFileFormat@ class has been added (it was in @org.eclipse.sirius.ext.swt@ before). * The interface @org.eclipse.sirius.business.api.migration.IMigrationParticipant@ exposes a new method @postXMLEndElement([..])@ which is called during a migration operation. This method should be overrided by participants which have to hook the loading process after each end of XML tag. The corresponding abstract class @org.eclipse.sirius.business.api.migration.AbstractMigrationParticipant@ provides a default NO-OP implementation. * @org.eclipse.sirius.common.tools.api.util.EclipseUtil.getConfigurationElementsFor()@ added to call @org.eclipse.core.runtime.Platform.getExtensionRegistry().getConfigurationElementsFor()@ and check @Platform.isRunning()@ before. +* A new class @org.eclipse.sirius.common.tools.api.interpreter.JavaExtensionsManager@ can now be used by langages interpreters to benefit from a consistent handling of Java extensions and support for loading Java services from the workspace. h4. Changes in @org.eclipse.sirius.ecore.extender@ diff --git a/plugins/org.eclipse.sirius.editor/META-INF/MANIFEST.MF b/plugins/org.eclipse.sirius.editor/META-INF/MANIFEST.MF index addde8c1b5..d84dda63ee 100644 --- a/plugins/org.eclipse.sirius.editor/META-INF/MANIFEST.MF +++ b/plugins/org.eclipse.sirius.editor/META-INF/MANIFEST.MF @@ -198,11 +198,14 @@ Export-Package: org.eclipse.sirius.editor.assist.content;version="2.0.4", org.eclipse.sirius.editor.tools.internal.property.filter;x-internal:=true;version="2.0.4", org.eclipse.sirius.editor.tools.internal.property.section;x-internal:=true;version="2.0.4", org.eclipse.sirius.editor.tools.internal.wizards;x-internal:=true;version="2.0.4", - org.eclipse.sirius.editor.utils;version="2.0.4", + org.eclipse.sirius.editor.utils;version="2.1.4", org.eclipse.sirius.editor.wizards;version="2.0.4" Bundle-ActivationPolicy: lazy Import-Package: org.eclipse.sirius.ext.base;version="2.0.0", org.eclipse.sirius.ext.base.relations;version="2.0.0", org.eclipse.sirius.ext.e3.ui.dialogs;version="3.0.0", org.eclipse.sirius.ext.emf;version="2.0.0", - org.eclipse.sirius.ext.swt;version="2.0.0" + org.eclipse.sirius.ext.swt;version="2.0.0", + org.eclipse.jdt.core, + org.eclipse.pde.core, + org.eclipse.pde.core.plugin diff --git a/plugins/org.eclipse.sirius.editor/plugin.xml b/plugins/org.eclipse.sirius.editor/plugin.xml index e238588633..c4ab82d0e4 100644 --- a/plugins/org.eclipse.sirius.editor/plugin.xml +++ b/plugins/org.eclipse.sirius.editor/plugin.xml @@ -1299,6 +1299,7 @@ <customizer class="org.eclipse.sirius.editor.tools.internal.editor.SiriusCoreEditorCustomization"></customizer> </extension> + <extension point="org.eclipse.ui.commands"> <command @@ -1329,6 +1330,13 @@ class="org.eclipse.sirius.editor.tools.internal.commands.VSMQuickOutlineHandler" commandId="org.eclipse.sirius.editor.command.show.outline"> </handler> + </extension> + + <extension + point="org.eclipse.sirius.common.classloading_override"> + <override + class="org.eclipse.sirius.editor.utils.WorkspaceClassLoading"> + </override> </extension> <!-- End of user code plugin.xml end specifics --> diff --git a/plugins/org.eclipse.sirius.editor/src-gen/org/eclipse/sirius/editor/utils/WorkspaceClassLoading.java b/plugins/org.eclipse.sirius.editor/src-gen/org/eclipse/sirius/editor/utils/WorkspaceClassLoading.java new file mode 100644 index 0000000000..55daa1ed7a --- /dev/null +++ b/plugins/org.eclipse.sirius.editor/src-gen/org/eclipse/sirius/editor/utils/WorkspaceClassLoading.java @@ -0,0 +1,554 @@ +/******************************************************************************* + * Copyright (c) 2013, 2014 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.sirius.editor.utils; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +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.IResourceDeltaVisitor; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Platform; +import org.eclipse.core.runtime.Status; +import org.eclipse.emf.ecore.plugin.EcorePlugin; +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.BaseDescription; +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.HostSpecification; +import org.eclipse.osgi.service.resolver.ImportPackageSpecification; +import org.eclipse.osgi.service.resolver.VersionConstraint; +import org.eclipse.pde.core.plugin.IPluginAttribute; +import org.eclipse.pde.core.plugin.IPluginElement; +import org.eclipse.pde.core.plugin.IPluginExtension; +import org.eclipse.pde.core.plugin.IPluginModelBase; +import org.eclipse.pde.core.plugin.IPluginObject; +import org.eclipse.pde.core.plugin.PluginRegistry; +import org.eclipse.sirius.common.tools.api.interpreter.ClasspathChangeCallback; +import org.eclipse.sirius.common.tools.api.interpreter.EPackageLoadingCallback; +import org.eclipse.sirius.common.tools.api.interpreter.EPackageLoadingCallback.EPackageDeclaration; +import org.eclipse.sirius.common.tools.internal.interpreter.BundleClassLoading; +import org.eclipse.sirius.editor.editorPlugin.SiriusEditorPlugin; +import org.osgi.framework.Bundle; +import org.osgi.framework.Constants; + +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; + +/** + * Limitations : + * <ul> + * <li>if you have in your workspace some projects which are actually used by + * Sirius and which might pass instances to the query implementation, things + * *will* go wrong as this utility will search first in the workspace.</li> + * <li>you can't use the generated model interfaces</li> + * </ul> + * + * @author cedric + */ +public class WorkspaceClassLoading extends BundleClassLoading { + + private IResourceChangeListener workspaceListener = new IResourceChangeListener() { + + @Override + public void resourceChanged(IResourceChangeEvent event) { + /* + * we are only interested in change which have been completed on the + * workspace. + */ + if (event.getType() != IResourceChangeEvent.POST_CHANGE) + return; + + final Set<String> changed = Sets.newLinkedHashSet(); + + IResourceDeltaVisitor projectClassesInvalidator = new IResourceDeltaVisitor() { + + @Override + public boolean visit(IResourceDelta delta) throws CoreException { + if (delta.getKind() != IResourceDelta.CHANGED) + return true; + // only interested in content changes + if ((delta.getFlags() & IResourceDelta.CONTENT) == 0) + return true; + IResource resource = delta.getResource(); + // only interested in files with the "txt" extension + if (resource.getType() == IResource.FILE + && ("class".equalsIgnoreCase(resource.getFileExtension()) || "ecore".equalsIgnoreCase(resource.getFileExtension()) || "MANIFEST.MF".equalsIgnoreCase(resource.getName())) + && resource.getProject() != null && resource.getProject().getName() != null) { + changed.add(resource.getProject().getName()); + } + return true; + } + + }; + try { + event.getDelta().accept(projectClassesInvalidator); + } catch (CoreException e) { + /* + * We can get errors when inspecting the changes if, for + * instance, the workspace was not completely refreshed. + */ + SiriusEditorPlugin.INSTANCE.log(e); + } + + if (changed.size() > 0) { + for (String projectName : changed) { + URLClassLoader old = projectsToClassLoader.get(projectName); + if (old != null) { + try { + old.close(); + } catch (IOException e) { + SiriusEditorPlugin.INSTANCE.log(e); + } + } + projectsToClassLoader.remove(projectName); + } + if (callback != null) { + callback.classpathChanged(changed); + } + } + } + + }; + + public WorkspaceClassLoading() { + + } + + protected Collection<Object> findCallees(IPluginModelBase model) { + BundleDescription desc = model.getBundleDescription(); + if (desc == null) + return Collections.<Object> emptyList(); + BundleDescription fFragmentDescription = null; + HostSpecification spec = desc.getHost(); + if (spec != null) { + fFragmentDescription = desc; + List<Object> fragmentDependencies = Lists.newArrayList(getDependencies(desc, fFragmentDescription)); + BaseDescription host = spec.getSupplier(); + if (host instanceof BundleDescription) { + BundleDescription hostDesc = (BundleDescription) host; + /* + * check to see if the host is already included as a dependency. + * If so, we don't need to include the host manually. + */ + for (Object object : fragmentDependencies) { + BundleDescription dependency = null; + if (object instanceof BundleSpecification) + dependency = ((BundleSpecification) object).getBundle(); + else if (object instanceof ImportPackageSpecification) { + ExportPackageDescription epd = (ExportPackageDescription) ((ImportPackageSpecification) object).getSupplier(); + if (epd != null) + dependency = epd.getSupplier(); + } + if (dependency != null && dependency.equals(hostDesc)) + return fragmentDependencies; + } + + // host not included as dependency, include it manually. + fragmentDependencies.add(0, hostDesc); + return fragmentDependencies; + } + return fragmentDependencies; + } + return getDependencies(desc, fFragmentDescription); + } + + private Collection<Object> getDependencies(BundleDescription desc, BundleDescription fFragmentDescription) { + /* + * use map to store dependencies so if Import-Package is supplied by + * same BundleDescription as supplier of Require-Bundle, it only shows + * up once Also, have to use BundleSpecficiation instead of + * BundleDescroption to show re-exported icon on re-exported + * Required-Bundles Have to use ImportPackageSpecification to determine + * if an import is optional and should be filtered. + */ + HashMap<Object, Object> dependencies = new HashMap<Object, Object>(); + BundleSpecification[] requiredBundles = desc.getRequiredBundles(); + for (int i = 0; i < requiredBundles.length; i++) { + BaseDescription bd = requiredBundles[i].getSupplier(); + if (bd != null) + dependencies.put(bd, requiredBundles[i]); + else + dependencies.put(requiredBundles[i], requiredBundles[i]); + } + ImportPackageSpecification[] importedPkgs = desc.getImportPackages(); + for (int i = 0; i < importedPkgs.length; i++) { + BaseDescription bd = importedPkgs[i].getSupplier(); + if (bd != null && bd instanceof ExportPackageDescription) { + BundleDescription exporter = ((ExportPackageDescription) bd).getExporter(); + if (exporter != null) { + Object obj = dependencies.get(exporter); + if (obj == null) { + dependencies.put(exporter, importedPkgs[i]); + } else if (!Constants.RESOLUTION_OPTIONAL.equals(importedPkgs[i].getDirective(Constants.RESOLUTION_DIRECTIVE)) && obj instanceof ImportPackageSpecification + && Constants.RESOLUTION_OPTIONAL.equals(((ImportPackageSpecification) obj).getDirective(Constants.RESOLUTION_DIRECTIVE))) { + /* + * if we have a non-optional Import-Package dependency + * on a bundle which we already depend on, check to make + * sure our current dependency is not optional. If it + * is, replace the optional dependency with the + * non-optional one + */ + dependencies.put(exporter, importedPkgs[i]); + } + } + } + /* + * ignore unresolved packages + */ + } + /* + * include fragments which are "linked" to this bundle + */ + BundleDescription frags[] = desc.getFragments(); + for (int i = 0; i < frags.length; i++) { + if (!frags[i].equals(fFragmentDescription)) + dependencies.put(frags[i], frags[i]); + } + return dependencies.values(); + } + + private void computeURLs(IProject project, List<URL> uRLs) { + 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) { + IFile reference = ResourcesPlugin.getWorkspace().getRoot().getFile(output); + if (reference.exists()) { + URL url; + try { + url = reference.getLocation().toFile().toURI().toURL(); + uRLs.add(url); + } catch (MalformedURLException e) { + /* + * We don't know how to handle this class path + * entry. + */ + } + } + + } + } + } + /* + * Add the default output location to the classpath anyway since + * source folders are not required to have their own + */ + final IPath output = javaProject.getOutputLocation(); + if (output != null) { + IFolder reference = ResourcesPlugin.getWorkspace().getRoot().getFolder(output); + if (reference.exists() && reference.getLocation() != null) { + URL url; + File file = reference.getLocation().toFile(); + try { + if (file != null && file.exists()) { + url = file.toURI().toURL(); + uRLs.add(url); + } + } catch (MalformedURLException e) { + /* + * the given path does not map to a file which can + * actually be mapped to an url, ignore it. + */ + } + } + + } + } catch (JavaModelException e) { + } + } + + private Map<String, URLClassLoader> projectsToClassLoader = Maps.newHashMap(); + + private ClasspathChangeCallback callback; + + @Override + public Class findClass(Set<String> viewpointProjects, Set<String> plugins, String qualifiedName) { + Class result = null; + IWorkspaceRoot root = EcorePlugin.getWorkspaceRoot(); + if (root != null) { + for (String projectName : viewpointProjects) { + ClassLoader loader = getOrCreateClassLoader(projectName, root); + if (loader != null) { + try { + result = loader.loadClass(qualifiedName); + } catch (ClassNotFoundException e) { + /* + * nothing to report,we would not find the class. + */ + } catch (NoClassDefFoundError e) { + /* + * nothing to report,we would not find the class. + */ + } + } + } + } + Iterator<String> it = plugins.iterator(); + while (result == null && it.hasNext()) { + Bundle bnd = Platform.getBundle(it.next()); + if (bnd != null) { + try { + result = bnd.loadClass(qualifiedName); + } catch (Throwable e) { + /* + * the class we are searching wasn't there. + */ + } + } + } + return result; + } + + private ClassLoader getOrCreateClassLoader(String projectName, IWorkspaceRoot root) { + URLClassLoader existing = projectsToClassLoader.get(projectName); + if (existing == null) { + existing = createClassLoader(projectName, root); + if (existing != null) { + projectsToClassLoader.put(projectName, existing); + } + } + return existing; + } + + protected Collection<EPackageLoadingCallback.EPackageDeclarationSource> getEPackagesDeclaredInBundles(Collection<String> bundles) { + Collection<EPackageLoadingCallback.EPackageDeclarationSource> result = Lists.newArrayList(); + IWorkspaceRoot root = EcorePlugin.getWorkspaceRoot(); + Set<String> bundlesToSearchInPlatform = Sets.newLinkedHashSet(); + if (root != null) { + Map<String, IPluginModelBase> symbolicNamestoModels = getBundlesInWorkspace(); + for (String bundleID : bundles) { + IPluginModelBase workspaceVersion = symbolicNamestoModels.get(bundleID); + if (workspaceVersion != null) { + for (IPluginExtension extensions : workspaceVersion.getExtensions().getExtensions()) { + if (EMF_GENERATED_PACKAGE_EXTENSIONPOINT.equals(extensions.getPoint())) { + String symbolicNameForProject = workspaceVersion.getBundleDescription().getSymbolicName(); + List<EPackageLoadingCallback.EPackageDeclaration> declarations = Lists.newArrayList(); + + for (IPluginObject object : extensions.getChildren()) { + if (object instanceof IPluginElement) { + IPluginElement element = (IPluginElement) object; + String nsURI = getAttributeValue(element, "uri"); + String className = getAttributeValue(element, "class"); + String genModel = getAttributeValue(element, "genModel"); + if (nsURI != null && className != null) { + declarations.add(new EPackageDeclaration(nsURI, className, genModel)); + } else { + IStatus status = new Status(IStatus.WARNING, SiriusEditorPlugin.PLUGIN_ID, "An EPackage declaration in project " + symbolicNameForProject + + " has been ignored because of missing informations."); + SiriusEditorPlugin.INSTANCE.log(status);; + } + } + } + if (declarations.size() > 0) { + result.add(new EPackageLoadingCallback.EPackageDeclarationSource(symbolicNameForProject, declarations, false)); + } + } + } + } else { + bundlesToSearchInPlatform.add(bundleID); + } + } + result.addAll(super.getEPackagesDeclaredInBundles(bundlesToSearchInPlatform)); + } else { + result = super.getEPackagesDeclaredInBundles(bundles); + } + return result; + } + + @Override + protected Set<String> getBundleDependencies(String currentBundleID) { + Set<String> dependenciesRetrievedThroughPlatform = super.getBundleDependencies(currentBundleID); + IPluginModelBase pdeModel = PluginRegistry.findModel(currentBundleID); + if (pdeModel != null && pdeModel.getUnderlyingResource() instanceof IFile) { + Set<String> dependenciesRetrievedThroughPDE = getDependenciesFromPDE(pdeModel); + return dependenciesRetrievedThroughPDE; + } else { + return dependenciesRetrievedThroughPlatform; + } + + } + + protected Set<String> getDependenciesFromPDE(IPluginModelBase pdeModel) { + Set<String> dependenciesRetrievedThroughPDE = Sets.newLinkedHashSet(); + /* + * we have this bundle in the workspace. + */ + Collection<Object> dependencies = Sets.newLinkedHashSet(findCallees(pdeModel)); + for (VersionConstraint requireBundleOrImportPackage : Iterables.filter(dependencies, VersionConstraint.class)) { + if (requireBundleOrImportPackage.getSupplier() instanceof BundleDescription && ((BundleDescription) requireBundleOrImportPackage.getSupplier()).getSymbolicName() != null) { + dependenciesRetrievedThroughPDE.add(((BundleDescription) requireBundleOrImportPackage.getSupplier()).getSymbolicName()); + } + } + return dependenciesRetrievedThroughPDE; + } + + private String getAttributeValue(IPluginElement element, String key) { + IPluginAttribute attr = element.getAttribute(key); + if (attr != null) { + return attr.getValue(); + } + return null; + } + + private URLClassLoader createClassLoader(String projectName, IWorkspaceRoot root) { + Map<String, IPluginModelBase> symbolicNamestoModels = getBundlesInWorkspace(); + + final Collection<IPluginModelBase> workspaceDependencies = Lists.newArrayList(); + final Collection<Bundle> installedDependencies = Lists.newArrayList(); + + collectAllClassPath(projectName, root, symbolicNamestoModels, workspaceDependencies, installedDependencies); + + List<URL> urls = new ArrayList<URL>(); + /* + * declare the URLs for the workspace dependencies + */ + for (IPluginModelBase wksDep : workspaceDependencies) { + IResource res = wksDep.getUnderlyingResource(); + if (res != null) { + computeURLs(res.getProject(), urls); + } + } + + URLClassLoader loader = new URLClassLoader(urls.toArray(new URL[urls.size()])) { + + @Override + public Class findClass(String name) throws ClassNotFoundException { + Class found = null; + /* + * search in the workspace projects first... + */ + try { + found = super.findClass(name); + } catch (Throwable e) { + /* + * the class we are searching wasn't found in the URLs + */ + } + Iterator<Bundle> it = installedDependencies.iterator(); + while (found == null && it.hasNext()) { + try { + found = it.next().loadClass(name); + } catch (Throwable e) { + /* + * the class we are searching wasn't there. + */ + } + + } + return found; + } + }; + + return loader; + } + + protected void collectAllClassPath(String projectName, IWorkspaceRoot root, Map<String, IPluginModelBase> symbolicNamestoModels, final Collection<IPluginModelBase> workspaceDependencies, + final Collection<Bundle> installedDependencies) { + IPluginModelBase pdeModel = PluginRegistry.findModel(projectName); + if (pdeModel != null) { + Collection<String> symbolicNames = getDependenciesFromPDE(pdeModel); + for (String requirementSymbolicName : symbolicNames) { + IPluginModelBase requiredDep = symbolicNamestoModels.get(requirementSymbolicName); + if (requiredDep != null) { + workspaceDependencies.add(requiredDep); + } else { + Bundle bnd = Platform.getBundle(requirementSymbolicName); + if (bnd != null) { + installedDependencies.add(bnd); + } + + } + + } + } + IPluginModelBase currentProject = symbolicNamestoModels.get(projectName); + if (currentProject != null) { + workspaceDependencies.add(currentProject); + } else { + Bundle bnd = Platform.getBundle(projectName); + if (bnd != null) { + installedDependencies.add(bnd); + } + } + } + + private Map<String, IPluginModelBase> getBundlesInWorkspace() { + Map<String, IPluginModelBase> symbolicNamestoModels = Maps.newHashMap(); + IPluginModelBase[] wrkspcesModels = PluginRegistry.getWorkspaceModels(); + for (int i = 0; i < wrkspcesModels.length; i++) { + IPluginModelBase iPluginModelBase = wrkspcesModels[i]; + if (iPluginModelBase.getBundleDescription() != null && iPluginModelBase.getBundleDescription().getSymbolicName() != null) { + symbolicNamestoModels.put(iPluginModelBase.getBundleDescription().getSymbolicName(), iPluginModelBase); + } + } + return symbolicNamestoModels; + } + + @Override + public void dispose() { + this.projectsToClassLoader.clear(); + IWorkspaceRoot root = EcorePlugin.getWorkspaceRoot(); + if (this.callback != null && root != null) { + ResourcesPlugin.getWorkspace().removeResourceChangeListener(workspaceListener); + } + for (URLClassLoader loader : projectsToClassLoader.values()) { + try { + loader.close(); + } catch (IOException e) { + SiriusEditorPlugin.INSTANCE.log(e); + } + } + projectsToClassLoader.clear(); + } + + @Override + public void setClasspathChangeCallback(ClasspathChangeCallback listener) { + IWorkspaceRoot root = EcorePlugin.getWorkspaceRoot(); + if (this.callback != null && listener == null && root != null) { + ResourcesPlugin.getWorkspace().removeResourceChangeListener(workspaceListener); + } + this.callback = listener; + if (root != null) { + ResourcesPlugin.getWorkspace().addResourceChangeListener(workspaceListener); + } + + } +} |
