/**
* <copyright>
*
* Copyright (c) 2010-2016 Thales Global Services S.A.S.
* 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:
* Thales Global Services S.A.S. - initial API and implementation
*
* </copyright>
*/
package org.eclipse.emf.diffmerge.impl.scopes;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import org.eclipse.emf.common.notify.Adapter;
import org.eclipse.emf.common.util.TreeIterator;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.diffmerge.api.scopes.IFragmentedModelScope;
import org.eclipse.emf.diffmerge.api.scopes.IPersistentModelScope;
import org.eclipse.emf.diffmerge.util.ModelImplUtil;
import org.eclipse.emf.diffmerge.util.structures.FArrayList;
import org.eclipse.emf.diffmerge.util.structures.FOrderedSet;
import org.eclipse.emf.diffmerge.util.structures.HashBinaryRelation;
import org.eclipse.emf.diffmerge.util.structures.IBinaryRelation;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.util.ECrossReferenceAdapter;
import org.eclipse.emf.edit.domain.AdapterFactoryEditingDomain;
import org.eclipse.emf.edit.domain.EditingDomain;
import org.eclipse.emf.edit.domain.IEditingDomainProvider;
/**
* A model scope which covers the contents of a given resource plus the contents
* of other resources which are being logically included or referenced according
* to custom rules, recursively.
* This kind of scope is typically useful for loading models which are fragmented
* among multiple resources via containment references and cross-references.
* The scope is built incrementally: the additional resources are discovered dynamically
* the first time the scope is being explored using getAllContents().
* A fix point is guaranteed to be found and the exploration of the scope is guaranteed
* to be complete every time.
* This implementation assumes an initialization phase that consists in a full exploration
* of the scope. No other action on the scope or modification of the resource set is
* expected to happen during this initialization phase.
* EMF undo/redo is supported because the local state does not change after full exploration,
* unless unload() has been called.
* @author Olivier Constant
*/
public class FragmentedModelScope extends AbstractEditableModelScope
implements IFragmentedModelScope.Editable {
/** Whether the resources should be opened in read-only mode */
private final boolean _isReadOnly;
/** The optional editing domain that encompasses the scope */
protected EditingDomain _editingDomain;
/** The non-null resource set that encompasses the scope */
protected final ResourceSet _resourceSet;
/** The optional input stream for loading */
protected InputStream _loadingStream;
/** The non-null, non-empty ordered set of resources defining the scope.
* It includes _rootResources, _includedResources and _referencedResources. */
protected final List<Resource> _resources;
/** The non-null, non-empty ordered subset of the resources which are roots */
protected final List<Resource> _rootResources;
/** The non-null, potentially empty set of resources initially present
* in the resource set before the scope was loaded (content is temporary) */
protected final Set<Resource> _initiallyPresentResources;
/** The non-null, initially empty set of resources that have been loaded due
* to the exploration of the scope */
protected final Set<Resource> _loadedResources;
/** The resource inclusion relationship */
protected final IBinaryRelation.Editable<Resource, Resource> _includedResources;
/** The resource referencing relationship */
protected final IBinaryRelation.Editable<Resource, Resource> _referencedResources;
/** The possible states of the scope, ordered */
protected enum ScopeState {
/**
* The scope has just been created.
*/
INITIALIZED,
/**
* The scope has been loaded but not fully explored.
*/
LOADED,
/**
* The scope has been loaded and fully explored.
*/
FULLY_EXPLORED,
/**
* The scope has been unloaded.
*/
UNLOADED
}
/** The current state of the scope */
protected ScopeState _state;
/**
* Constructor
* @param resource_p a non-null resource that belongs to a non-null resource set
* @param readOnly_p whether the scope is in read-only mode, if applicable
* Precondition: resource_p != null && resource_p.getResourceSet() != null
*/
public FragmentedModelScope(Resource resource_p, boolean readOnly_p) {
this(resource_p.getURI(), resource_p.getResourceSet(), readOnly_p);
}
/**
* Constructor
* @param uri_p a non-null URI of the resource to load as root
* @param editingDomain_p a non-null editing domain that encompasses the scope
* @param readOnly_p whether the scope should be read-only, if supported
*/
public FragmentedModelScope(URI uri_p, EditingDomain editingDomain_p, boolean readOnly_p) {
this(Collections.singleton(uri_p), editingDomain_p, readOnly_p);
}
/**
* Constructor
* @param uri_p a non-null resource URI
* @param resourceSet_p a non-null resource set where the resources must be loaded
* @param readOnly_p whether the scope is in read-only mode, if applicable
*/
public FragmentedModelScope(URI uri_p, ResourceSet resourceSet_p, boolean readOnly_p) {
this(Collections.singleton(uri_p), resourceSet_p, readOnly_p);
}
/**
* Constructor
* @param uris_p a non-null collection of URIs of resources to load as roots
* @param editingDomain_p a non-null editing domain that encompasses the scope
* @param readOnly_p whether the scope should be read-only, if supported
*/
public FragmentedModelScope(Collection<URI> uris_p, EditingDomain editingDomain_p, boolean readOnly_p) {
this(uris_p, editingDomain_p.getResourceSet(), readOnly_p);
_editingDomain = editingDomain_p;
}
/**
* Constructor
* @param uris_p a non-null collection of URIs of resources to load as roots
* @param resourceSet_p a non-null resource set where the resources must be loaded
* @param readOnly_p whether the scope is in read-only mode, if applicable
*/
public FragmentedModelScope(Collection<URI> uris_p, ResourceSet resourceSet_p, boolean readOnly_p) {
this(resourceSet_p, readOnly_p);
for (URI uri : uris_p) {
Resource rootResource = _resourceSet.getResource(uri, false);
if (rootResource == null)
rootResource = _resourceSet.createResource(uri);
_rootResources.add(rootResource);
addNewResource(rootResource);
}
}
/**
* Common constructor
* @param resourceSet_p the non-null resource set that encompasses the scope
* @param readOnly_p whether the scope is in read-only mode, if applicable
*/
protected FragmentedModelScope(ResourceSet resourceSet_p, boolean readOnly_p) {
_state = ScopeState.INITIALIZED;
_loadingStream = null;
_isReadOnly = readOnly_p;
_resourceSet = resourceSet_p;
_resources = new ArrayList<Resource>();
_rootResources = new ArrayList<Resource>();
_includedResources = new HashBinaryRelation<Resource, Resource>();
_referencedResources = new HashBinaryRelation<Resource, Resource>();
_initiallyPresentResources = new HashSet<Resource>();
_initiallyPresentResources.addAll(_resourceSet.getResources());
_loadedResources = new HashSet<Resource>();
if (_resourceSet instanceof IEditingDomainProvider)
_editingDomain = ((IEditingDomainProvider)_resourceSet).getEditingDomain();
}
/**
* @see org.eclipse.emf.diffmerge.api.scopes.IEditableModelScope#add(org.eclipse.emf.ecore.EObject)
*/
public boolean add(EObject element_p) {
boolean result = false;
Resource defaultResource = getResourceForNewRoot(element_p);
if (defaultResource != null) {
defaultResource.getContents().add(element_p);
result = true;
}
return result;
}
/**
* @see org.eclipse.emf.diffmerge.impl.scopes.AbstractEditableModelScope#add(org.eclipse.emf.ecore.EObject, org.eclipse.emf.ecore.EReference, org.eclipse.emf.ecore.EObject)
*/
@Override
@SuppressWarnings("null") // To avoid irrelevant warning
public boolean add(EObject source_p, EReference reference_p, EObject value_p) {
Resource oldResource = value_p.eResource();
boolean wasRoot = oldResource != null && oldResource.getContents().contains(value_p);
Object formerId = getExtrinsicID(value_p);
boolean result = super.add(source_p, reference_p, value_p);
if (wasRoot && reference_p.isContainment())
oldResource.getContents().remove(value_p); // Not automatically handled
if (formerId != null)
// In case resource has changed, thus changing the extrinsic ID
setExtrinsicID(value_p, formerId);
return result;
}
/**
* Add the given resource to the set of covered resources
* @param resource_p a non-null resource which is not contained in getResources()
* Precondition: !getResources().contains(resource_p)
* Postcondition: getResources().contains(resource_p)
*/
protected void addNewResource(Resource resource_p) {
_resources.add(resource_p);
if (!_initiallyPresentResources.contains(resource_p))
_loadedResources.add(resource_p);
}
/**
* Return whether the given collection contains proxies relative to the
* given holding element whose target is already loaded
* @param collection_p a non-null collection of elements
* @param source_p a non-null element potentially holding proxies
*/
protected boolean containsUnnecessaryProxies(Collection<EObject> collection_p,
EObject source_p) {
for (EObject current : collection_p) {
if (current.eIsProxy() && current != ModelImplUtil.resolveIfLoaded(current, source_p))
return true;
}
return false;
}
/**
* Called as soon as full scope exploration has been done
*/
protected void explorationFinished() {
_state = ScopeState.FULLY_EXPLORED;
// Completion of _loadedResources: additional resources may be involved because
// of automatic proxy resolving. A consequence of this update of _loadedResources
// is that the loaded resources of a scope S1 may wrongly include those of another
// scope S2 on the same resource set (S2 loaded after S1 and explored after S1),
// which is OK as long as both scopes are unloaded together.
_loadedResources.addAll(_resourceSet.getResources());
_loadedResources.removeAll(_initiallyPresentResources);
_initiallyPresentResources.clear();
// Handling read-only on loaded resources
if (isReadOnly() && _editingDomain instanceof AdapterFactoryEditingDomain) {
AdapterFactoryEditingDomain afEditingDomain = (AdapterFactoryEditingDomain)_editingDomain;
Map<Resource, Boolean> readOnlyMap = afEditingDomain.getResourceToReadOnlyMap();
if (readOnlyMap != null) {
for (Resource loadedResource : _loadedResources) {
readOnlyMap.put(loadedResource, Boolean.TRUE);
}
}
}
}
/**
* @see org.eclipse.emf.diffmerge.impl.scopes.AbstractModelScope#get(EObject, EReference)
*/
@Override
public List<EObject> get(EObject source_p, EReference reference_p) {
// Current result, may require resolution of in-scope proxies
List<EObject> result = super.get(source_p, reference_p);
boolean requiresResolution = containsUnnecessaryProxies(result, source_p);
if (requiresResolution) // Recompute result if needed
result = get(source_p, reference_p, true);
return result;
}
/**
* @see org.eclipse.emf.diffmerge.impl.scopes.AbstractModelScope#getAllContents()
*/
@Override
public TreeIterator<EObject> getAllContents() {
return new ExpandingMultiResourceTreeIterator();
}
/**
* @see org.eclipse.emf.diffmerge.api.scopes.IModelScope#getContents()
* Result is guaranteed to be accurate only if hasBeenExplored().
*/
public List<EObject> getContents() {
List<EObject> result = new FArrayList<EObject>();
for (Resource resource : _rootResources)
result.addAll(resource.getContents());
return Collections.unmodifiableList(result);
}
/**
* Return the cross-references, for the given element, which are in the scope
* @param element_p a non-null element belonging to the scope
* @return a non-null, potentially empty collection of non-containment, non-container references
*/
protected Collection<EReference> getCrossReferencesInScope(EObject element_p) {
// Override if needed
return new ArrayList<EReference>();
}
/**
* @see org.eclipse.emf.diffmerge.impl.scopes.AbstractModelScope#getDefaultOriginator()
*/
@Override
protected Object getDefaultOriginator() {
return getHoldingResource();
}
/**
* @see IPersistentModelScope#getExtrinsicID(EObject)
*/
@Override
public Object getExtrinsicID(EObject element_p) {
// Increases visibility
return super.getExtrinsicID(element_p);
}
/**
* @see org.eclipse.emf.diffmerge.api.scopes.IPersistentModelScope#getHoldingResource()
*/
public Resource getHoldingResource() {
return _rootResources.isEmpty()? null: _rootResources.get(0);
}
/**
* @see org.eclipse.emf.diffmerge.api.scopes.IFragmentedModelScope#getIncludedResources(org.eclipse.emf.ecore.resource.Resource)
* Result is guaranteed to be accurate only if hasBeenExplored().
*/
public List<Resource> getIncludedResources(Resource resource_p) {
return _includedResources.get(resource_p);
}
/**
* Return load options for loading the given resource
* @param resource_p a non-null resource
* @return a non-null, potentially empty, modifiable option map
*/
protected Map<Object, Object> getLoadOptions(Resource resource_p) {
// Override if needed
return new HashMap<Object, Object>();
}
/**
* @see org.eclipse.emf.diffmerge.api.scopes.IFragmentedModelScope#getReferencedResources(org.eclipse.emf.ecore.resource.Resource)
* Result is guaranteed to be accurate only if hasBeenExplored().
*/
public List<Resource> getReferencedResources(Resource resource_p) {
return _referencedResources.get(resource_p);
}
/**
* @see org.eclipse.emf.diffmerge.api.scopes.IFragmentedModelScope#getRootResources()
* Result is guaranteed to be accurate only if hasBeenExplored().
*/
public List<Resource> getRootResources() {
return Collections.unmodifiableList(_rootResources);
}
/**
* Return a list of resources that must be integrated to the scope, given a
* contextual element. The resource of the element needs not be included.
* @param element_p a non-null element belonging to the scope
* @return a non-null, potentially empty list
*/
protected List<Resource> getRelevantReferencedResources(EObject element_p) {
List<Resource> result = new FOrderedSet<Resource>();
Collection<EReference> refsInScope = getCrossReferencesInScope(element_p);
for (EReference ref : refsInScope) {
if (!ref.isContainment() && !ref.isContainer() && element_p.eIsSet(ref)) {
List<EObject> values = get(element_p, ref, true);
for (EObject value : values) {
Resource valueResource = value.eResource();
if (valueResource != null)
result.add(valueResource);
}
}
}
return result;
}
/**
* Return the Resource to use for adding the given root element
* @param newRoot_p a non-null element to be integrated to the scope as a root
* @return a Resource, or null if none was found
*/
protected Resource getResourceForNewRoot(EObject newRoot_p) {
// Return the first suitable resource
for (Resource resource : _resources) {
if (isSuitableFor(resource, newRoot_p))
return resource;
}
return null;
}
/**
* @see org.eclipse.emf.diffmerge.api.scopes.IFragmentedModelScope#getResources()
* Result is guaranteed to be accurate only if hasBeenExplored().
* @return a non-null, non-empty list of resources
*/
public List<Resource> getResources() {
return Collections.unmodifiableList(_resources);
}
/**
* Return whether this scope has been fully explored, that is, at least one complete iteration
* based on getAllContents has been performed and the contents are still available
* @see org.eclipse.emf.diffmerge.api.scopes.IFragmentedModelScope#isFullyExplored()
*/
public boolean isFullyExplored() {
return _state == ScopeState.FULLY_EXPLORED;
}
/**
* @see org.eclipse.emf.diffmerge.api.scopes.IPersistentModelScope#isLoaded()
*/
public boolean isLoaded() {
return _state != ScopeState.INITIALIZED && _state != ScopeState.UNLOADED;
}
/**
* @see org.eclipse.emf.diffmerge.impl.scopes.AbstractEditableModelScope#isReadOnly()
*/
@Override
public boolean isReadOnly() {
return _isReadOnly;
}
/**
* Return whether the given resource is suitable for storing the given element as a root
* @param resource_p a non-null resource
* @param root_p a non-null element
*/
protected boolean isSuitableFor(Resource resource_p, EObject root_p) {
// Override if needed
return true;
}
/**
* @see org.eclipse.emf.diffmerge.api.scopes.IPersistentModelScope#load()
*/
public boolean load() throws Exception {
boolean result = false;
if (_state == ScopeState.INITIALIZED || _state == ScopeState.LOADED) {
if (_loadingStream != null) {
result = loadFromStream(_loadingStream);
} else {
result = true;
for (Resource rootResource : _rootResources) {
result = result && loadResource(rootResource);
}
}
_state = ScopeState.LOADED;
}
return result;
}
/**
* Load the scope from the given input stream
* @param stream_p a non-null input stream
* @return whether the operation could be performed
*/
protected boolean loadFromStream(InputStream stream_p) throws Exception {
boolean result = false;
Resource holdingResource = getHoldingResource();
if (holdingResource != null) {
Map<?,?> options = getLoadOptions(holdingResource);
holdingResource.load(stream_p, options);
result = true;
}
return result;
}
/**
* Load the given root resource
* @param resource_p a non-null resource
* @return whether the operation could be performed
* @throws Exception an exception indicating that the operation failed in an unexpected way
*/
protected boolean loadResource(Resource resource_p) throws Exception {
Map<?,?> options = getLoadOptions(resource_p);
resource_p.load(options);
return true;
}
/**
* Get notified that the given resource is included via the containment tree
* into the other given resource. We assume that all roots of the included resource
* are reachable from the including resource as is normally the case with
* the fragmentation mechanism.
* @param including_p a non-null resource
* @param included_p a non-null resource which is not including_p
*/
protected void notifyInclusion(Resource including_p, Resource included_p) {
if (!_resources.contains(included_p))
addNewResource(included_p);
// New inclusion
_includedResources.add(including_p, included_p);
// Remove from roots and referencing relation
_rootResources.remove(included_p);
// Inclusion takes precedence over referencing
_referencedResources.remove(including_p, included_p);
}
/**
* Get notified that the given source resource references the given target resource
* @param source_p a non-null resource
* @param target_p a non-null resource which is not source_p
*/
protected void notifyReference(Resource source_p, Resource target_p) {
if (!_resources.contains(target_p)) {
addNewResource(target_p);
_rootResources.add(target_p);
_referencedResources.add(source_p, target_p);
}
}
/**
* @see org.eclipse.emf.diffmerge.api.scopes.IPersistentModelScope.Editable#save()
*/
public boolean save() throws Exception {
Map<Object, Object> options = new HashMap<Object, Object>();
options.put(Resource.OPTION_SAVE_ONLY_IF_CHANGED,
Resource.OPTION_SAVE_ONLY_IF_CHANGED_MEMORY_BUFFER);
for (Resource resource : getResources()) {
resource.save(options);
}
return true;
}
/**
* @see org.eclipse.emf.diffmerge.api.scopes.IPersistentModelScope.Editable#setExtrinsicID(EObject, Object)
*/
@Override
public boolean setExtrinsicID(EObject element_p, Object id_p) {
// Increases visibility
return super.setExtrinsicID(element_p, id_p);
}
/**
* @see org.eclipse.emf.diffmerge.api.scopes.IPersistentModelScope.Editable#setStream(java.io.InputStream)
*/
public void setStream(InputStream stream_p) {
_loadingStream = stream_p;
}
/**
* @see org.eclipse.emf.diffmerge.api.scopes.IPersistentModelScope#unload()
*/
public List<Resource> unload() {
for (Resource loadedResource : _loadedResources) {
for (Adapter adapter : new ArrayList<Adapter>(loadedResource.eAdapters())) {
if (adapter instanceof ECrossReferenceAdapter)
loadedResource.eAdapters().remove(adapter);
}
}
for (Resource loadedResource : _loadedResources) {
unloadResource(loadedResource);
}
List<Resource> result = new ArrayList<Resource>(_loadedResources);
_loadedResources.clear();
if (!result.isEmpty())
_state = ScopeState.UNLOADED;
return result;
}
/**
* Unload the given resource
* @param resource_p a non-null resource
*/
protected void unloadResource(Resource resource_p) {
try {
if (resource_p.isLoaded()) // Actually loaded, not just assumed as such
resource_p.unload();
_resourceSet.getResources().remove(resource_p);
} catch (Exception e) {
// Proceed
e.printStackTrace();
}
}
/**
* An iterator over the dynamic list of resources
*/
protected class ExpandingMultiResourceTreeIterator extends MultiResourceTreeIterator {
/** The non-null, non-empty ordered set of resources whose exploration has started */
protected final Set<Resource> _exploredResources;
/** The potentially null next element */
protected EObject _next;
/** The potentially null actual resource of the preceding element, if any */
protected Resource _currentResource;
/** Whether iteration has finished */
protected boolean _finished;
/**
* Constructor
*/
public ExpandingMultiResourceTreeIterator() {
super(new DynamicUniqueListIterator<Resource>(_resources));
_exploredResources = new LinkedHashSet<Resource>();
_next = null;
_currentResource = null;
_finished = false;
}
/**
* Update to the next resource if relevant, and return whether it was
*/
protected boolean checkNextResource() {
boolean result = false;
while ((_contentIterator == null || !_contentIterator.hasNext()) &&
_resourceIterator.hasNext()) {
result = true;
Resource nextResource = _resourceIterator.next();
if (!_exploredResources.contains(nextResource))
_contentIterator = nextResource.getAllContents();
}
return result;
}
/**
* @see org.eclipse.emf.diffmerge.impl.scopes.MultiResourceTreeIterator#hasNext()
*/
@Override
public boolean hasNext() {
update();
return _next != null;
}
/**
* @see org.eclipse.emf.diffmerge.impl.scopes.MultiResourceTreeIterator#next()
*/
@Override
public EObject next() {
if (hasNext()) {
EObject result = _next;
_currentResource = _next.eResource();
_next = null;
return result;
}
throw new NoSuchElementException();
}
/**
* @see org.eclipse.emf.diffmerge.impl.scopes.MultiResourceTreeIterator#update()
*/
@Override
protected void update() {
while (_next == null && !_finished) {
boolean resourceChangedInList = checkNextResource();
boolean firstExploration = _state != ScopeState.FULLY_EXPLORED;
if (_contentIterator == null || !_contentIterator.hasNext()) {
// Iteration finished
_finished = true;
_exploredResources.clear();
_currentResource = null;
if (firstExploration)
// First exploration finished
explorationFinished();
} else {
// Elements remaining
EObject candidate = _contentIterator.next();
boolean candidateOK = true;
Resource candidateResource = candidate.eResource();
if (candidateResource != null) {
boolean resourceAlreadyExplored = !_exploredResources.add(candidateResource);
// Since the element is in a resource, we know the resource will be explored
// because we assume all resource roots are reachable in the case of resource inclusion
boolean resourceChangedByInclusion = false;
Resource candidateContainerResource = null;
// Determine whether the current element leads to a new resource by inclusion
if (!resourceChangedInList && _currentResource != null && _currentResource != candidateResource) {
EObject candidateContainer = candidate.eContainer();
if (candidateContainer != null) {
candidateContainerResource = candidateContainer.eResource();
resourceChangedByInclusion =
candidateContainerResource != null && candidateContainerResource != candidateResource;
}
}
if (resourceChangedByInclusion && firstExploration) {
// Resource reached by inclusion: Notify (candidateContainerResource cannot be null)
notifyInclusion(candidateContainerResource, candidateResource);
}
if (resourceChangedByInclusion && resourceAlreadyExplored) {
// Resource reached by inclusion but already visited: Skip element and its subtree
_contentIterator.prune();
candidateOK = false;
}
}
if (candidateOK) {
_next = candidate;
if (firstExploration && candidateResource != null) {
for (Resource additionalResource : getRelevantReferencedResources(_next))
notifyReference(candidateResource, additionalResource);
}
}
}
}
}
}
}