blob: e79ca2e69b291c41c75753f97b7d37ed3bdc2886 [file] [log] [blame]
/*****************************************************************************
* Copyright (c) 2013, 2017 CEA LIST.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* CEA LIST - Initial API and implementation
*****************************************************************************/
package org.eclipse.papyrus.cdo.internal.core.controlmode;
import static com.google.common.collect.Iterables.concat;
import static com.google.common.collect.Iterables.filter;
import static com.google.common.collect.Iterables.transform;
import static org.eclipse.emf.ecore.util.EcoreUtil.getRootContainer;
import static org.eclipse.papyrus.cdo.internal.core.controlmode.CDOProxyManager.createPapyrusCDOURI;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.emf.cdo.CDOObject;
import org.eclipse.emf.cdo.CDOState;
import org.eclipse.emf.cdo.common.id.CDOID;
import org.eclipse.emf.cdo.common.id.CDOIDUtil;
import org.eclipse.emf.cdo.eresource.EresourcePackage;
import org.eclipse.emf.cdo.util.CDOUtil;
import org.eclipse.emf.cdo.view.CDOView;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.InternalEObject;
import org.eclipse.emf.ecore.InternalEObject.EStore;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.emf.ecore.util.InternalEList;
import org.eclipse.emf.spi.cdo.CDOStore;
import org.eclipse.emf.spi.cdo.InternalCDOView;
import org.eclipse.gmf.runtime.common.core.command.CommandResult;
import org.eclipse.gmf.runtime.common.core.command.ICommand;
import org.eclipse.gmf.runtime.common.core.command.IdentityCommand;
import org.eclipse.papyrus.cdo.internal.core.Activator;
import org.eclipse.papyrus.cdo.internal.core.CDOUtils;
import org.eclipse.papyrus.cdo.internal.core.l10n.Messages;
import org.eclipse.papyrus.infra.services.controlmode.ControlModeRequest;
import org.eclipse.papyrus.infra.services.controlmode.commands.AbstractControlCommand;
import org.eclipse.papyrus.infra.services.controlmode.participants.IControlCommandParticipant;
import org.eclipse.papyrus.infra.services.controlmode.participants.IControlModeParticipant;
import org.eclipse.papyrus.infra.services.controlmode.participants.IUncontrolCommandParticipant;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
/**
* A {@link IControlModeParticipant} for CDO resources that handles replacing references to controlled
* elements with proxies that, even in a CDO view, must be resolved by the usual EMF mechanism.
*/
public class CDOControlModeParticipant implements IControlCommandParticipant, IUncontrolCommandParticipant {
private static final Set<CDOState> TEMPORARY_ID_STATES = EnumSet.of(CDOState.TRANSIENT, CDOState.NEW);
private List<EObject> objectsToClearResource;
public CDOControlModeParticipant() {
super();
}
@Override
public String getID() {
return CDOControlModeParticipant.class.getName();
}
@Override
public int getPriority() {
return 255;
}
@Override
public boolean provideControlCommand(ControlModeRequest request) {
return isCDOResource(request);
}
private boolean isCDOResource(ControlModeRequest request) {
return CDOUtils.isCDOURI(request.getSourceURI());
}
@Override
public boolean provideUnControlCommand(ControlModeRequest request) {
return isCDOResource(request);
}
@Override
public ICommand getPreControlCommand(ControlModeRequest request) {
return IdentityCommand.INSTANCE;
}
@Override
public ICommand getPostControlCommand(ControlModeRequest request) {
return new AbstractCDOControlCommand(request) {
@Override
protected void buildUpdates(ControlModeRequest request, IUpdate.Collector updates) {
collectProxyCrossReferenceUpdates(updates, request.getModelSet(), request.getNewURI());
}
};
}
/**
* Obtains an {@linkplain IUpdate update} operation that converts external cross-references to the elements
* in a logical model unit indicated by the given representative {@code resource} to proxies.
*
* @param resource
* a resource that is a component of a logical model unit
*
* @return an operation that, when {@linkplain IUpdate#apply() applied}, will convert incoming cross-references to proxies
*/
public IUpdate getProxyCrossReferencesUpdate(Resource resource) {
IUpdate.Compound result = new CompoundUpdate();
collectProxyCrossReferenceUpdates(result, resource.getResourceSet(), resource.getURI());
return result;
}
private void collectProxyCrossReferenceUpdates(IUpdate.Collector updates, ResourceSet resourceSet, URI unitURI) {
for (final EObject object : getAllPersistentSubunitContents(resourceSet, unitURI)) {
// replace references to the element by a proxy
CDOID proxy = null;
for (EStructuralFeature.Setting next : getExternalCrossReferences(object)) {
if (proxy == null) {
proxy = CDOIDUtil.createExternal(CDOProxyManager.createPapyrusCDOURI(object));
}
updates.add(new ControlUpdate(next, object, proxy));
}
}
}
public IUpdate getProxyCrossReferencesUpdate(final EObject owner, final EReference crossReference) {
IUpdate result = IUpdate.EMPTY;
final CDOStore[] store = { null };
for (ListIterator<? extends EObject> xrefs = CDOUtils.iterator(owner, crossReference, false); xrefs.hasNext();) {
final int index = xrefs.nextIndex();
final EObject referent = xrefs.next();
if (!referent.eIsProxy() && !inSameUnit(owner, referent) && inSameModel(owner, referent)) {
if (store[0] == null) {
store[0] = ((InternalCDOView) CDOUtils.getCDOObject(owner).cdoView()).getStore();
}
result = result.chain(new OneWayUpdate() {
@Override
public void apply() {
store[0].set((InternalEObject) owner, crossReference, index, CDOIDUtil.createExternal(createPapyrusCDOURI(referent)));
}
});
}
}
return result;
}
@Override
public ICommand getPreUncontrolCommand(ControlModeRequest request) {
return new AbstractCDOControlCommand(request) {
@Override
protected CommandResult doExecuteWithResult(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
objectsToClearResource = Lists.newArrayList();
return super.doExecuteWithResult(monitor, info);
}
@Override
protected void buildUpdates(ControlModeRequest request, IUpdate.Collector updates) {
final URI sourceURI = getRequest().getSourceURI();
IUpdate resolveProxies = IUpdate.EMPTY;
// update references from other units to the unit being re-integrated
for (final EObject object : getAllPersistentSubunitContents(getRequest().getModelSet(), sourceURI)) {
// replace proxy references to the element by the element or an updated proxy
// which will be located in the destination resource
CDOID sourceProxy = null;
URI destinationResourceURI = null;
CDOID proxy = null;
// internal cross-references within the unit won't be affected, as they are all moving
for (final EStructuralFeature.Setting next : getExternalCrossReferences(object)) {
if (next.getEObject().eIsProxy()) {
// the cross-reference will be from a proxy only when it is a proxy owned by the cross-referenced object,
// by virtue of an opposite reference. In this case, it's only interesting if the proxy targets the
// parent (re-integrated-into) resource. So, resolve this proxy because we need to update the
// reference, below
resolveProxies = resolveProxies.chain(new OneWayUpdate() {
@Override
public void apply() {
resolve(object, (EReference) next.getEStructuralFeature(), next.getEObject());
}
});
} else {
if (sourceProxy == null) {
// create the source proxy
sourceProxy = CDOIDUtil.createExternal(CDOProxyManager.createPapyrusCDOURI(object));
// and the destination proxy
destinationResourceURI = request.getTargetResource(object.eResource().getURI().fileExtension()).getURI();
String proxyURI = CDOProxyManager.createPapyrusCDOURI(destinationResourceURI, object);
proxy = CDOIDUtil.createExternal(proxyURI);
}
updates.add(new UncontrolUpdate(next, object, sourceProxy, destinationResourceURI, proxy));
}
}
// upon removal from their resources, root elements will nonetheless retain a
// reference to the resource that formerly contained them. We need to force
// clearing of this reference in the CDO store
if (((InternalEObject) object).eDirectResource() != null) {
objectsToClearResource.add(object);
}
}
// resolve proxies as per above, after completing the loop, to avoid concurrent modifications
resolveProxies.apply();
// update references from the unit being re-integrated to the unit it is re-integrating into
for (final EObject object : getAllPersistentSubunitContents(getRequest().getModelSet(), getRequest().getNewURI())) {
// replace proxy references to the element by the element or an updated proxy
// which will be located in the destination resource
CDOID targetProxy = null;
for (EStructuralFeature.Setting next : getExternalCrossReferences(object)) {
if (inUnit(next.getEObject(), sourceURI)) {
// replace proxy references from the sub-unit into the parent with a direct reference, as this will
// no longer be a cross-unit reference
if (targetProxy == null) {
targetProxy = CDOIDUtil.createExternal(CDOProxyManager.createPapyrusCDOURI(object));
}
updates.add(new UncontrolUpdate(next, object, targetProxy));
}
}
}
}
};
}
void resolve(EObject object, EReference reference, EObject proxy) {
EReference opposite = reference.getEOpposite();
if (opposite != null) {
if (opposite.isMany()) {
InternalEList<?> list = (InternalEList<?>) object.eGet(opposite, false);
int index = list.basicIndexOf(proxy);
if (index >= 0) {
list.get(index); // resolve just this index
}
} else {
object.eGet(opposite, true); // resolve the scalar reference
}
}
}
@Override
public ICommand getPostUncontrolCommand(ControlModeRequest request) {
return new AbstractCDOControlCommand(request) {
@Override
protected CommandResult doExecuteWithResult(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
CommandResult result = super.doExecuteWithResult(monitor, info);
objectsToClearResource = null;
return result;
}
@Override
protected void buildUpdates(ControlModeRequest request, IUpdate.Collector updates) {
for (EObject next : objectsToClearResource) {
// some former resource roots may not be reattached (e.g., SashWindowsMgr)
if (CDOUtil.getCDOObject(next).cdoState() != CDOState.TRANSIENT) {
updates.add(new ClearResourceUpdate(next));
}
}
}
};
}
/**
* Get all cross-references to an {@code object} from outside of its own controlled unit <em>within</em> the same model (cross-model references do
* not count).
*
* @param object
* an object being controlled
* @return references to it from outside of its (new) controlled unit
*/
Iterable<EStructuralFeature.Setting> getExternalCrossReferences(final EObject object) {
return Iterables.filter(CDOUtils.crossReference(object), new Predicate<EStructuralFeature.Setting>() {
@Override
public boolean apply(EStructuralFeature.Setting input) {
boolean result = false;
EStructuralFeature ref = input.getEStructuralFeature();
if ((ref != EresourcePackage.Literals.CDO_RESOURCE__CONTENTS) && ref.isChangeable() && !ref.isDerived()) {
result = !inSameUnit(input.getEObject(), object) && inSameModel(input.getEObject(), object);
}
return result;
}
});
}
static boolean isPersistentObject(EObject object) {
boolean result = !object.eIsProxy();
if (result) {
CDOObject cdo = CDOUtils.getCDOObject(object);
result = (cdo != null) && !TEMPORARY_ID_STATES.contains(cdo.cdoState());
}
return result;
}
private static boolean inSameModel(EObject object, EObject other) {
// is the one object in the other's logical Papyrus model?
URI model1 = getResourceURI(getRootContainer(object));
URI model2 = getResourceURI(getRootContainer(other));
return (model1 != null) && (model2 != null) && model1.trimFileExtension().equals(model2.trimFileExtension());
}
private static URI getResourceURI(EObject object) {
URI result;
if (object.eIsProxy()) {
result = ((InternalEObject) object).eProxyURI().trimFragment();
} else {
Resource res = object.eResource();
result = (res == null) ? null : res.getURI();
}
return result;
}
private static boolean inSameUnit(EObject object, EObject other) {
// is the one object in the other's unit?
URI uri = getResourceURI(other);
return inUnit(object, uri);
}
private static boolean inUnit(EObject object, URI unit) {
boolean result = false;
if (unit != null) {
// get the extension-less model URIs
URI uri = getResourceURI(object);
if (uri != null) {
uri = uri.trimFileExtension();
URI otherURI = unit.trimFileExtension();
result = uri.equals(otherURI);
}
}
return result;
}
/**
* Iterates all of the proper contents of the resources comprising the logical model unit
* indicated by the representative {@code unitURI}.
*
* @param rset
* a resource set in which to load and/or iterate the resource contents
* @param unitURI
* the URI of one of the resources in the logical model unit, which therefore is representative of the unit
*
* @return an iterator over the entire logical model unit's proper contents
*/
static Iterable<EObject> getAllPersistentSubunitContents(ResourceSet rset, URI unitURI) {
final URI base = unitURI.trimFileExtension();
Iterable<Resource> resources = filter(rset.getResources(), new Predicate<Resource>() {
@Override
public boolean apply(Resource input) {
return input.getURI().trimFileExtension().equals(base);
}
});
Iterable<EObject> result = concat(transform(resources, new Function<Resource, Iterable<EObject>>() {
@Override
public Iterable<EObject> apply(final Resource input) {
return new Iterable<EObject>() {
@Override
public Iterator<EObject> iterator() {
return EcoreUtil.getAllProperContents(input, false);
}
};
}
}));
result = filter(result, new Predicate<EObject>() {
@Override
public boolean apply(EObject input) {
return isPersistentObject(input);
}
});
return result;
}
//
// Nested types
//
public static interface IUpdate {
/**
* An update that does nothing. It is {@linkplain IUpdate#isEmpty() empty}.
*/
IUpdate EMPTY = new IUpdate() {
@Override
public boolean isEmpty() {
return true;
}
@Override
public void apply() {
// pass
}
@Override
public void revert() {
// revert
}
@Override
public IUpdate chain(IUpdate update) {
return (update == null) ? this : update;
}
};
/**
* Queries whether I have nothing to do on {@link #apply() apply}.
*
* @return whether I am empty
*/
boolean isEmpty();
void apply();
void revert();
IUpdate chain(IUpdate update);
interface Collector {
void add(IUpdate update);
}
interface Compound extends IUpdate, Collector {
// all methods inherited
}
}
static final class CompoundUpdate implements IUpdate.Compound {
private final List<IUpdate> updates = Lists.newArrayList();
@Override
public void add(IUpdate update) {
updates.add(update);
}
@Override
public Compound chain(IUpdate update) {
if ((update != null) && !update.isEmpty()) {
add(update);
}
return this;
}
@Override
public boolean isEmpty() {
return updates.isEmpty();
}
@Override
public void apply() {
for (IUpdate next : updates) {
next.apply();
}
}
@Override
public void revert() {
for (ListIterator<IUpdate> iter = updates.listIterator(updates.size()); iter.hasPrevious();) {
iter.previous().revert();
}
}
static IUpdate compose(IUpdate first, IUpdate second) {
IUpdate result;
if ((second == null) || second.isEmpty()) {
result = (first == null) ? IUpdate.EMPTY : first;
} else if ((first == null) || first.isEmpty()) {
// we already know it's not null or empty
result = second;
} else {
IUpdate.Compound compound = new CompoundUpdate();
compound.add(first);
compound.add(second);
result = compound;
}
return result;
}
}
private static abstract class OneWayUpdate implements IUpdate {
@Override
public void revert() {
throw new UnsupportedOperationException("OneWayUpdate cannot be reverted"); //$NON-NLS-1$
}
@Override
public IUpdate chain(IUpdate update) {
return CompoundUpdate.compose(this, update);
}
@Override
public boolean isEmpty() {
return false;
}
}
private static abstract class Update implements IUpdate {
final EStructuralFeature.Setting setting;
final CDOStore store;
Update(EStructuralFeature.Setting setting) {
this.setting = setting;
InternalEObject owner = (InternalEObject) setting.getEObject();
CDOObject cdoOwner = CDOUtil.getCDOObject(owner);
InternalCDOView view = (InternalCDOView) cdoOwner.cdoView();
store = view.getStore();
}
Update(EObject object) {
this.setting = null;
CDOObject cdo = CDOUtil.getCDOObject(object);
CDOView view = cdo.cdoView();
this.store = (view instanceof InternalCDOView) ? ((InternalCDOView) view).getStore() : null;
}
@Override
public IUpdate chain(IUpdate update) {
return CompoundUpdate.compose(this, update);
}
@Override
public boolean isEmpty() {
// a leaf update is, by definition, not empty
return false;
}
}
private static final class ControlUpdate extends Update {
final EObject originalObject;
final CDOID proxy;
final int index;
ControlUpdate(EStructuralFeature.Setting setting, EObject originalObject, CDOID proxy) {
super(setting);
this.originalObject = originalObject;
this.proxy = proxy;
EStructuralFeature feature = setting.getEStructuralFeature();
InternalEObject owner = (InternalEObject) setting.getEObject();
if (!feature.isMany()) {
index = EStore.NO_INDEX;
} else {
// don't go directly to the store because it may have proxies.
// Use the resolved view in the EObject, instead
index = ((EList<?>) owner.eGet(feature)).indexOf(originalObject);
if (index < 0) {
Activator.log.error("Setting does not include the object being replaced by a proxy.", null); //$NON-NLS-1$
}
}
}
@Override
public void apply() {
EStructuralFeature feature = setting.getEStructuralFeature();
if ((index >= 0) || !feature.isMany()) {
InternalEObject owner = (InternalEObject) setting.getEObject();
store.set(owner, feature, index, proxy);
}
}
@Override
public void revert() {
EStructuralFeature feature = setting.getEStructuralFeature();
if (index >= 0 || !feature.isMany()) {
InternalEObject owner = (InternalEObject) setting.getEObject();
store.set(owner, feature, index, CDOUtils.getCDOID(originalObject));
}
}
}
private static final class UncontrolUpdate extends Update {
final EObject originalObject;
final CDOID originalProxy;
final URI destinationURI;
final CDOID destinationProxy;
final int index;
UncontrolUpdate(EStructuralFeature.Setting setting, EObject originalObject, CDOID originalProxy, URI destinationURI, CDOID destinationProxy) {
super(setting);
this.originalObject = originalObject;
this.originalProxy = originalProxy;
this.destinationURI = destinationURI;
this.destinationProxy = destinationProxy;
EStructuralFeature feature = setting.getEStructuralFeature();
InternalEObject owner = (InternalEObject) setting.getEObject();
if (!feature.isMany()) {
index = EStore.NO_INDEX;
} else {
// don't go directly to the store because it may have proxies.
// Use the resolved view in the EObject, instead
index = ((EList<?>) owner.eGet(feature)).indexOf(originalObject);
if (index < 0) {
Activator.log.error("Setting does not include the object being replaced by a proxy.", null); //$NON-NLS-1$
}
}
}
UncontrolUpdate(EStructuralFeature.Setting setting, EObject originalObject, CDOID originalProxy) {
this(setting, originalObject, originalProxy, null, null);
}
@Override
public void apply() {
EStructuralFeature feature = setting.getEStructuralFeature();
if ((index >= 0) || !feature.isMany()) {
InternalEObject owner = (InternalEObject) setting.getEObject();
if ((destinationURI == null) || inUnit(owner, destinationURI)) {
// direct reference
store.set(owner, feature, index, CDOUtils.getCDOID(originalObject));
} else {
// proxy reference for cross-unit
store.set(owner, feature, index, destinationProxy);
}
}
}
@Override
public void revert() {
EStructuralFeature feature = setting.getEStructuralFeature();
if (index >= 0 || !feature.isMany()) {
InternalEObject owner = (InternalEObject) setting.getEObject();
// on reversion, we are processing only references that were external
// to the unit that was to be re-integrated, so necessarily all
// references must be set to the original proxies
store.set(owner, feature, index, originalProxy);
}
}
}
private static final class ClearResourceUpdate extends Update {
private CDOObject object;
ClearResourceUpdate(EObject object) {
super(object);
this.object = CDOUtil.getCDOObject(object);
}
@Override
public void apply() {
InternalEObject object = (InternalEObject) CDOUtil.getEObject(this.object);
store.setContainer(object, null, object.eInternalContainer(), object.eContainerFeatureID());
}
@Override
public void revert() {
// there is no need to revert clearing the resource reference; it will be
// reestablished naturally by undo of the base command
}
}
private static abstract class AbstractCDOControlCommand extends AbstractControlCommand implements IUpdate.Compound {
AbstractCDOControlCommand(ControlModeRequest request) {
super(Messages.CDOControlModeParticipant_commandLabel, Collections.emptyList(), request);
}
private List<IUpdate> updates;
@Override
protected CommandResult doExecuteWithResult(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
final ImmutableListCollector collector = new ImmutableListCollector();
buildUpdates(getRequest(), collector);
this.updates = collector.close();
apply();
return CommandResult.newOKCommandResult();
}
protected abstract void buildUpdates(ControlModeRequest request, IUpdate.Collector updates);
@Override
protected IStatus doUndo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
IStatus result = super.doUndo(monitor, info);
if (result.isOK()) {
// setting proxies in the way we did is not recorded by EMF, so we have to undo it ourselves
revert();
}
return result;
}
@Override
protected IStatus doRedo(IProgressMonitor monitor, IAdaptable info) throws ExecutionException {
IStatus result = super.doRedo(monitor, info);
if (result.isOK()) {
// setting proxies in the way we did is not recorded by EMF, so we have to redo it ourselves
apply();
}
return result;
}
@Override
public boolean isEmpty() {
return (updates == null) || updates.isEmpty();
}
@Override
public void apply() {
for (IUpdate next : updates) {
next.apply();
}
}
@Override
public void revert() {
for (ListIterator<IUpdate> iter = updates.listIterator(updates.size()); iter.hasPrevious();) {
iter.previous().revert();
}
}
@Override
public void add(IUpdate update) {
throw new UnsupportedOperationException("AbtsractCDOControlCommand is not externally modifiable"); //$NON-NLS-1$
}
@Override
public Compound chain(IUpdate update) {
throw new UnsupportedOperationException("AbtsractCDOControlCommand is not externally modifiable"); //$NON-NLS-1$
}
};
private static final class ImmutableListCollector implements IUpdate.Collector {
private final ImmutableList.Builder<IUpdate> builder = ImmutableList.builder();
@Override
public void add(IUpdate update) {
builder.add(update);
}
List<IUpdate> close() {
return builder.build();
}
}
}