blob: 7eb8c611e5023b26115fb6b505dcbc09cf9e6e0a [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2008-2014 Chair for Applied Software Engineering,
* Technische Universitaet Muenchen.
* 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:
* Maximilian Koegel, Edgar Mueller - initial API and implementation
******************************************************************************/
package org.eclipse.emf.emfstore.internal.client.model.impl;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.emf.common.command.Command;
import org.eclipse.emf.common.notify.Notification;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EClassifier;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.EStructuralFeature.Setting;
import org.eclipse.emf.ecore.InternalEObject;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.emf.ecore.util.EcoreUtil.Copier;
import org.eclipse.emf.emfstore.client.ESLocalProject;
import org.eclipse.emf.emfstore.client.changetracking.ESCommandObserver;
import org.eclipse.emf.emfstore.client.observer.ESCommitObserver;
import org.eclipse.emf.emfstore.client.observer.ESPostCreationObserver;
import org.eclipse.emf.emfstore.client.observer.ESShareObserver;
import org.eclipse.emf.emfstore.client.observer.ESUpdateObserver;
import org.eclipse.emf.emfstore.internal.client.model.CompositeOperationHandle;
import org.eclipse.emf.emfstore.internal.client.model.ESWorkspaceProviderImpl;
import org.eclipse.emf.emfstore.internal.client.model.ProjectSpace;
import org.eclipse.emf.emfstore.internal.client.model.changeTracking.NotificationToOperationConverter;
import org.eclipse.emf.emfstore.internal.client.model.changeTracking.notification.filter.FilterStack;
import org.eclipse.emf.emfstore.internal.client.model.changeTracking.notification.recording.NotificationRecorder;
import org.eclipse.emf.emfstore.internal.client.model.exceptions.MissingCommandException;
import org.eclipse.emf.emfstore.internal.client.model.impl.api.ESLocalProjectImpl;
import org.eclipse.emf.emfstore.internal.client.model.util.WorkspaceUtil;
import org.eclipse.emf.emfstore.internal.common.model.IdEObjectCollection;
import org.eclipse.emf.emfstore.internal.common.model.ModelElementId;
import org.eclipse.emf.emfstore.internal.common.model.Project;
import org.eclipse.emf.emfstore.internal.common.model.impl.IdEObjectCollectionImpl;
import org.eclipse.emf.emfstore.internal.common.model.util.IdEObjectCollectionChangeObserver;
import org.eclipse.emf.emfstore.internal.common.model.util.ModelUtil;
import org.eclipse.emf.emfstore.internal.common.model.util.NotificationInfo;
import org.eclipse.emf.emfstore.internal.common.model.util.SettingWithReferencedElement;
import org.eclipse.emf.emfstore.internal.server.model.versioning.operations.AbstractOperation;
import org.eclipse.emf.emfstore.internal.server.model.versioning.operations.CompositeOperation;
import org.eclipse.emf.emfstore.internal.server.model.versioning.operations.CreateDeleteOperation;
import org.eclipse.emf.emfstore.internal.server.model.versioning.operations.MultiReferenceOperation;
import org.eclipse.emf.emfstore.internal.server.model.versioning.operations.OperationsFactory;
import org.eclipse.emf.emfstore.internal.server.model.versioning.operations.ReferenceOperation;
import org.eclipse.emf.emfstore.internal.server.model.versioning.operations.SingleReferenceOperation;
import org.eclipse.emf.emfstore.internal.server.model.versioning.operations.impl.CreateDeleteOperationImpl;
import org.eclipse.emf.emfstore.internal.server.model.versioning.operations.semantic.SemanticCompositeOperation;
import org.eclipse.emf.emfstore.server.model.ESChangePackage;
import org.eclipse.emf.emfstore.server.model.versionspec.ESPrimaryVersionSpec;
/**
* Tracks changes on any given {@link IdEObjectCollection}.
*
* @author koegel
* @author emueller
*/
public class OperationRecorder implements ESCommandObserver, ESCommitObserver, ESUpdateObserver, ESShareObserver,
IdEObjectCollectionChangeObserver {
/**
* Name of unknown creator.
*/
public static final String UNKOWN_CREATOR = Messages.OperationRecorder_Unknown;
private int currentOperationListSize;
private List<AbstractOperation> operations;
private final List<OperationRecorderListener> observers;
private final RemovedElementsCache removedElementsCache;
private final NotificationToOperationConverter converter;
private NotificationRecorder notificationRecorder;
private CompositeOperation compositeOperation;
private final ProjectSpaceBase projectSpace;
private final IdEObjectCollectionImpl collection;
private boolean isRecording;
private boolean commandIsRunning;
private final OperationRecorderConfig config;
/**
* Constructor.
*
* @param projectSpace
* the {@link ProjectSpaceBase} the recorder should be attached to
*/
// TODO: provide ext. point for rollBackInCaseOfCommandFailure
public OperationRecorder(ProjectSpaceBase projectSpace) {
this.projectSpace = projectSpace;
collection = (IdEObjectCollectionImpl) projectSpace.getProject();
operations = new ArrayList<AbstractOperation>();
observers = new ArrayList<OperationRecorderListener>();
removedElementsCache = new RemovedElementsCache(collection);
config = new OperationRecorderConfig();
converter = new NotificationToOperationConverter(collection);
}
/**
* Clears the operations list.
*
* @return the list of cleared operations
*/
public List<AbstractOperation> clearOperations() {
final List<AbstractOperation> ops = new ArrayList<AbstractOperation>(operations);
operations.clear();
return ops;
}
/**
* Returns the configuration options for the operation recorder.
*
* @return the operation recorder configuration options
*/
public OperationRecorderConfig getConfig() {
return config;
}
/**
* Returns the collection the operation recorder is operation on.
*
* @return the collection the operation recorder is operation on
*/
public IdEObjectCollection getCollection() {
return collection;
}
/**
*
* {@inheritDoc}
*
* @see org.eclipse.emf.emfstore.internal.common.model.util.IdEObjectCollectionChangeObserver#modelElementAdded(org.eclipse.emf.emfstore.internal.common.model.IdEObjectCollection,
* org.eclipse.emf.ecore.EObject)
*/
public void modelElementAdded(IdEObjectCollection project, EObject modelElement) {
// if element was just pasted from clipboard then do nothing
// if (this.getModelElementsFromClipboard().contains(modelElement)) {
// return;
// }
if (!isRecording) {
return;
}
checkCommandConstraints(modelElement);
// notify Post Creation Listeners with change tracking switched off since only attribute changes are allowd
stopChangeRecording();
ESWorkspaceProviderImpl.getObserverBus().notify(ESPostCreationObserver.class).onCreation(modelElement);
startChangeRecording();
final Set<EObject> allModelElements = new LinkedHashSet<EObject>();
allModelElements.add(modelElement);
allModelElements.addAll(ModelUtil.getAllContainedModelElements(modelElement, false));
// collect out-going cross-reference for containment tree of modelElement
final List<SettingWithReferencedElement> crossReferences = ModelUtil.collectOutgoingCrossReferences(collection,
allModelElements);
// collect in-going cross-reference for containment tree of modelElement
final List<SettingWithReferencedElement> ingoingCrossReferences = collectIngoingCrossReferences(collection,
allModelElements);
crossReferences.addAll(ingoingCrossReferences);
// clean up already existing references when collection already contained the object
final List<SettingWithReferencedElement> savedSettings = removedElementsCache
.getRemovedRootElementToReferenceSetting(modelElement);
if (savedSettings != null) {
final List<SettingWithReferencedElement> toRemove = new ArrayList<SettingWithReferencedElement>();
for (final SettingWithReferencedElement setting : savedSettings) {
for (final SettingWithReferencedElement newSetting : crossReferences) {
if (setting.getSetting().getEStructuralFeature()
.equals(newSetting.getSetting().getEStructuralFeature())
&& setting.getReferencedElement().equals(newSetting.getReferencedElement())) {
toRemove.add(newSetting);
}
}
}
crossReferences.removeAll(toRemove);
}
// collect recorded operations and add to create operation
final List<ReferenceOperation> recordedOperations = generateCrossReferenceOperations(crossReferences);
List<AbstractOperation> resultingOperations;
// check if create element has been deleted during running command, if so do not record a create operation
if (commandIsRunning && removedElementsCache.contains(modelElement)) {
resultingOperations = new ArrayList<AbstractOperation>(recordedOperations);
} else {
final CreateDeleteOperation createDeleteOperation = createCreateDeleteOperation(modelElement, false);
createDeleteOperation.getSubOperations().addAll(recordedOperations);
resultingOperations = new ArrayList<AbstractOperation>();
resultingOperations.add(createDeleteOperation);
}
if (compositeOperation != null) {
compositeOperation.getSubOperations().addAll(resultingOperations);
} else {
bufferOrRecordOperations(resultingOperations);
}
}
private void checkCommandConstraints(EObject modelElement) {
if (!commandIsRunning && config.isForceCommands()) {
WorkspaceUtil.handleException(Messages.OperationRecorder_ElementChangedWithoutCommand_0,
new MissingCommandException(
MessageFormat.format(Messages.OperationRecorder_ElementChangedWithoutCommand_1, modelElement)));
}
}
private List<SettingWithReferencedElement> collectIngoingCrossReferences(IdEObjectCollection collection,
Set<EObject> allModelElements) {
final List<SettingWithReferencedElement> settings = new ArrayList<SettingWithReferencedElement>();
for (final EObject modelElement : allModelElements) {
final Collection<Setting> inverseReferences = projectSpace.findInverseCrossReferences(modelElement);
for (final Setting setting : inverseReferences) {
if (!ModelUtil.shouldBeCollected(collection, allModelElements, setting.getEObject())) {
continue;
}
final EReference reference = (EReference) setting.getEStructuralFeature();
final EClassifier eType = reference.getEType();
if (reference.isContainer() || reference.isContainment() || !reference.isChangeable()
|| !(eType instanceof EClass)) {
continue;
}
final SettingWithReferencedElement settingWithReferencedElement = new SettingWithReferencedElement(
setting,
modelElement);
settings.add(settingWithReferencedElement);
}
}
return settings;
}
private List<ReferenceOperation> generateCrossReferenceOperations(
Collection<SettingWithReferencedElement> crossReferences) {
final List<ReferenceOperation> result = new ArrayList<ReferenceOperation>();
for (final SettingWithReferencedElement setting : crossReferences) {
final EObject referencedElement = setting.getReferencedElement();
// fetch ID of referenced element
ModelElementId newModelElementId = collection.getModelElementId(referencedElement);
if (newModelElementId == null) {
newModelElementId = collection.getDeletedModelElementId(referencedElement);
}
final EObject eObject = setting.getSetting().getEObject();
final EReference reference = (EReference) setting.getSetting().getEStructuralFeature();
if (setting.getSetting().getEStructuralFeature().isMany()) {
final int position = ((List<?>) eObject.eGet(reference)).indexOf(referencedElement);
final MultiReferenceOperation multiRefOp = NotificationToOperationConverter
.createMultiReferenceOperation(
collection, eObject, reference, Arrays.asList(referencedElement), true, position);
result.add(multiRefOp);
} else {
final SingleReferenceOperation singleRefOp = NotificationToOperationConverter
.createSingleReferenceOperation(
collection, null, newModelElementId, reference, eObject);
result.add(singleRefOp);
}
}
return result;
}
private void operationsRecorded(List<AbstractOperation> operations) {
if (operations.size() == 0) {
return;
}
for (final OperationRecorderListener observer : observers) {
observer.operationsRecorded(operations);
}
}
/**
* Adds an operation recorder observer.
*
* @param observer
* the observer to be added
*/
public void addOperationRecorderListener(OperationRecorderListener observer) {
observers.add(observer);
}
/**
* Removes an operation recorder observer.
*
* @param observer
* the observer to be removed
*/
public void removeOperationRecorderListener(OperationRecorderListener observer) {
observers.remove(observer);
}
/**
* Starts change recording on this workspace, resumes previous recordings if
* there are any.
*
*/
public void startChangeRecording() {
if (notificationRecorder == null) {
notificationRecorder = new NotificationRecorder();
}
isRecording = true;
}
/**
* Stops current recording of changes and adds recorded changes to this
* project spaces changes.
*
*/
public void stopChangeRecording() {
isRecording = false;
}
private List<AbstractOperation> recordingFinished() {
// create operations from "valid" notifications, log invalid ones,
// accumulate the ops
final List<AbstractOperation> ops = new LinkedList<AbstractOperation>();
final List<NotificationInfo> rec = notificationRecorder.getRecording().asMutableList();
for (final NotificationInfo n : rec) {
if (!n.isValid()) {
WorkspaceUtil.log(Messages.OperationRecorder_InvalidNotificationMessage + n.getValidationMessage(),
null, 0);
continue;
}
final AbstractOperation op = converter.convert(n);
if (op != null) {
ops.add(op);
} else {
// we should never get here, this would indicate a
// consistency error,
// n.isValid() should have been false
WorkspaceUtil.log(Messages.OperationRecorder_InvalidNotificationClassification_0
+ Messages.OperationRecorder_InvalidNotificationClassification_1 + n.toString(), null, 0);
continue;
}
}
return ops;
}
/**
* Returns the notification recorder of the project space.
*
* @return the notification recorder
*/
public NotificationRecorder getNotificationRecorder() {
return notificationRecorder;
}
/**
* Create a CreateDeleteOperation.
*
* @param modelElement
* the model element to delete or create
* @param delete
* whether the element is deleted or created
* @return the operation
*/
private CreateDeleteOperation createCreateDeleteOperation(EObject modelElement, boolean delete) {
final CreateDeleteOperation createDeleteOperation = OperationsFactory.eINSTANCE.createCreateDeleteOperation();
createDeleteOperation.setDelete(delete);
final EObject element = modelElement;
final List<EObject> allContainedModelElements = ModelUtil.getAllContainedModelElementsAsList(element, false);
allContainedModelElements.add(element);
final Copier copier = new Copier(true, false);
final EObject copiedElement = copier.copy(element);
copier.copyReferences();
final List<EObject> copiedAllContainedModelElements = ModelUtil.getAllContainedModelElementsAsList(
copiedElement,
false);
copiedAllContainedModelElements.add(copiedElement);
for (int i = 0; i < allContainedModelElements.size(); i++) {
final EObject child = allContainedModelElements.get(i);
final EObject copiedChild = copiedAllContainedModelElements.get(i);
final ModelElementId childId = collection.getModelElementId(child);
((CreateDeleteOperationImpl) createDeleteOperation).getEObjectToIdMap().put(copiedChild, childId);
}
createDeleteOperation.setModelElement(copiedElement);
createDeleteOperation.setModelElementId(collection.getModelElementId(modelElement));
createDeleteOperation.setClientDate(new Date());
return createDeleteOperation;
}
/**
*
* {@inheritDoc}
*
* @see org.eclipse.emf.emfstore.internal.common.model.util.IdEObjectCollectionChangeObserver#modelElementRemoved(org.eclipse.emf.emfstore.internal.common.model.IdEObjectCollection,
* org.eclipse.emf.ecore.EObject)
*/
public void modelElementRemoved(IdEObjectCollection project, EObject modelElement) {
if (isRecording) {
if (!commandIsRunning) {
handleElementDelete(modelElement);
collection.clearAllocatedCaches();
} else {
final Set<EObject> allModelElements = new LinkedHashSet<EObject>();
allModelElements.add(modelElement);
allModelElements.addAll(
ModelUtil.getAllContainedModelElements(
modelElement,
/* includeTransientContainments= */false));
final List<SettingWithReferencedElement> crossReferences = ModelUtil.collectOutgoingCrossReferences(
collection, allModelElements);
final List<SettingWithReferencedElement> ingoingCrossReferences = collectIngoingCrossReferences(
collection,
allModelElements);
crossReferences.addAll(ingoingCrossReferences);
removedElementsCache.addRemovedElement(modelElement, allModelElements, crossReferences);
}
}
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.emfstore.client.changetracking.ESCommandObserver#commandCompleted(org.eclipse.emf.common.command.Command)
*/
public void commandCompleted(Command command) {
commandCompleted(command, false);
}
/**
* Called to notify listener about the successful completion of the given command.
*
* @param command
* the completed command
* @param isNestedCommand
* whether the completed command is a command inside another one
*/
public void commandCompleted(Command command, boolean isNestedCommand) {
// means that we have not seen a command start yet
// if (currentClipboard == null) {
// return;
// }
final List<EObject> deletedElements = new ArrayList<EObject>();
for (int i = removedElementsCache.getRemovedRootElements().size() - 1; i >= 0; i--) {
final EObject removedElement = removedElementsCache.getRemovedRootElements().get(i);
if (!collection.contains(removedElement)) {
if (!deletedElements.contains(removedElement)) {
deletedElements.add(0, removedElement);
}
}
}
for (final EObject deletedElement : deletedElements) {
// element was deleted
projectSpace.executeRunnable(new Runnable() {
public void run() {
handleElementDelete(deletedElement);
}
});
}
// add all cut elements to modelElements to guarantee a consistent state if it is allowed
final Project project = projectSpace.getProject();
final EList<EObject> cutElements = project.getCutElements();
if (config.isDenyAddCutElementsToModelElements() && cutElements.size() != 0) {
throw new IllegalStateException(
Messages.OperationRecorder_CutElementsPresent_0
+ Messages.OperationRecorder_CutElementsPresent_1);
}
for (final EObject eObject : new ArrayList<EObject>(cutElements)) {
project.addModelElement(eObject);
}
operations = modifyOperations(operations, command);
operationsRecorded(operations);
removedElementsCache.clear();
operations.clear();
collection.clearAllocatedCaches();
commandIsRunning = isNestedCommand;
}
private List<AbstractOperation> modifyOperations(List<AbstractOperation> operations, Command command) {
if (operations.isEmpty() || config.getOperationModifier() == null) {
return operations;
}
return config.getOperationModifier().modify(operations, command);
}
private void deleteOutgoingCrossReferencesOfContainmentTree(Set<EObject> allEObjects) {
final List<SettingWithElementsToRemove> settingsToUnset = new ArrayList<SettingWithElementsToRemove>();
// delete all non containment cross references to other elements
for (final EObject modelElement : allEObjects) {
for (final EReference reference : modelElement.eClass().getEAllReferences()) {
if (!EClass.class.isInstance(reference.getEType())) {
continue;
}
final EClass eClass = (EClass) reference.getEType();
if (Map.Entry.class.isAssignableFrom(eClass.getInstanceClass()) && reference.isContainment()
&& reference.isChangeable()) {
handleMapEntryDeletion(modelElement, eClass, reference, allEObjects);
continue;
}
if (reference.isContainer() || reference.isContainment() || !reference.isChangeable()) {
continue;
}
// remove all (outgoing) references to elements outside of the containment tree of the element to be
// deleted
if (reference.isMany()) {
@SuppressWarnings("unchecked")
final Set<EObject> referencesToRemove = filterAllNonContained(
(List<EObject>) modelElement.eGet(reference),
allEObjects);
if (referencesToRemove.size() > 0) {
settingsToUnset.add(
new SettingWithElementsToRemove(
InternalEObject.class.cast(modelElement).eSetting(reference),
referencesToRemove));
}
} else {
final EObject referencedElement = (EObject) modelElement.eGet(reference);
if (referencedElement != null && !allEObjects.contains(referencedElement)) {
settingsToUnset.add(new SettingWithElementsToRemove(
InternalEObject.class.cast(modelElement).eSetting(reference)));
}
}
}
}
unsetAll(settingsToUnset);
}
private void unsetAll(final List<SettingWithElementsToRemove> settingsToUnset) {
for (final SettingWithElementsToRemove settingWithElementsToRemove : settingsToUnset) {
final Setting setting = settingWithElementsToRemove.setting;
final EStructuralFeature feature = setting.getEStructuralFeature();
final Set<EObject> referencesToRemove = settingWithElementsToRemove.elementsToRemove;
if (feature.isMany()) {
@SuppressWarnings("unchecked")
final List<EObject> referencedElements = (List<EObject>) setting.getEObject().eGet(feature);
referencedElements.removeAll(referencesToRemove);
} else {
setting.getEObject().eSet(feature, null);
}
}
}
/**
* Returns all elements that are not contained in {@code allElements}.
*
* @param elements
* the set of elements whose containment in {@code allElements} should be checked
* @param allElements
* the set of all elements
* @return all elements that are not contained in {@code allElements}
*/
private Set<EObject> filterAllNonContained(final List<EObject> elements, Set<EObject> allElements) {
final Set<EObject> referencedElementsCopy = new LinkedHashSet<EObject>(elements);
referencedElementsCopy.removeAll(allElements);
return referencedElementsCopy;
}
private void handleMapEntryDeletion(EObject modelElement, EClass mapEntryEClass, EReference reference,
Set<EObject> allEObjects) {
@SuppressWarnings("unchecked")
final List<EObject> mapEntriesEList = (List<EObject>) modelElement.eGet(reference);
final Set<EObject> mapEntriesToRemove = new LinkedHashSet<EObject>();
final EReference nonContainmentKeyReference = getNonContainmentKeyReference(mapEntryEClass);
// key references seems to be containment, skip loop
if (nonContainmentKeyReference == null) {
return;
}
// find all map entries which reference an object outside of the containment tree
// with their non-containment key
for (final EObject mapEntry : mapEntriesEList) {
final Object targetElement = mapEntry.eGet(nonContainmentKeyReference);
if (!allEObjects.contains(targetElement)) {
mapEntriesToRemove.add(mapEntry);
}
}
if (mapEntriesToRemove.isEmpty()) {
return;
}
// the reference is a containment map feature and its referenced entries do have at least one
// non-containment key cross-reference that goes to an element outside of
// the containment tree, therefore we delete respective map entries
// instead of waiting for the referenced key element to be cut off from the map entry
// in the children recursion
// since cutting off a key reference will render the map into an invalid state on de-serialization
// which can
// result in unresolved proxies
EcoreUtil.resolveAll(modelElement);
for (final EObject mapEntryToRemove : mapEntriesToRemove) {
mapEntriesEList.remove(mapEntryToRemove);
handleElementDelete(mapEntryToRemove);
}
}
private EReference getNonContainmentKeyReference(EClass eClass) {
for (final EReference eRef : eClass.getEReferences()) {
if (eRef.getName().equals("key") && !eRef.isContainment()) { //$NON-NLS-1$
return eRef;
} else if (eRef.getName().equals("key") && eRef.isContainment()) { //$NON-NLS-1$
return null;
}
}
// no key reference found
return null;
}
private void handleElementDelete(EObject deletedElement) {
final Set<EObject> allDeletedModelElements = ModelUtil.getAllContainedModelElements(deletedElement,
false /* includeTransientContainments is false */);
allDeletedModelElements.add(deletedElement);
deleteOutgoingCrossReferencesOfContainmentTree(allDeletedModelElements);
if (config.isCutOffIncomingCrossReferences()) {
for (final EObject element : allDeletedModelElements) {
// delete incoming cross references
final Collection<Setting> inverseReferences = projectSpace.findInverseCrossReferences(element);
ModelUtil.deleteIncomingCrossReferencesToElement(element,
inverseReferences,
allDeletedModelElements);
}
}
if (!isRecording) {
return;
}
final List<EObject> allContainedModelElements = ModelUtil.getAllContainedModelElementsAsList(deletedElement,
false);
allContainedModelElements.add(deletedElement);
final EObject copiedElement = ModelUtil.clone(deletedElement);
final List<EObject> copiedAllContainedModelElements = ModelUtil.getAllContainedModelElementsAsList(
copiedElement,
false);
copiedAllContainedModelElements.add(copiedElement);
final CreateDeleteOperation deleteOperation = OperationsFactory.eINSTANCE.createCreateDeleteOperation();
deleteOperation.setClientDate(new Date());
deleteOperation.setModelElement(copiedElement);
deleteOperation.setModelElementId(getDeletedModelElementId(deletedElement));
// sync IDs into Map
for (int i = 0; i < allContainedModelElements.size(); i++) {
final EObject child = allContainedModelElements.get(i);
final EObject copiedChild = copiedAllContainedModelElements.get(i);
final ModelElementId childId = getDeletedModelElementId(child); // collection.getDeletedModelElementId(child);
((CreateDeleteOperationImpl) deleteOperation).getEObjectToIdMap().put(copiedChild, childId);
}
deleteOperation.setDelete(true);
// extract all reference ops that belong to the delete
final List<CompositeOperation> compositeOperationsToDelete = new ArrayList<CompositeOperation>();
final List<ReferenceOperation> extractReferenceOperationsForDelete = extractReferenceOperationsForDelete(
deletedElement, compositeOperationsToDelete);
deleteOperation.getSubOperations().addAll(extractReferenceOperationsForDelete);
operations.removeAll(compositeOperationsToDelete);
if (compositeOperation != null) {
compositeOperation.getSubOperations().add(deleteOperation);
} else {
bufferOrRecordOperation(deleteOperation);
}
}
private ModelElementId getDeletedModelElementId(EObject deletedElement) {
if (!commandIsRunning) {
return collection.getDeletedModelElementId(deletedElement);
}
return ModelUtil.clone(removedElementsCache.getRemovedElementId(deletedElement));
}
@SuppressWarnings("unchecked")
private List<ReferenceOperation> extractReferenceOperationsForDelete(EObject deletedElement,
List<CompositeOperation> compositeOperationsToDelete) {
final Set<ModelElementId> allDeletedElementsIds = new LinkedHashSet<ModelElementId>();
for (final EObject child : ModelUtil.getAllContainedModelElements(deletedElement, false)) {
final ModelElementId childId = collection.getDeletedModelElementId(child);
allDeletedElementsIds.add(childId);
}
allDeletedElementsIds.add(collection.getDeletedModelElementId(deletedElement));
final List<ReferenceOperation> referenceOperationsForDelete = new ArrayList<ReferenceOperation>();
final List<AbstractOperation> newOperations = operations.subList(0, operations.size());
final List<AbstractOperation> l = new ArrayList<AbstractOperation>();
for (int i = newOperations.size() - 1; i >= 0; i--) {
final AbstractOperation operation = newOperations.get(i);
if (belongsToDelete(operation, allDeletedElementsIds)) {
referenceOperationsForDelete.add(0, (ReferenceOperation) operation);
l.add(operation);
continue;
}
if (operation instanceof CompositeOperation
&& ((CompositeOperation) operation).getMainOperation() != null) {
final CompositeOperation compositeOperation = (CompositeOperation) operation;
boolean doesNotBelongToDelete = false;
for (final AbstractOperation subOperation : compositeOperation.getSubOperations()) {
if (!belongsToDelete(subOperation, allDeletedElementsIds)) {
doesNotBelongToDelete = true;
break;
}
}
if (!doesNotBelongToDelete) {
referenceOperationsForDelete.addAll(0,
(Collection<? extends ReferenceOperation>) compositeOperation.getSubOperations());
compositeOperationsToDelete.add(compositeOperation);
}
continue;
}
break;
}
operations.removeAll(l);
return referenceOperationsForDelete;
}
private boolean belongsToDelete(AbstractOperation operation, Set<ModelElementId> allDeletedElementsIds) {
if (operation instanceof ReferenceOperation) {
final ReferenceOperation referenceOperation = (ReferenceOperation) operation;
final Set<ModelElementId> allInvolvedModelElements = referenceOperation.getAllInvolvedModelElements();
if (allInvolvedModelElements.removeAll(allDeletedElementsIds)) {
return isDestructorReferenceOperation(referenceOperation);
}
}
return false;
}
private boolean isDestructorReferenceOperation(ReferenceOperation referenceOperation) {
if (referenceOperation instanceof MultiReferenceOperation) {
final MultiReferenceOperation multiReferenceOperation = (MultiReferenceOperation) referenceOperation;
return !multiReferenceOperation.isAdd();
} else if (referenceOperation instanceof SingleReferenceOperation) {
final SingleReferenceOperation singleReferenceOperation = (SingleReferenceOperation) referenceOperation;
return singleReferenceOperation.getOldValue() != null && singleReferenceOperation.getNewValue() == null;
}
return false;
}
/**
*
* {@inheritDoc}
*
* @see org.eclipse.emf.emfstore.client.changetracking.ESCommandObserver#commandFailed(org.eclipse.emf.common.command.Command,
* java.lang.Exception)
*/
public void commandFailed(Command command, Exception exception) {
// this is a backup in order to remove obsolete operations. In most
// (all?) cases though, the rollback of the
// transaction does this.
if (compositeOperation != null) {
for (int i = compositeOperation.getSubOperations().size() - 1; i >= currentOperationListSize; i--) {
compositeOperation.getSubOperations().remove(i);
}
}
if (config.isRollbackAtCommandFailure()) {
for (int i = operations.size() - 1; i >= 0; i--) {
operations.get(i).reverse().apply(collection);
}
} else {
commandCompleted(command);
}
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.emfstore.client.changetracking.ESCommandObserver#commandStarted(org.eclipse.emf.common.command.Command)
*/
public void commandStarted(Command command) {
currentOperationListSize = 0;
commandIsRunning = true;
}
/**
* Returns the composite operation.
*
* @return the composite operation
*/
public CompositeOperation getCompositeOperation() {
return compositeOperation;
}
/**
* Begins a composite operation.
*
* @return the handle to the newly created composite operation
*/
public CompositeOperationHandle beginCompositeOperation() {
if (compositeOperation != null) {
throw new IllegalStateException(Messages.OperationRecorder_OnlyOneCompositeAllowed);
}
compositeOperation = OperationsFactory.eINSTANCE.createCompositeOperation();
final CompositeOperationHandle handle = new CompositeOperationHandle(this, compositeOperation);
notificationRecorder.newRecording();
return handle;
}
/**
* Replace and complete the current composite operation.
*
* @param semanticCompositeOperation
* the semantic operation that replaces the composite operation
*/
public void endCompositeOperation(SemanticCompositeOperation semanticCompositeOperation) {
compositeOperation = semanticCompositeOperation;
endCompositeOperation();
}
/**
* Complete the current composite operation.
*/
public void endCompositeOperation() {
bufferOrRecordOperation(compositeOperation);
compositeOperation = null;
notificationRecorder.stopRecording();
}
/**
* Aborts the current composite operation.
*/
public void abortCompositeOperation() {
final AbstractOperation reversedCompositeOperation = compositeOperation.reverse();
projectSpace.applyOperations(
Collections.singletonList(reversedCompositeOperation), false);
removedElementsCache.clear();
notificationRecorder.stopRecording();
compositeOperation = null;
currentOperationListSize = operations.size();
}
/**
*
* {@inheritDoc}
*
* @see org.eclipse.emf.emfstore.internal.common.model.util.IdEObjectCollectionChangeObserver#notify(org.eclipse.emf.common.notify.Notification,
* org.eclipse.emf.emfstore.internal.common.model.IdEObjectCollection, org.eclipse.emf.ecore.EObject)
*/
public void notify(Notification notification, IdEObjectCollection collection, EObject modelElement) {
if (!isRecording) {
return;
}
// filter unwanted notifications
if (FilterStack.DEFAULT.check(new NotificationInfo(notification).toAPI(), collection)) {
return;
}
checkCommandConstraints(modelElement);
notificationRecorder.record(notification);
if (notificationRecorder.isRecordingComplete()) {
final List<AbstractOperation> ops = recordingFinished();
// add resulting operations as sub-operations to composite or top-level operations
if (compositeOperation != null) {
compositeOperation.getSubOperations().addAll(ops);
return;
}
if (ops.size() > 1) {
bufferOrRecordOperation(createCompositeOperation(ops));
} else if (ops.size() == 1) {
bufferOrRecordOperation(ops.get(0));
}
}
}
private CompositeOperation createCompositeOperation(List<AbstractOperation> ops) {
final CompositeOperation op = OperationsFactory.eINSTANCE.createCompositeOperation();
op.getSubOperations().addAll(ops);
// set the last operation as the main one for natural composites
op.setMainOperation(ops.get(ops.size() - 1));
op.setModelElementId(ModelUtil.clone(op.getMainOperation().getModelElementId()));
return op;
}
private void bufferOrRecordOperations(List<AbstractOperation> operations) {
if (commandIsRunning && config.isEmitOperationsUponCommandCompletion()) {
this.operations.addAll(operations);
} else {
operationsRecorded(operations);
}
}
private void bufferOrRecordOperation(AbstractOperation operation) {
bufferOrRecordOperations(Arrays.asList(operation));
}
/**
*
* {@inheritDoc}
*
* @see org.eclipse.emf.emfstore.internal.common.model.util.IdEObjectCollectionChangeObserver#collectionDeleted(org.eclipse.emf.emfstore.internal.common.model.IdEObjectCollection)
*/
public void collectionDeleted(IdEObjectCollection collection) {
}
/**
* Returns the project space this operation recorder is attached to.
*
* @return the project space this operation recorder is attached to
*/
public ProjectSpace getProjectSpace() {
return projectSpace;
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.emfstore.client.observer.ESUpdateObserver#inspectChanges(org.eclipse.emf.emfstore.client.ESLocalProject,
* java.util.List, org.eclipse.core.runtime.IProgressMonitor)
*/
public boolean inspectChanges(ESLocalProject project, List<ESChangePackage> changePackages,
IProgressMonitor monitor) {
return true;
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.emfstore.client.observer.ESCommitObserver#inspectChanges(org.eclipse.emf.emfstore.client.ESLocalProject,
* org.eclipse.emf.emfstore.server.model.ESChangePackage, org.eclipse.core.runtime.IProgressMonitor)
*/
public boolean inspectChanges(ESLocalProject project, ESChangePackage changePackage, IProgressMonitor monitor) {
return true;
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.emfstore.client.observer.ESUpdateObserver#updateCompleted(org.eclipse.emf.emfstore.client.ESLocalProject,
* org.eclipse.core.runtime.IProgressMonitor)
*/
public void updateCompleted(ESLocalProject project, IProgressMonitor monitor) {
clearAllocatedCaches(project);
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.emfstore.client.observer.ESShareObserver#shareDone(org.eclipse.emf.emfstore.client.ESLocalProject)
*/
public void shareDone(ESLocalProject localProject) {
clearAllocatedCaches(localProject);
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.emfstore.client.observer.ESCommitObserver#commitCompleted(org.eclipse.emf.emfstore.client.ESLocalProject,
* org.eclipse.emf.emfstore.server.model.versionspec.ESPrimaryVersionSpec,
* org.eclipse.core.runtime.IProgressMonitor)
*/
public void commitCompleted(ESLocalProject project, ESPrimaryVersionSpec newRevision, IProgressMonitor monitor) {
clearAllocatedCaches(project);
}
private void clearAllocatedCaches(ESLocalProject project) {
if (((ESLocalProjectImpl) project).toInternalAPI().getProject().equals(collection) && !commandIsRunning) {
collection.clearAllocatedCaches();
}
}
/**
* Whether the operation recorder considers a command is being run.
*
* @return <code>true</code> if a command is being run, <code>false</code> otherwise
*/
public boolean isCommandRunning() {
return commandIsRunning;
}
/**
* Helper class to capture a setting a all elements that need to be
* removed from the feature in case the feature represents a many reference.
* Otherwise the collection of elements to be removed remains empty.
*
*/
class SettingWithElementsToRemove {
/**
* The {@link Setting}.
*/
private final Setting setting;
/**
* The elements to be removed in case the feature within in the setting is a many reference.
*/
private final Set<EObject> elementsToRemove = new LinkedHashSet<EObject>();
/**
* Constructor for non-many references.
*
* @param setting
* a setting consisting of an {@link EObject} and a non-many reference
*/
SettingWithElementsToRemove(Setting setting) {
this.setting = setting;
}
/**
* Constructor for many references.
*
* @param setting
* a setting consisting of an {@link EObject} and a many reference
* @param elementsToRemove
* the elemets to be removed from the many reference
*/
SettingWithElementsToRemove(Setting setting, Set<EObject> elementsToRemove) {
this.setting = setting;
this.elementsToRemove.addAll(elementsToRemove);
}
/**
* Returns the {@link Setting}.
*
* @return the setting
*/
public Setting getSetting() {
return setting;
}
/**
* Returns the elements to be removed in case the feature within in the setting is a many reference.
*
* @return the elements to be removed. In case the feature of the setting is non-many, the set
* will be empty
*/
public Set<EObject> getElementsToRemove() {
return elementsToRemove;
}
}
}