Java tutorial
/******************************************************************************* * Copyright (c) 2013, 2016 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 javax.lang.model.SourceVersion; 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.DslCommonPlugin; 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 = "/"; //$NON-NLS-1$ private static final Set<String> JAVA_SERVICES_BUNDLES_WHITE_LIST = Sets.newHashSet(DslCommonPlugin.PLUGIN_ID); /** * 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 boolean shouldLoadServices = true; private boolean shouldLoadEPackages = true; private ClasspathChangeCallback onWorkspaceChange = new ClasspathChangeCallback() { @Override 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) { reload(); } } }; 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(); } /** * 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) { reload(); } } } /* * This method is synchronized as we expect the workspace listener to call * it when an event requires a reload. There is no guarantee this call is * made from the same thread as the others and we don't want several reloads * being done concurrently. */ private synchronized void reload() { this.shouldLoadEPackages = true; this.shouldLoadServices = true; } /*** * This will trigger the loading of EPackages or java Classes with the * current configuration (search scope and imports). */ public synchronized void reloadIfNeeded() { if (this.shouldLoadEPackages) { reloadEPackages(); this.shouldLoadEPackages = false; } if (this.shouldLoadServices) { loadJavaExtensions(this.imports); this.shouldLoadServices = false; } } 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); //$NON-NLS-1$ 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("/"); //$NON-NLS-1$ } EObject genModel = set.getEObject(genModelURI, true); if (genModel != null && genModel.eClass().getEPackage() != null && "GenModel".equals(genModel.eClass().getName()) //$NON-NLS-1$ && "genmodel".equals(genModel.eClass().getEPackage().getName())) { //$NON-NLS-1$ Collection<EObject> genPackages = (Collection<EObject>) genModel .eGet(genModel.eClass().getEStructuralFeature("genPackages")); //$NON-NLS-1$ 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")); //$NON-NLS-1$ 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")); //$NON-NLS-1$ 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 (classQualifiedName != null && SourceVersion.isName(classQualifiedName)) { boolean newImport = this.imports.add(classQualifiedName); if (newImport) { this.shouldLoadServices = true; } } } /** * 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)) { 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 loadJavaExtensions(Set<String> addedImports) { Map<String, Class> toUnload = Maps.newLinkedHashMap(); Map<String, Class> toLoad = Maps.newLinkedHashMap(); Set<String> notFound = Sets.newLinkedHashSet(); for (String qualifiedName : addedImports) { Class<?> found = classLoading.findClass(viewpointProjects, viewpointPlugins, qualifiedName); if (found == null) { // Find services in white list found = classLoading.findClass(Collections.<String>emptySet(), JAVA_SERVICES_BUNDLES_WHITE_LIST, qualifiedName); } if (found != null) { this.couldNotBeLoaded.remove(qualifiedName); Class<?> alreadyHere = loadedClasses.get(qualifiedName); if (alreadyHere != null) { toUnload.put(qualifiedName, alreadyHere); } loadedClasses.put(qualifiedName, found); toLoad.put(qualifiedName, found); } else { notFound.add(qualifiedName); this.couldNotBeLoaded.add(qualifiedName); } } /* * We make sure we notify the callbacks with the following orders : * always notify an unload first, then the loaded classes, then the * qualified names which were not found. This makes it easier for the * callbacks to maintain their own data structures in sync regarding the * latest versions of the classes. */ for (Map.Entry<String, Class> classToUnload : toUnload.entrySet()) { unloaded(classToUnload.getKey(), classToUnload.getValue()); } for (Map.Entry<String, Class> classToLoad : toLoad.entrySet()) { loaded(classToLoad.getKey(), classToLoad.getValue()); } for (String qualifiedName : notFound) { 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; } }