| /******************************************************************************* |
| * Copyright (c) 2012, 2018 Obeo and others. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * Obeo - initial API and implementation |
| * Stefan Dirix - bug 464904 |
| * Philip Langer - bug 516508, 508526 |
| *******************************************************************************/ |
| package org.eclipse.emf.compare.ide.internal.utils; |
| |
| import static com.google.common.collect.Iterables.transform; |
| import static com.google.common.collect.Lists.newArrayList; |
| import static org.eclipse.emf.compare.ide.utils.ResourceUtil.createURIFor; |
| |
| import com.google.common.annotations.Beta; |
| import com.google.common.base.Function; |
| import com.google.common.base.Predicate; |
| import com.google.common.collect.Collections2; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableSet; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.LinkedHashMap; |
| import java.util.ListIterator; |
| import java.util.Map; |
| import java.util.Queue; |
| import java.util.Set; |
| import java.util.concurrent.ConcurrentLinkedQueue; |
| |
| import org.eclipse.core.resources.IStorage; |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.NullProgressMonitor; |
| import org.eclipse.core.runtime.OperationCanceledException; |
| import org.eclipse.core.runtime.Status; |
| import org.eclipse.core.runtime.SubMonitor; |
| import org.eclipse.emf.common.notify.Notifier; |
| import org.eclipse.emf.common.util.EList; |
| import org.eclipse.emf.common.util.TreeIterator; |
| import org.eclipse.emf.common.util.URI; |
| import org.eclipse.emf.common.util.WrappedException; |
| import org.eclipse.emf.compare.ide.EMFCompareIDEPlugin; |
| import org.eclipse.emf.compare.ide.hook.IResourceSetHook; |
| import org.eclipse.emf.compare.ide.internal.EMFCompareIDEMessages; |
| import org.eclipse.emf.compare.ide.internal.hook.ResourceSetHookRegistry; |
| import org.eclipse.emf.compare.ide.utils.ResourceUtil; |
| import org.eclipse.emf.compare.ide.utils.StorageTraversal; |
| import org.eclipse.emf.compare.rcp.EMFCompareRCPPlugin; |
| import org.eclipse.emf.compare.rcp.policy.ILoadOnDemandPolicy; |
| import org.eclipse.emf.ecore.EObject; |
| import org.eclipse.emf.ecore.EStructuralFeature; |
| import org.eclipse.emf.ecore.InternalEObject; |
| import org.eclipse.emf.ecore.resource.Resource; |
| import org.eclipse.emf.ecore.resource.URIConverter; |
| import org.eclipse.emf.ecore.resource.impl.ExtensibleURIConverterImpl; |
| import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl; |
| import org.eclipse.emf.ecore.util.EcoreUtil; |
| import org.eclipse.emf.ecore.util.InternalEList; |
| import org.eclipse.emf.ecore.xmi.XMLResource; |
| |
| /** |
| * This implementation of a resource set will be created from a {@link StorageTraversal}, and only those |
| * resources that are part of the traversal will be loaded. This will allow us to resolve the proxies between |
| * these "traversed" resources. |
| * <p> |
| * This resource set will prevent loading any resources that is not part of the initial traversal. The only |
| * exception to this rule is if one of the registered {@link ILoadOnDemandPolicy} says otherwise. Users should |
| * not try to add more resources into this resource set using any of the createResource methods. |
| * </p> |
| * |
| * @author <a href="mailto:laurent.goubet@obeo.fr">Laurent Goubet</a> |
| */ |
| @Beta |
| public final class NotLoadingResourceSet extends ResourceSetImpl implements DisposableResourceSet, IProxyCreationListener { |
| /** |
| * All of the storages that this resource set is going to load, mapped to their resource (workspace) URIs. |
| */ |
| private final Map<URI, IStorage> storageToURI; |
| |
| /** |
| * Registry holding {@link IResourceSetHook}s. |
| */ |
| private final ResourceSetHookRegistry hookRegistry; |
| |
| /** |
| * Holds <code>true</code> if the resource set has been disposed. |
| */ |
| private boolean isDisposed; |
| |
| /** Registers the proxies we detect while resolving our resources. */ |
| private Queue<ProxyEntry> proxyQueue; |
| |
| /** Turn from not loading resource set into loading resource set. */ |
| private boolean allowResourceLoad; |
| |
| /** |
| * Constructor. |
| * |
| * @param storagesToLoad |
| * see {@link #storageToURI}. |
| * @param hookRegistry |
| * Registry holding {@link IResourceSetHook}s. |
| */ |
| private NotLoadingResourceSet(Map<URI, IStorage> storagesToLoad, ResourceSetHookRegistry hookRegistry) { |
| this.storageToURI = storagesToLoad; |
| this.hookRegistry = hookRegistry; |
| this.proxyQueue = new ConcurrentLinkedQueue<ProxyEntry>(); |
| } |
| |
| /** |
| * Constructs a resource set to contain the resources described by the given traversals. |
| * |
| * @param traversals |
| * All traversals we are to load. |
| * @param monitor |
| * the monitor to which progress will be reported. |
| * @param resourceSetHookRegistry |
| * A registry of {@link IResourceSetHook}s that potentialy can hook on the new |
| * {@link org.eclipse.emf.ecore.resource.ResourceSet} (can be <code>null</code> if none). |
| * @return resource set containing the resources described by the given traversals. |
| */ |
| public static NotLoadingResourceSet create(final StorageTraversal traversals, IProgressMonitor monitor, |
| ResourceSetHookRegistry resourceSetHookRegistry) { |
| SubMonitor progress = SubMonitor.convert(monitor, 100); |
| progress.subTask(EMFCompareIDEMessages.getString("NotLoadingResourceSet.monitor.resolve")); //$NON-NLS-1$ |
| |
| final URIConverter converter = new ExtensibleURIConverterImpl(); |
| final Set<? extends IStorage> storages = traversals.getStorages(); |
| final Map<URI, IStorage> storageToURI = new LinkedHashMap<URI, IStorage>(storages.size()); |
| for (IStorage storage : storages) { |
| final IStorage old = storageToURI.put(converter.normalize(createURIFor(storage)), storage); |
| if (old != null) { |
| // FIXME debug and log : duplicate storage URI somehow |
| } |
| } |
| final NotLoadingResourceSet resourceSet = new NotLoadingResourceSet(storageToURI, |
| resourceSetHookRegistry); |
| resourceSet.setURIConverter(converter); |
| |
| resourceSet.setURIResourceMap(new HashMap<URI, Resource>(storages.size() << 1)); |
| |
| // loading is 60% of the total work? |
| final int loadWorkPercentage = 60; |
| SubMonitor subMonitor = progress.newChild(loadWorkPercentage).setWorkRemaining(storages.size()); |
| |
| resourceSet.load(subMonitor); |
| |
| final int resolveWorkPercentage = 40; |
| subMonitor = progress.newChild(resolveWorkPercentage); |
| |
| // Then resolve all proxies between our "loaded" resources. |
| resourceSet.resolveProxies(subMonitor); |
| |
| return resourceSet; |
| } |
| |
| /** |
| * Retrieves the hooks that need to be hooked on this resource set. |
| * |
| * @param urisToLoad |
| * The collection of uris that will be loaded. |
| * @return {@link Collection} of {@link IResourceSetHook}s that need to be hocked. |
| */ |
| private Collection<IResourceSetHook> getMatchingHooks(final Collection<URI> urisToLoad) { |
| final Collection<IResourceSetHook> hooks; |
| if (hookRegistry == null) { |
| hooks = Collections.emptyList(); |
| } else { |
| hooks = Collections2.filter(hookRegistry.getResourceSetHooks(), |
| new Predicate<IResourceSetHook>() { |
| |
| public boolean apply(IResourceSetHook input) { |
| return input.isHookFor(urisToLoad); |
| } |
| }); |
| } |
| return hooks; |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * @see org.eclipse.emf.ecore.resource.impl.ResourceSetImpl#demandLoadHelper(org.eclipse.emf.ecore.resource.Resource) |
| */ |
| @Override |
| protected void demandLoadHelper(Resource resource) { |
| // Checks if there is an IStorage that should be used to fill this ressource. |
| IStorage storage = getMatchingStorage(resource); |
| if (storage != null) { |
| try { |
| loadFromStorage(resource, storage); |
| } catch (IOException e) { |
| logLoadingFromStorageFailed(resource, storage, e); |
| } |
| } else { |
| super.demandLoadHelper(resource); |
| } |
| } |
| |
| /** |
| * Logs a error on a {@link Resource} that has failed loading. |
| * |
| * @param resource |
| * The {@link Resource} to be loaded. |
| * @param storage |
| * The {@link IStorage} providing the content of the resource. |
| * @param e |
| * The raised exception. |
| */ |
| private void logLoadingFromStorageFailed(Resource resource, IStorage storage, Exception e) { |
| Status errorStatus = new Status(IStatus.ERROR, EMFCompareIDEPlugin.PLUGIN_ID, |
| EMFCompareIDEMessages.getString("StorageResourceWrapper.failToLoad", //$NON-NLS-1$ |
| resource.getURI().toString(), storage.getName()), |
| e); |
| EMFCompareIDEPlugin.getDefault().getLog().log(errorStatus); |
| } |
| |
| /** |
| * Gets the {@link IStorage} that should be used to fill the passed {@link Resource}. |
| * |
| * @param resource |
| * The {@link Resource} to load. |
| * @return A {@link IStorage} to use for loading the resource or <code>null</code> if there is no |
| * {@link IStorage} for this {@link Resource}. |
| */ |
| private IStorage getMatchingStorage(Resource resource) { |
| URIConverter theURIConverter = getURIConverter(); |
| URI resourceNormalizedUri = theURIConverter.normalize(resource.getURI()); |
| return storageToURI.get(resourceNormalizedUri); |
| } |
| |
| /** |
| * Loads a {@link Resource} using a the content of {@link IStorage}. |
| * |
| * @param resource |
| * The {@link Resource} to be loaded. |
| * @param storage |
| * The {@link IStorage} providing the content. |
| * @throws IOException |
| * Exception raised if there is an error with the {@link IStorage}. |
| */ |
| private void loadFromStorage(Resource resource, IStorage storage) throws IOException { |
| ResourceUtil.setAssociatedStorage(resource, storage); |
| try (InputStream stream = storage.getContents()) { |
| resource.load(stream, getLoadOptions()); |
| } catch (CoreException | WrappedException e) { |
| logLoadingFromStorageFailed(resource, storage, e); |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * @see org.eclipse.emf.ecore.resource.impl.ResourceSetImpl#handleDemandLoadException(org.eclipse.emf.ecore.resource.Resource, |
| * java.io.IOException) |
| */ |
| @Override |
| protected void handleDemandLoadException(Resource resource, IOException exception) { |
| try { |
| super.handleDemandLoadException(resource, exception); |
| // CHECKSTYLE:OFF |
| } catch (RuntimeException e) { |
| // CHECKSTYLE:ON |
| // do nothing. The errors are added to the Resource#getErrors() in super(). |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * @see org.eclipse.emf.ecore.resource.impl.ResourceSetImpl#getResource(org.eclipse.emf.common.util.URI, |
| * boolean) |
| */ |
| @Override |
| public Resource getResource(URI uri, boolean loadOnDemand) { |
| checkNotDisposed(); |
| |
| // Bypass the load on demand policies when possible. |
| // Note that we cache null as well, so if a proxy fails to resolve we don't repeatedly try to load it. |
| // That lookup can be expensive for a resource set with hundreds of resources. |
| Map<URI, Resource> uriToResource = getURIResourceMap(); |
| final Resource cached = uriToResource.get(uri); |
| if (cached != null || (!allowResourceLoad && uriToResource.containsKey(uri))) { |
| return cached; |
| } |
| |
| final Resource resource; |
| // If this is a resource we know and want to load, do it |
| final IStorage storage = storageToURI.get(uri); |
| if (loadOnDemand && storage != null) { |
| resource = load(storage, uri, new NullProgressMonitor()); |
| } else if (allowResourceLoad) { |
| resource = super.getResource(uri, loadOnDemand); |
| } else { |
| ILoadOnDemandPolicy.Registry registry = EMFCompareRCPPlugin.getDefault() |
| .getLoadOnDemandPolicyRegistry(); |
| if (registry.hasAnyAuthorizingPolicy(uri)) { |
| resource = super.getResource(uri, true); |
| } else { |
| resource = super.getResource(uri, false); |
| } |
| } |
| |
| // This is only necessary to cache the case of null. |
| // Caching that allows us to avoid repeated looking up a resource that doesn't exist in the case of a |
| // broken proxy. |
| uriToResource.put(uri, resource); |
| return resource; |
| } |
| |
| /** |
| * Resolve the proxies registered when loading our resources (as mapped within {@link #proxyQueue}) so |
| * that all links between the resources we want to load have been resolved. Note that this will leave |
| * other proxies (proxies to some resource that is not included in the {@link #storageToURI} map) |
| * unresolved. |
| * |
| * @param monitor |
| * Monitor on which to report progress to the user. |
| */ |
| private void resolveProxies(SubMonitor monitor) { |
| |
| int workRemaining = proxyQueue.size(); |
| int currentCount = 0; |
| monitor.setWorkRemaining(workRemaining); |
| |
| while (!proxyQueue.isEmpty()) { |
| |
| ProxyEntry proxyEntry = proxyQueue.poll(); |
| EObject eObject = proxyEntry.eObject; |
| EStructuralFeature proxyFeature = proxyEntry.proxyFeature; |
| |
| // Get the value without resolving proxies. |
| Object value = eObject.eGet(proxyFeature, false); |
| // If it's a list... |
| if (value instanceof InternalEList<?>) { |
| // Iterate over the values without resolving proxies. |
| @SuppressWarnings("unchecked") |
| InternalEList<InternalEObject> values = (InternalEList<InternalEObject>)value; |
| final ListIterator<InternalEObject> crossRefs = values.basicListIterator(); |
| while (crossRefs.hasNext()) { |
| // If it has a proxy URI and that resource is already loaded... |
| final InternalEObject nextValue = crossRefs.next(); |
| URI eProxyURI = nextValue.eProxyURI(); |
| if (eProxyURI != null && getResource(eProxyURI.trimFragment(), false) != null) { |
| // Force it to be resolved. |
| values.get(crossRefs.previousIndex()); |
| } |
| } |
| } else if (value != null) { |
| // It must be an EObject and if has a proxy URI and that resource is already loaded... |
| URI eProxyURI = ((InternalEObject)value).eProxyURI(); |
| if (eProxyURI != null && getResource(eProxyURI.trimFragment(), false) != null) { |
| // Force it to be resolved. |
| eObject.eGet(proxyFeature, true); |
| } |
| } |
| |
| monitor.worked(1); |
| currentCount++; |
| if (currentCount > workRemaining) { |
| workRemaining = proxyQueue.size(); |
| currentCount = 0; |
| monitor.setWorkRemaining(workRemaining); |
| } |
| } |
| } |
| |
| /** |
| * Loads resources known by the {@link #storageToURI} map. |
| * |
| * @param monitor |
| * {@link IProgressMonitor} reporting progress. |
| */ |
| private void load(IProgressMonitor monitor) { |
| checkNotDisposed(); |
| |
| final Set<URI> urisToLoad = ImmutableSet.copyOf(storageToURI.keySet()); |
| final Collection<IResourceSetHook> hooks = getMatchingHooks(urisToLoad); |
| |
| for (IResourceSetHook hook : hooks) { |
| hook.preLoadingHook(this, urisToLoad); |
| } |
| |
| for (Map.Entry<URI, IStorage> entry : storageToURI.entrySet()) { |
| load(entry.getValue(), entry.getKey(), monitor); |
| } |
| |
| for (IResourceSetHook hook : hooks) { |
| hook.postLoadingHook(this, urisToLoad); |
| } |
| } |
| |
| /** |
| * Retrieve an already loaded resource with the given URI, or load it using the content from the given |
| * storage. |
| * |
| * @param storage |
| * The storage from which to load this resource. |
| * @param uri |
| * The uri to use for our new resource. |
| * @param monitor |
| * Monitor on which to report progress to the user. |
| * @return The loaded resource. |
| */ |
| private Resource load(IStorage storage, URI uri, IProgressMonitor monitor) { |
| if (monitor.isCanceled()) { |
| throw new OperationCanceledException(); |
| } |
| |
| final Resource cached = getURIResourceMap().get(uri); |
| if (cached != null) { |
| return cached; |
| } |
| |
| final Resource loaded = demandCreateResource(uri); |
| getURIResourceMap().put(uri, loaded); |
| try { |
| loadFromStorage(loaded, storage); |
| monitor.worked(1); |
| } catch (IOException e) { |
| logLoadingFromStorageFailed(loaded, storage, e); |
| } |
| return loaded; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public Map<Object, Object> getLoadOptions() { |
| this.loadOptions = super.getLoadOptions(); |
| final NotifyingParserPool parserPool = new NotifyingParserPool(false); |
| parserPool.addProxyListener(this); |
| loadOptions.put(XMLResource.OPTION_USE_PARSER_POOL, parserPool); |
| loadOptions.put(XMLResource.OPTION_USE_DEPRECATED_METHODS, Boolean.FALSE); |
| loadOptions.put(XTEXT_SCOPING_LIVE_SCOPE_OPTION, Boolean.TRUE); |
| return loadOptions; |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * @see org.eclipse.emf.compare.ide.internal.utils.DisposableResourceSet#dispose() |
| */ |
| public void dispose() { |
| ImmutableList<Resource> currentResources = ImmutableList.copyOf(getResources()); |
| Collection<URI> resourceSetUris = newArrayList( |
| transform(currentResources, new Function<Resource, URI>() { |
| public URI apply(Resource input) { |
| return input.getURI(); |
| } |
| })); |
| for (IResourceSetHook hook : getMatchingHooks(resourceSetUris)) { |
| hook.onDispose(currentResources); |
| } |
| |
| // the properties view retains resources in memory somehow (at least with uml). |
| // resource.unload does not unload all resources... |
| // removing the uml CacheAdapter isn't enough either |
| // we need to get rid of all adapters manually |
| for (Resource resource : currentResources) { |
| if (resource.isLoaded()) { |
| TreeIterator<EObject> allContents = EcoreUtil.getAllProperContents(resource, false); |
| while (allContents.hasNext()) { |
| final EObject next = allContents.next(); |
| next.eAdapters().clear(); |
| } |
| resource.eAdapters().clear(); |
| } |
| } |
| |
| getResources().clear(); |
| eAdapters().clear(); |
| isDisposed = true; |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * @see org.eclipse.emf.ecore.resource.impl.ResourceSetImpl#createResource(org.eclipse.emf.common.util.URI, |
| * java.lang.String) |
| */ |
| @Override |
| public Resource createResource(URI uri, String contentType) { |
| checkNotDisposed(); |
| getURIResourceMap().remove(uri); // clear potentially cached null value |
| return super.createResource(uri, contentType); |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * @see org.eclipse.emf.ecore.resource.impl.ResourceSetImpl#getEObject(org.eclipse.emf.common.util.URI, |
| * boolean) |
| */ |
| @Override |
| public EObject getEObject(URI uri, boolean loadOnDemand) { |
| checkNotDisposed(); |
| return super.getEObject(uri, loadOnDemand); |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * @see org.eclipse.emf.ecore.resource.impl.ResourceSetImpl#getAllContents() |
| */ |
| @Override |
| public TreeIterator<Notifier> getAllContents() { |
| checkNotDisposed(); |
| return super.getAllContents(); |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * @see org.eclipse.emf.ecore.resource.impl.ResourceSetImpl#getResources() |
| */ |
| @Override |
| public EList<Resource> getResources() { |
| checkNotDisposed(); |
| return super.getResources(); |
| } |
| |
| /** |
| * Checks that the resource set is not disposed. If it is throws an {@link IllegalStateException}. |
| * |
| * @throws IllegalStateException |
| * If the resource set is disposed. |
| */ |
| private void checkNotDisposed() { |
| if (isDisposed) { |
| throw new IllegalStateException("The resource set is disposed"); //$NON-NLS-1$ |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public void proxyCreated(Resource source, EObject eObject, EStructuralFeature eStructuralFeature, |
| EObject proxy, int position) { |
| ProxyEntry proxyEntry = new ProxyEntry(eObject, eStructuralFeature); |
| proxyQueue.add(proxyEntry); |
| } |
| |
| /** |
| * Allow/disallow the resource set to load its resources when asked to. This is useful after the |
| * comparison process, where every resource should be loaded through {@link #getResource(URI, boolean)}. |
| * |
| * @param allowResourceLoad |
| * true to allow the resource set to load its resources when asked to, false otherwise. |
| */ |
| public void setAllowResourceLoad(boolean allowResourceLoad) { |
| this.allowResourceLoad = allowResourceLoad; |
| } |
| |
| /** |
| * Helper class to encapsulate pairs of {@link EObject}s and {@link EStructuralFeature}s pointing to proxy |
| * objects. |
| */ |
| private static class ProxyEntry { |
| /** |
| * The {@link EObject} containing a {@link EStructuralFeature} pointing to a proxy object. It's |
| * protected to avoid a generated bridge method when accessed. |
| */ |
| protected final EObject eObject; |
| |
| /** |
| * The {@link EStructuralFeature} within the {@link #eObject} pointing to a proxy object. It's |
| * protected to avoid a generated bridge method when accessed. |
| */ |
| protected final EStructuralFeature proxyFeature; |
| |
| /** |
| * Constructor. |
| * |
| * @param eObject |
| * The {@link #eObject}. |
| * @param proxyFeature |
| * The {@link #proxyFeature}. |
| */ |
| ProxyEntry(EObject eObject, EStructuralFeature proxyFeature) { |
| this.eObject = eObject; |
| this.proxyFeature = proxyFeature; |
| } |
| } |
| } |