blob: 4e4613f7995c45888550a612baf5f10af8524cc6 [file] [log] [blame]
/*******************************************************************************
* Copyright 2011 Chair for Applied Software Engineering,
* Technische Universitaet Muenchen.
* All rights reserved. This program and the accompanying materials
* are made available under the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
******************************************************************************/
package org.eclipse.emf.emfstore.client.model.impl;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
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.Setting;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.emf.ecore.util.EcoreUtil.Copier;
import org.eclipse.emf.edit.domain.EditingDomain;
import org.eclipse.emf.emfstore.client.model.CompositeOperationHandle;
import org.eclipse.emf.emfstore.client.model.Configuration;
import org.eclipse.emf.emfstore.client.model.ProjectSpace;
import org.eclipse.emf.emfstore.client.model.WorkspaceManager;
import org.eclipse.emf.emfstore.client.model.changeTracking.NotificationToOperationConverter;
import org.eclipse.emf.emfstore.client.model.changeTracking.commands.CommandObserver;
import org.eclipse.emf.emfstore.client.model.changeTracking.commands.EMFStoreCommandStack;
import org.eclipse.emf.emfstore.client.model.changeTracking.notification.NotificationInfo;
import org.eclipse.emf.emfstore.client.model.changeTracking.notification.filter.FilterStack;
import org.eclipse.emf.emfstore.client.model.changeTracking.notification.recording.NotificationRecorder;
import org.eclipse.emf.emfstore.client.model.exceptions.MissingCommandException;
import org.eclipse.emf.emfstore.client.model.observers.PostCreationObserver;
import org.eclipse.emf.emfstore.client.model.util.WorkspaceUtil;
import org.eclipse.emf.emfstore.common.extensionpoint.ExtensionPoint;
import org.eclipse.emf.emfstore.common.model.IdEObjectCollection;
import org.eclipse.emf.emfstore.common.model.ModelElementId;
import org.eclipse.emf.emfstore.common.model.Project;
import org.eclipse.emf.emfstore.common.model.impl.IdEObjectCollectionImpl;
import org.eclipse.emf.emfstore.common.model.util.EObjectChangeNotifier;
import org.eclipse.emf.emfstore.common.model.util.IdEObjectCollectionChangeObserver;
import org.eclipse.emf.emfstore.common.model.util.ModelUtil;
import org.eclipse.emf.emfstore.common.model.util.SettingWithReferencedElement;
import org.eclipse.emf.emfstore.server.model.versioning.operations.AbstractOperation;
import org.eclipse.emf.emfstore.server.model.versioning.operations.CompositeOperation;
import org.eclipse.emf.emfstore.server.model.versioning.operations.CreateDeleteOperation;
import org.eclipse.emf.emfstore.server.model.versioning.operations.MultiReferenceOperation;
import org.eclipse.emf.emfstore.server.model.versioning.operations.OperationsFactory;
import org.eclipse.emf.emfstore.server.model.versioning.operations.ReferenceOperation;
import org.eclipse.emf.emfstore.server.model.versioning.operations.SingleReferenceOperation;
import org.eclipse.emf.emfstore.server.model.versioning.operations.impl.CreateDeleteOperationImpl;
import org.eclipse.emf.emfstore.server.model.versioning.operations.semantic.SemanticCompositeOperation;
/**
* Tracks changes on any given {@link IdEObjectCollection}.
*
* @author koegel
* @author emueller
*/
public class OperationRecorder implements CommandObserver, IdEObjectCollectionChangeObserver {
/**
* Name of unknown creator.
*/
public static final String UNKOWN_CREATOR = "unknown";
private int currentOperationListSize;
private EditingDomain editingDomain;
private EMFStoreCommandStack emfStoreCommandStack;
// TODO: currently not in use
// private Set<EObject> currentClipboard;
private List<AbstractOperation> operations;
private List<EObject> removedElements;
private NotificationToOperationConverter converter;
private NotificationRecorder notificationRecorder;
private CompositeOperation compositeOperation;
private ProjectSpaceBase projectSpace;
private IdEObjectCollectionImpl collection;
private EObjectChangeNotifier changeNotifier;
private boolean isRecording;
private boolean commandIsRunning;
private boolean cutOffIncomingCrossReferences;
private boolean forceCommands;
private boolean denyAddCutElementsToModelElements;
private boolean emitOperationsWhenCommandCompleted;
// no use of ObserverBus
private List<OperationRecorderListener> observers;
private OperationModificator modificator;
private Map<EObject, List<SettingWithReferencedElement>> removedElementsToReferenceSettings;
/**
* Constructor.
*
* @param projectSpace
* the {@link ProjectSpaceBase} the recorder should be attached to
* @param changeNotifier
* a change notifier that informs clients about changes in the collection
*/
public OperationRecorder(ProjectSpaceBase projectSpace, EObjectChangeNotifier changeNotifier) {
this.projectSpace = projectSpace;
this.collection = (IdEObjectCollectionImpl) projectSpace.getProject();
this.changeNotifier = changeNotifier;
operations = new ArrayList<AbstractOperation>();
observers = new ArrayList<OperationRecorderListener>();
removedElements = new ArrayList<EObject>();
removedElementsToReferenceSettings = new HashMap<EObject, List<SettingWithReferencedElement>>();
editingDomain = Configuration.getEditingDomain();
converter = new NotificationToOperationConverter(collection);
cutOffIncomingCrossReferences = getBooleanExtensionPoint("org.eclipse.emf.emfstore.client.recording.options",
"cutOffIncomingCrossReferences", true); // cut off incoming cross-references by default
forceCommands = getBooleanExtensionPoint("org.eclipse.emf.emfstore.client.recording.options", "forceCommands",
false); // usage of commands is not forced by default
denyAddCutElementsToModelElements = getBooleanExtensionPoint(
"org.eclipse.emf.emfstore.client.recording.options", "denyAddCutElementsToModelElements", false);
emitOperationsWhenCommandCompleted = true;
setModificator(initOperationModificator());
}
private OperationModificator initOperationModificator() {
OperationModificator result = new ExtensionPoint("org.eclipse.emf.emfstore.client.recording.options").getClass(
"operationModificator", OperationModificator.class);
if (result != null) {
return result;
}
return null;
}
private boolean getBooleanExtensionPoint(String extensionPointId, String attributeName, boolean defaultValue) {
Boolean booleanOption = new ExtensionPoint(extensionPointId).getBoolean(attributeName);
if (booleanOption != null) {
return booleanOption;
}
return defaultValue;
}
/**
* Clears the operations list.
*/
public void clearOperations() {
operations.clear();
}
/**
* Returns the collection the operation recorder is operation on.
*
* @return the collection the operation recorder is operation on
*/
public IdEObjectCollection getCollection() {
return collection;
}
/**
* Returns the elements removed during execution of the operations
* that have been recorded by the recorder.
*
* @return the elements removed during execution of the operations
*/
public List<EObject> getRemovedElements() {
return removedElements;
}
private Set<EObject> getModelElementsFromClipboard() {
Set<EObject> result = new HashSet<EObject>();
if (editingDomain == null) {
return result;
}
Collection<Object> clipboard = editingDomain.getClipboard();
if (clipboard == null) {
return result;
}
for (Object element : clipboard) {
if (element instanceof EObject) {
result.add((EObject) element);
}
}
return result;
}
/**
*
* {@inheritDoc}
*
* @see org.eclipse.emf.emfstore.common.model.util.IdEObjectCollectionChangeObserver#modelElementAdded(org.eclipse.emf.emfstore.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();
WorkspaceManager.getObserverBus().notify(PostCreationObserver.class).onCreation(modelElement);
startChangeRecording();
Set<EObject> allModelElements = new HashSet<EObject>();
allModelElements.add(modelElement);
allModelElements.addAll(ModelUtil.getAllContainedModelElements(modelElement, false));
// collect out-going cross-reference for containment tree of modelElement
List<SettingWithReferencedElement> crossReferences = ModelUtil.collectOutgoingCrossReferences(collection,
allModelElements);
// collect in-going cross-reference for containment tree of modelElement
List<SettingWithReferencedElement> ingoingCrossReferences = collectIngoingCrossReferences(collection,
allModelElements);
crossReferences.addAll(ingoingCrossReferences);
// clean up already existing references when collection already contained the object
List<SettingWithReferencedElement> savedSettings = removedElementsToReferenceSettings.get(modelElement);
if (savedSettings != null) {
List<SettingWithReferencedElement> toRemove = new ArrayList<SettingWithReferencedElement>();
for (SettingWithReferencedElement setting : savedSettings) {
for (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
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 && removedElements.contains(modelElement)) {
resultingOperations = new ArrayList<AbstractOperation>(recordedOperations);
} else {
CreateDeleteOperation createDeleteOperation = createCreateDeleteOperation(modelElement, false);
createDeleteOperation.getSubOperations().addAll(recordedOperations);
resultingOperations = new ArrayList<AbstractOperation>();
resultingOperations.add(createDeleteOperation);
}
if (this.compositeOperation != null) {
compositeOperation.getSubOperations().addAll(resultingOperations);
return;
}
if (commandIsRunning && emitOperationsWhenCommandCompleted) {
operations.addAll(resultingOperations);
} else {
operationsRecorded(resultingOperations);
}
}
private void checkCommandConstraints(EObject modelElement) {
if (!commandIsRunning && forceCommands) {
WorkspaceUtil.handleException("An element has been changed without using a command!",
new MissingCommandException("Element " + modelElement + " has been changed without using a command"));
}
}
private List<SettingWithReferencedElement> collectIngoingCrossReferences(IdEObjectCollection collection,
Set<EObject> allModelElements) {
List<SettingWithReferencedElement> settings = new ArrayList<SettingWithReferencedElement>();
for (EObject modelElement : allModelElements) {
Collection<Setting> inverseReferences = projectSpace.findInverseCrossReferences(modelElement);
for (Setting setting : inverseReferences) {
if (!ModelUtil.shouldBeCollected(collection, allModelElements, setting.getEObject())) {
continue;
}
EReference reference = (EReference) setting.getEStructuralFeature();
EClassifier eType = reference.getEType();
if (reference.isContainer() || reference.isContainment() || !reference.isChangeable()
|| (!(eType instanceof EClass))) {
continue;
}
SettingWithReferencedElement settingWithReferencedElement = new SettingWithReferencedElement(setting,
modelElement);
settings.add(settingWithReferencedElement);
}
}
return settings;
}
private List<ReferenceOperation> generateCrossReferenceOperations(
Collection<SettingWithReferencedElement> crossReferences) {
List<ReferenceOperation> result = new ArrayList<ReferenceOperation>();
for (SettingWithReferencedElement setting : crossReferences) {
EObject referencedElement = setting.getReferencedElement();
// fetch ID of referenced element
ModelElementId newModelElementId = collection.getModelElementId(referencedElement);
if (newModelElementId == null) {
newModelElementId = collection.getDeletedModelElementId(referencedElement);
}
EObject eObject = setting.getSetting().getEObject();
EReference reference = (EReference) setting.getSetting().getEStructuralFeature();
if (setting.getSetting().getEStructuralFeature().isMany()) {
int position = ((List<?>) eObject.eGet(reference)).indexOf(referencedElement);
MultiReferenceOperation multiRefOp = NotificationToOperationConverter.createMultiReferenceOperation(
collection, eObject, reference, Arrays.asList(referencedElement), true, position);
result.add(multiRefOp);
} else {
SingleReferenceOperation singleRefOp = NotificationToOperationConverter.createSingleReferenceOperation(
collection, null, newModelElementId, reference, eObject);
result.add(singleRefOp);
}
}
return result;
}
private void operationsRecorded(List<? extends AbstractOperation> operations) {
if (operations.size() == 0) {
return;
}
for (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.
*
* @generated NOT
*/
public void startChangeRecording() {
if (notificationRecorder == null) {
notificationRecorder = new NotificationRecorder();
}
isRecording = true;
}
/**
*
*
* @param notificationDisabled
* the notificationDisabled to set
*/
public void disableNotifications(boolean notificationDisabled) {
if (this.changeNotifier != null) {
this.changeNotifier.disableNotifications(notificationDisabled);
}
}
/**
* Stops current recording of changes and adds recorded changes to this
* project spaces changes.
*
* @generated NOT
*/
public void stopChangeRecording() {
this.isRecording = false;
}
private List<AbstractOperation> recordingFinished() {
// create operations from "valid" notifications, log invalid ones,
// accumulate the ops
List<AbstractOperation> ops = new LinkedList<AbstractOperation>();
List<NotificationInfo> rec = notificationRecorder.getRecording().asMutableList();
for (NotificationInfo n : rec) {
if (!n.isValid()) {
WorkspaceUtil.log("INVALID NOTIFICATION MESSAGE DETECTED: " + n.getValidationMessage(), null, 0);
continue;
} else {
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("INVALID NOTIFICATION CLASSIFICATION,"
+ " notification is valid, but cannot be converted to an operation: " + 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) {
CreateDeleteOperation createDeleteOperation = OperationsFactory.eINSTANCE.createCreateDeleteOperation();
createDeleteOperation.setDelete(delete);
EObject element = modelElement;
List<EObject> allContainedModelElements = ModelUtil.getAllContainedModelElementsAsList(element, false);
allContainedModelElements.add(element);
Copier copier = new Copier(true, false);
EObject copiedElement = copier.copy(element);
copier.copyReferences();
List<EObject> copiedAllContainedModelElements = ModelUtil.getAllContainedModelElementsAsList(copiedElement,
false);
copiedAllContainedModelElements.add(copiedElement);
for (int i = 0; i < allContainedModelElements.size(); i++) {
EObject child = allContainedModelElements.get(i);
if (ModelUtil.isIgnoredDatatype(child)) {
continue;
}
EObject copiedChild = copiedAllContainedModelElements.get(i);
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.common.model.util.ProjectChangeObserver#modelElementRemoved(org.eclipse.emf.emfstore.common.model.Project,
* org.eclipse.emf.emfstore.common.model.ModelElement)
*/
public void modelElementRemoved(IdEObjectCollection project, EObject modelElement) {
if (isRecording) {
removedElements.add(modelElement);
Set<EObject> allModelElements = new HashSet<EObject>();
allModelElements.add(modelElement);
allModelElements.addAll(ModelUtil.getAllContainedModelElements(modelElement, false));
List<SettingWithReferencedElement> crossReferences = ModelUtil.collectOutgoingCrossReferences(collection,
allModelElements);
List<SettingWithReferencedElement> ingoingCrossReferences = collectIngoingCrossReferences(collection,
allModelElements);
crossReferences.addAll(ingoingCrossReferences);
if (crossReferences.size() != 0) {
for (EObject eObject : allModelElements) {
removedElementsToReferenceSettings.put(eObject, crossReferences);
}
}
}
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.emfstore.client.model.changeTracking.commands.CommandObserver#commandCompleted(org.eclipse.emf.common.command.Command)
*/
public void commandCompleted(Command command) {
// means that we have not seen a command start yet
// if (currentClipboard == null) {
// return;
// }
List<EObject> deletedElements = new ArrayList<EObject>();
for (int i = removedElements.size() - 1; i >= 0; i--) {
EObject removedElement = removedElements.get(i);
if (!collection.containsInstance(removedElement)) {
if (!deletedElements.contains(removedElement)) {
deletedElements.add(0, removedElement);
}
}
}
Set<EObject> newElementsOnClipboardAfterCommand = getModelElementsFromClipboard();
// newElementsOnClipboardAfterCommand.removeAll(currentClipboard);
// handle deleted elements => cut command
for (EObject deletedElement : deletedElements) {
if (newElementsOnClipboardAfterCommand.contains(deletedElement)) {
// TODO: EM, where to put cut elements?
// element was cut
// projectSpace.getProject().getCutElements().add(deletedElement);
} else {
// element was deleted
handleElementDelete(deletedElement);
}
}
// add all cut elements to modelElements to guarantee a consistent state if it is allowed
Project project = projectSpace.getProject();
EList<EObject> cutElements = project.getCutElements();
if (denyAddCutElementsToModelElements && cutElements.size() != 0) {
throw new IllegalStateException(
"It is not allowed to have cutelements at the end of the command."
+ " Remove them or use denyAddCutElementsToModelElements flag in the org.eclipse.emf.emfstore.client.recording.options extension point.");
}
for (EObject eObject : new ArrayList<EObject>(cutElements)) {
project.addModelElement(eObject);
}
operations = modifyOperations(operations, command);
operationsRecorded(operations);
removedElements.clear();
operations.clear();
commandIsRunning = false;
// remove all deleted elements
newElementsOnClipboardAfterCommand.removeAll(deletedElements);
collection.clearVolatileCaches();
}
private List<AbstractOperation> modifyOperations(List<AbstractOperation> operations, Command command) {
if (getModificator() == null) {
return operations;
}
return getModificator().modify(operations, command);
}
private void deleteOutgoingCrossReferencesOfContainmentTree(Set<EObject> allEObjects) {
// delete all non containment cross references to other elments
for (EObject modelElement : allEObjects) {
for (EReference reference : modelElement.eClass().getEAllReferences()) {
EClassifier eType = reference.getEType();
if (!(eType instanceof EClass)) {
continue;
}
if (Map.Entry.class.isAssignableFrom(eType.getInstanceClass()) && reference.isContainment()
&& reference.isChangeable()) {
handleMapEntryDeletion(modelElement, eType, 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")
List<EObject> referencedElements = (List<EObject>) modelElement.eGet(reference);
List<EObject> referencesToRemove = new ArrayList<EObject>();
for (EObject referencedElement : referencedElements) {
if (!allEObjects.contains(referencedElement)) {
referencesToRemove.add(referencedElement);
}
}
referencedElements.removeAll(referencesToRemove);
} else {
EObject referencedElement = (EObject) modelElement.eGet(reference);
if (referencedElement != null && !allEObjects.contains(referencedElement)) {
modelElement.eSet(reference, null);
}
}
}
}
}
private void handleMapEntryDeletion(EObject modelElement, EClassifier eType, EReference reference,
Set<EObject> allEObjects) {
EClass mapEntryEClass = (EClass) eType;
EReference nonContainmentKeyReference = getNonContainmentKeyReference(mapEntryEClass);
// key references seems to be containment, skip loop
if (nonContainmentKeyReference == null) {
return;
}
@SuppressWarnings("unchecked")
List<EObject> mapEntriesEList = (List<EObject>) modelElement.eGet(reference);
boolean outgoingKeyReferenceFound = false;
// check key reference of all map entries if they reference one of the objects in the containment
// tree
for (EObject eObject : mapEntriesEList) {
Object eGet = eObject.eGet(nonContainmentKeyReference);
if (!allEObjects.contains(eGet)) {
outgoingKeyReferenceFound = true;
break;
}
}
if (!outgoingKeyReferenceFound) {
// no bad reference found, skip special treatment
return;
}
// copy list before clearing reference
// TODO is this really the underlying list
List<EObject> mapEntries = new ArrayList<EObject>(mapEntriesEList);
// the reference is a containment map feature and its referenced entries do have at least one
// non-containment key crossreference that goes to an element outside of
// the containment tree, therefore we
// delete the 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 deserialization
// which can
// result in unresolved proxies
EcoreUtil.resolveAll(modelElement);
modelElement.eUnset(reference);
for (EObject mapEntry : mapEntries) {
handleElementDelete(mapEntry);
}
}
private EReference getNonContainmentKeyReference(EClass eClass) {
for (EReference eRef : eClass.getEReferences()) {
if (eRef.getName().equals("key") && !eRef.isContainment()) {
return eRef;
} else if (eRef.getName().equals("key") && eRef.isContainment()) {
return null;
}
}
// no key reference found
return null;
}
private void handleElementDelete(EObject deletedElement) {
Set<EObject> allContainedModelElementsSet = ModelUtil.getAllContainedModelElements(deletedElement, false);
allContainedModelElementsSet.add(deletedElement);
deleteOutgoingCrossReferencesOfContainmentTree(allContainedModelElementsSet);
//
// if (!CommonUtil.isSelfContained(deletedElement, true)) {
// throw new IllegalStateException(
// "Element was removed from containment of project but still has cross references!: "
// + collection.getDeletedModelElementId(deletedElement).getId());
// // TODO: EM, remove project cast, if possible
// } else
if (cutOffIncomingCrossReferences) {
for (EObject eObject : allContainedModelElementsSet) {
// delete incoming cross references
Collection<Setting> inverseReferences = projectSpace.findInverseCrossReferences(eObject);
ModelUtil.deleteIncomingCrossReferencesFromParent(inverseReferences, eObject);
}
}
if (!isRecording) {
return;
}
List<EObject> allContainedModelElements = ModelUtil.getAllContainedModelElementsAsList(deletedElement, false);
allContainedModelElements.add(deletedElement);
EObject copiedElement = ModelUtil.clone(deletedElement);
List<EObject> copiedAllContainedModelElements = ModelUtil.getAllContainedModelElementsAsList(copiedElement,
false);
copiedAllContainedModelElements.add(copiedElement);
CreateDeleteOperation deleteOperation = OperationsFactory.eINSTANCE.createCreateDeleteOperation();
deleteOperation.setClientDate(new Date());
deleteOperation.setModelElement(copiedElement);
deleteOperation.setModelElementId(collection.getDeletedModelElementId(deletedElement));
// sync IDs into Map
for (int i = 0; i < allContainedModelElements.size(); i++) {
EObject child = allContainedModelElements.get(i);
EObject copiedChild = copiedAllContainedModelElements.get(i);
ModelElementId childId = collection.getDeletedModelElementId(child);
((CreateDeleteOperationImpl) deleteOperation).getEObjectToIdMap().put(copiedChild, childId);
}
deleteOperation.setDelete(true);
// extract all reference ops that belong to the delete
List<CompositeOperation> compositeOperationsToDelete = new ArrayList<CompositeOperation>();
deleteOperation.getSubOperations().addAll(
extractReferenceOperationsForDelete(deletedElement, compositeOperationsToDelete));
operations.removeAll(compositeOperationsToDelete);
if (compositeOperation != null) {
compositeOperation.getSubOperations().add(deleteOperation);
saveResource(compositeOperation.eResource());
} else {
if (commandIsRunning) {
operations.add(deleteOperation);
} else {
operationRecorded(deleteOperation);
}
}
}
@SuppressWarnings("unchecked")
private List<ReferenceOperation> extractReferenceOperationsForDelete(EObject deletedElement,
List<CompositeOperation> compositeOperationsToDelete) {
Set<ModelElementId> allDeletedElementsIds = new HashSet<ModelElementId>();
for (EObject child : ModelUtil.getAllContainedModelElements(deletedElement, false)) {
ModelElementId childId = collection.getDeletedModelElementId(child);
allDeletedElementsIds.add(childId);
}
allDeletedElementsIds.add(collection.getDeletedModelElementId(deletedElement));
List<ReferenceOperation> referenceOperationsForDelete = new ArrayList<ReferenceOperation>();
List<AbstractOperation> newOperations = operations.subList(0, operations.size());
List<AbstractOperation> l = new ArrayList<AbstractOperation>();
for (int i = newOperations.size() - 1; i >= 0; i--) {
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) {
CompositeOperation compositeOperation = (CompositeOperation) operation;
boolean doesNotBelongToDelete = false;
for (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) {
ReferenceOperation referenceOperation = (ReferenceOperation) operation;
Set<ModelElementId> allInvolvedModelElements = referenceOperation.getAllInvolvedModelElements();
if (allInvolvedModelElements.removeAll(allDeletedElementsIds)) {
return isDestructorReferenceOperation(referenceOperation);
}
}
return false;
}
private boolean isDestructorReferenceOperation(ReferenceOperation referenceOperation) {
if (referenceOperation instanceof MultiReferenceOperation) {
MultiReferenceOperation multiReferenceOperation = (MultiReferenceOperation) referenceOperation;
return !multiReferenceOperation.isAdd();
} else if (referenceOperation instanceof SingleReferenceOperation) {
SingleReferenceOperation singleReferenceOperation = (SingleReferenceOperation) referenceOperation;
return singleReferenceOperation.getOldValue() != null && singleReferenceOperation.getNewValue() == null;
}
return false;
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.emfstore.client.model.changeTracking.commands.CommandObserver#commandFailed(org.eclipse.emf.common.command.Command,
* org.eclipse.core.runtime.OperationCanceledException)
*/
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);
}
}
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.emfstore.client.model.changeTracking.commands.CommandObserver#commandStarted(org.eclipse.emf.common.command.Command)
*/
public void commandStarted(Command command) {
currentOperationListSize = 0;
// TODO: clipboard currently not in use
// currentClipboard = getModelElementsFromClipboard();
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("Can only have one composite at once!");
}
compositeOperation = OperationsFactory.eINSTANCE.createCompositeOperation();
CompositeOperationHandle handle = new CompositeOperationHandle(this, compositeOperation);
notificationRecorder.newRecording();
// operations.add(compositeOperation);
// currentOperationListSize++;
return handle;
}
/**
* Replace and complete the current composite operation.
*
* @param semanticCompositeOperation
* the semantic operation that replaces the composite operation
*/
public void endCompositeOperation(SemanticCompositeOperation semanticCompositeOperation) {
// operations.remove(operations.size() - 1);
// operations.add(semanticCompositeOperation);
compositeOperation = semanticCompositeOperation;
endCompositeOperation();
}
/**
* Complete the current composite operation.
*/
public void endCompositeOperation() {
if (!commandIsRunning || !emitOperationsWhenCommandCompleted) {
// operations.remove(compositeOperation);
operationRecorded(compositeOperation);
} else {
operations.add(compositeOperation);
}
this.compositeOperation = null;
}
/**
* Aborts the current composite operation.
*/
public void abortCompositeOperation() {
// if (operations.size() > 0) {
// AbstractOperation lastOp = operations.get(operations.size() - 1);
// AbstractOperation lastOp = compositeOperation.getSubOperations().get(
// compositeOperation.getSubOperations().size() - 1);
projectSpace.applyOperations(Collections.singletonList(compositeOperation.reverse()), false);
this.removedElements.clear();
notificationRecorder.stopRecording();
compositeOperation = null;
currentOperationListSize = operations.size();
removedElements.clear();
}
/**
*
* {@inheritDoc}
*
* @see org.eclipse.emf.emfstore.common.model.util.IdEObjectCollectionChangeObserver#notify(org.eclipse.emf.common.notify.Notification,
* org.eclipse.emf.emfstore.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), collection)) {
return;
}
checkCommandConstraints(modelElement);
notificationRecorder.record(notification);
if (notificationRecorder.isRecordingComplete()) {
List<AbstractOperation> ops = recordingFinished();
// add resulting operations as sub-operations to composite or top-level operations
if (compositeOperation != null) {
compositeOperation.getSubOperations().addAll(ops);
// FIXME: ugly hack for recording of create operation cross references
saveResource(compositeOperation.eResource());
return;
}
if (ops.size() > 1) {
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()));
if (commandIsRunning && emitOperationsWhenCommandCompleted) {
operations.add(op);
} else {
operationRecorded(op);
}
} else if (ops.size() == 1) {
if (commandIsRunning && emitOperationsWhenCommandCompleted) {
operations.add(ops.get(0));
} else {
operationRecorded(ops.get(0));
}
}
}
}
private void operationRecorded(AbstractOperation op) {
operationsRecorded(Arrays.asList(op));
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.emfstore.common.model.util.IdEObjectCollectionChangeObserver#collectionDeleted(org.eclipse.emf.emfstore.common.model.IdEObjectCollection)
*/
public void collectionDeleted(IdEObjectCollection collection) {
if (emfStoreCommandStack != null) {
emfStoreCommandStack.removeCommandStackObserver(this);
}
}
/**
* Whether operation should be emitted when command is completed.
*
* @param emitOperationsImmediately
* If false, operations will be emitted immediately upon recording
*/
public void emitOperationsWhenCommandCompleted(boolean emitOperationsImmediately) {
this.emitOperationsWhenCommandCompleted = emitOperationsImmediately;
}
/**
* Saves the given resource.
* If the passed resource is null, this method has no effect
*
* @param resource
* the resource to be saved
*/
private void saveResource(Resource resource) {
if (resource == null) {
return;
}
try {
resource.save(ModelUtil.getResourceSaveOptions());
} catch (IOException e) {
String message = String.format("Resource %s could not be saved!", resource.getURI());
WorkspaceUtil.logWarning(message, null);
}
}
/**
* 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;
}
/**
* Returns the used operation modificator, if any.
*
* @return the operation modificator in use
*/
public OperationModificator getModificator() {
return modificator;
}
/**
* Sets an optional operation modificator to be used.
*
* @param modificator
* the operation modificator to be used
*/
public void setModificator(OperationModificator modificator) {
this.modificator = modificator;
}
}