blob: 930fd643348a34f53d1c90aa49de48b40d2a18aa [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2005, 2012 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.bpel.ui.commands.util;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.bpel.common.extension.model.Extension;
import org.eclipse.bpel.common.extension.model.ExtensionMap;
import org.eclipse.bpel.common.extension.model.ExtensionmodelPackage;
import org.eclipse.bpel.common.extension.model.impl.ExtensionImpl;
import org.eclipse.bpel.common.extension.model.impl.ExtensionMapImpl;
import org.eclipse.bpel.common.extension.model.notify.ExtensionModelNotification;
import org.eclipse.bpel.ui.BPELEditor;
import org.eclipse.bpel.ui.BPELUIPlugin;
import org.eclipse.bpel.ui.util.BPELUtil;
import org.eclipse.bpel.ui.util.ModelHelper;
import org.eclipse.emf.common.notify.Adapter;
import org.eclipse.emf.common.notify.Notification;
import org.eclipse.emf.common.notify.Notifier;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.util.EContentAdapter;
import org.eclipse.core.runtime.Assert;
/**
* Records the exact changes made to the model during each 'user change'. This
* history information can be used to Undo and Redo user changes automatically.
*/
public class ModelAutoUndoRecorder implements IAutoUndoRecorder {
protected Set<Resource> ignoreResources = new HashSet<Resource>();
protected boolean VERBOSE_DEBUG = false;
protected boolean DEBUG = false || VERBOSE_DEBUG;
protected Set<Notifier> listenerRootSet = new HashSet<Notifier>();
protected List<Object> currentChangeList = null;
/**
* IUndoHandler to undo/redo a change to the ExtensionMap (adding, changing or
* removing an extension entry).
*/
class EMapSingleChangeHandler implements IUndoHandler {
ExtensionMap fExtensionMap;
EObject fExtendedObject, fOldExtension, fNewExtension;
/**
* @param extensionMap
* @param extendedObject
* @param oldExtension
* @param newExtension
*/
public EMapSingleChangeHandler(ExtensionMap extensionMap, EObject extendedObject,
EObject oldExtension, EObject newExtension)
{
this.fExtensionMap = extensionMap; this.fExtendedObject = extendedObject;
this.fOldExtension = oldExtension; this.fNewExtension = newExtension;
}
/**
* @see org.eclipse.bpel.ui.commands.util.IUndoHandler#undo()
*/
public void undo() {
if (DEBUG) System.out.println("undo single change"); //$NON-NLS-1$
if (fOldExtension == null) {
if (fExtensionMap.containsKey(fExtendedObject)) {
fExtensionMap.remove(fExtendedObject);
}
} else {
fExtensionMap.put(fExtendedObject, fOldExtension);
}
}
/**
* @see org.eclipse.bpel.ui.commands.util.IUndoHandler#redo()
*/
public void redo() {
if (DEBUG) System.out.println("redo single change"); //$NON-NLS-1$
if (fNewExtension == null) {
if (fExtensionMap.containsKey(fExtendedObject)) {
fExtensionMap.remove(fExtendedObject);
}
} else {
fExtensionMap.put(fExtendedObject, fNewExtension);
}
}
}
/**
* IUndoHandler to undo/redo a bulk change to the ExtensionMap (i.e. putAll or clear).
*/
class EMapMultiChangeHandler implements IUndoHandler {
ExtensionMap fExtensionMap;
Map fOldContents, fNewContents;
/**
* @param extensionMap
* @param oldContents
* @param newContents
*/
public EMapMultiChangeHandler(ExtensionMap extensionMap, Map oldContents, Map newContents) {
this.fExtensionMap = extensionMap;
this.fOldContents = oldContents;
this.fNewContents = newContents;
}
/**
* @see org.eclipse.bpel.ui.commands.util.IUndoHandler#undo()
*/
public void undo() {
if (DEBUG) System.out.println("undo multi-change"); //$NON-NLS-1$
fExtensionMap.clear();
if (fOldContents != null) fExtensionMap.putAll(fOldContents);
}
/**
* @see org.eclipse.bpel.ui.commands.util.IUndoHandler#redo()
*/
public void redo() {
if (DEBUG) System.out.println("redo multi-change"); //$NON-NLS-1$
fExtensionMap.clear();
if (fNewContents != null) {
fExtensionMap.putAll(fNewContents);
}
}
}
class ModelAutoUndoAdapter extends EContentAdapter {
protected ModelAutoUndoRecorder getAutoUndoRecorder() { return ModelAutoUndoRecorder.this; }
/**
* Handles a containment change by adding and removing the adapter as appropriate.
*/
@Override
protected void handleContainment(Notification notification) {
switch (notification.getEventType()) {
case Notification.SET:
case Notification.UNSET: {
Notifier newValue = (Notifier)notification.getNewValue();
if (newValue != null && !newValue.eAdapters().contains(this)) {
newValue.eAdapters().add(this);
}
break;
}
case Notification.ADD: {
Notifier newValue = (Notifier) notification.getNewValue();
if (newValue != null && !newValue.eAdapters().contains(this)) {
newValue.eAdapters().add(this);
}
break;
}
case Notification.ADD_MANY: {
Collection<Notifier> newValues = (Collection<Notifier>) notification.getNewValue();
for(Notifier next : newValues) {
if (!next.eAdapters().contains(this)) {
next.eAdapters().add(this);
}
}
break;
}
//if (n.getNotifier() instanceof ResourceSet) return;
}
}
/**
* note: super implementation doesn't handle overlapping targets very well
*
* @see org.eclipse.emf.ecore.util.EContentAdapter#setTarget(org.eclipse.emf.common.notify.Notifier)
*/
@Override
public void setTarget(Notifier aTarget) {
this.target = aTarget;
List<?> contents = null;
if (target instanceof EObject) {
contents = ((EObject)aTarget).eContents();
} else if (target instanceof ResourceSet) {
contents = ((ResourceSet)target).getResources();
} else if (target instanceof Resource) {
contents = ((Resource)target).getContents();
} else {
return ;
}
for (Object next : contents) {
Notifier notifier = (Notifier) next ;
if (!notifier.eAdapters().contains(this)) {
notifier.eAdapters().add(this);
}
}
}
/**
* @see org.eclipse.emf.ecore.util.EContentAdapter#notifyChanged(org.eclipse.emf.common.notify.Notification)
*/
@Override
public void notifyChanged(Notification n) {
switch (n.getEventType()) {
case Notification.ADD_MANY:
case Notification.REMOVE_MANY:
case Notification.ADD:
case Notification.REMOVE:
case Notification.SET:
case Notification.UNSET:
case Notification.MOVE:
if (!ignoreChange(n)) {
recordChange(n);
}
}
super.notifyChanged(n);
}
}
protected ModelAutoUndoAdapter modelAutoUndoAdapter = new ModelAutoUndoAdapter();
protected boolean ignoreChange(Notification n) {
Resource res = null;
if (n.getNotifier() instanceof ResourceSet) {
// don't record resources being added (usually this is due to demand-loading).
// TODO: what about resources being deleted? Is this a problem?
return true;
}
if (n.getNotifier() instanceof Resource) {
res = (Resource)n.getNotifier();
} else if (n.getNotifier() instanceof EObject) {
res = ((EObject)n.getNotifier()).eResource();
} else {
// we won't know how to undo this notification anyways, so ignore it.
// TODO: this should never occur
return true;
}
if (res != null && ignoreResources.contains(res)) {
if (VERBOSE_DEBUG) System.out.println("IGNORING -- "+ //$NON-NLS-1$
(n.isTouch()?"<t> " : "CHG: ")+BPELUtil.debug(n)); //$NON-NLS-1$ //$NON-NLS-2$
return true;
}
return false;
}
protected void recordChange(Notification n) {
if (currentChangeList == null) {
// ignore.
//System.out.println("IGNORING!! -- "+ //$NON-NLS-1$
// (n.isTouch()?"<t> " : "CHG: ")+BPELUtil.debug(n)); //$NON-NLS-1$ //$NON-NLS-2$
return;
}
// hackedy-hack hack.
if (n.getNotifier() instanceof Extension) {
// ignore (these are an implementation detail of ExtensionMapImpl).
return;
}
if (n.getNotifier() instanceof ExtensionMapImpl) {
// ignore "real" events concerning the EXTENSION_MAP__EXTENSIONS list.
// record only "semantic" events (tagged with EXTENSION_MAP__EXTENSIONS_KEY).
// those represent put() and remove() calls to the extension map.
if (n.getFeatureID(ExtensionMap.class) == ExtensionmodelPackage.EXTENSION_MAP__EXTENSIONS) {
if (DEBUG) System.out.println("ignoring impl notification: "+BPELUtil.debug(n)); //$NON-NLS-1$
return;
}
if (n instanceof ExtensionModelNotification) {
// A semantic notification just for us. Thanks Sebastian!
ExtensionModelNotification emn = (ExtensionModelNotification)n;
ExtensionMap extensionMap = (ExtensionMap)n.getNotifier();
// handle put()
if (n.getFeatureID(ExtensionMap.class) == ExtensionModelNotification.EXTENSION_MAP_PUT) {
EObject object = (EObject)emn.getArg1();
EObject oldExt = (EObject)emn.getArg2();
EObject newExt = extensionMap.get(object);
if (DEBUG) System.out.println("record PUT: "+BPELUtil.debugObject(object)+": "+ //$NON-NLS-1$ //$NON-NLS-2$
BPELUtil.debugObject(oldExt)+" ==> "+BPELUtil.debugObject(newExt)); //$NON-NLS-1$
currentChangeList.add(new EMapSingleChangeHandler(extensionMap, object, oldExt, newExt));
} else if (n.getFeatureID(ExtensionMap.class) == ExtensionModelNotification.EXTENSION_MAP_REMOVE) {
EObject object = (EObject)emn.getArg1();
EObject oldExt = (EObject)emn.getArg2();
EObject newExt = null;
if (DEBUG) System.out.println("record REMOVE: "+BPELUtil.debugObject(object)+": "+ //$NON-NLS-1$ //$NON-NLS-2$
BPELUtil.debugObject(oldExt)+" ==> "+BPELUtil.debugObject(newExt)); //$NON-NLS-1$
currentChangeList.add(new EMapSingleChangeHandler(extensionMap, object, oldExt, newExt));
} else if (n.getFeatureID(ExtensionMap.class) == ExtensionModelNotification.EXTENSION_MAP_PUTALL) {
Map oldContents = (Map)emn.getArg1();
Map newContents = new HashMap(extensionMap);
if (DEBUG) System.out.println("record PUTALL: "+oldContents.size()+" items ==> "+newContents.size()+" items"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
currentChangeList.add(new EMapMultiChangeHandler(extensionMap, oldContents, newContents));
} else if (n.getFeatureID(ExtensionMap.class) == ExtensionModelNotification.EXTENSION_MAP_CLEAR) {
Map oldContents = (Map)emn.getArg1();
Map newContents = Collections.EMPTY_MAP;
if (DEBUG) System.out.println("record CLEAR: "+oldContents.size()+" items ==> "+newContents.size()+" items"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
currentChangeList.add(new EMapMultiChangeHandler(extensionMap, oldContents, newContents));
} else {
if (DEBUG) System.out.println("WARNING: ModelAutoUndoRecorder.recordChange(): unknown event type from ExtensionMapImpl"); //$NON-NLS-1$
}
return;
}
if (DEBUG) System.out.println("ExtensionMap: "+BPELUtil.debug(n)); //$NON-NLS-1$
}
// handle all other notifications.
if (DEBUG) System.out.println((n.isTouch()?"<t> " : "CHG: ")+BPELUtil.debug(n)); //$NON-NLS-1$ //$NON-NLS-2$
currentChangeList.add(n);
}
/**
* @param resource
*/
public void startIgnoringResource(Resource resource) {
ignoreResources.add(resource);
}
/**
* @param resource
*/
public void stopIgnoringResource(Resource resource) {
ignoreResources.remove(resource);
}
/**
* @see org.eclipse.bpel.ui.commands.util.IAutoUndoRecorder#startChanges(java.util.List)
*/
@SuppressWarnings("nls")
public void startChanges( List<Object> modelRoots) {
if (VERBOSE_DEBUG) {
System.out.println("startChanges()"); //$NON-NLS-1$
}
if (currentChangeList != null) {
throw new IllegalStateException("startChages(): pending current change list");
}
currentChangeList = new ArrayList<Object>();
addModelRoots(modelRoots);
}
/**
* @see org.eclipse.bpel.ui.commands.util.IAutoUndoRecorder#finishChanges()
*/
@SuppressWarnings("nls")
public List<Object> finishChanges() {
if (currentChangeList == null) {
throw new IllegalStateException("Nothing to finish, currentChangeList is committed");
}
if (VERBOSE_DEBUG) System.out.println("finishChanges(): "+currentChangeList.size()); //$NON-NLS-1$
List<Object> result = currentChangeList;
currentChangeList = null;
clearModelRoots();
return result;
}
protected void addModelRoot(Object root) {
// TODO: TEMPORARY HACK!! This is to work around the problems with
// duplicate/overlapping adapters when we use non-resource roots.
// I don't know what the real solution is for this problem.
if (root instanceof EObject) { root = ((EObject)root).eResource(); }
if (root instanceof Notifier) {
List<Adapter> adapters = ((Notifier)root).eAdapters();
// careful: only add adapter if it hasn't already been added, or duplicate
// notifications will be recorded and things will break.
if (!adapters.contains(modelAutoUndoAdapter)) {
if (VERBOSE_DEBUG) System.out.println(" >>> Add Root: "+root); //$NON-NLS-1$
adapters.add(modelAutoUndoAdapter);
listenerRootSet.add((Notifier) root);
} else {
if (VERBOSE_DEBUG) System.out.println(" >>> Overlapping Root: "+root); //$NON-NLS-1$
}
}
}
/**
* @see org.eclipse.bpel.ui.commands.util.IAutoUndoRecorder#addModelRoots(java.util.List)
*/
public void addModelRoots(List<Object> modelRoots) {
boolean gotExtensionMap = false;
for(Object root : modelRoots) {
addModelRoot(root);
// HACK! treat the ExtensionMap as a model root
if (!gotExtensionMap) {
BPELEditor bpelEditor = ModelHelper.getBPELEditor(root);
if (bpelEditor != null) {
addModelRoot(bpelEditor.getExtensionMap());
}
gotExtensionMap = true;
}
}
}
protected void clearModelRoots() {
if (VERBOSE_DEBUG) System.out.println(" <<< Clear Model Roots"); //$NON-NLS-1$
for (Notifier notifier : listenerRootSet) {
// HACK!
while ((notifier instanceof EObject) && ((EObject)notifier).eContainer().eAdapters().contains(modelAutoUndoAdapter)) {
notifier = ((EObject)notifier).eContainer();
}
notifier.eAdapters().remove(modelAutoUndoAdapter);
}
listenerRootSet.clear();
}
/**
* @see org.eclipse.bpel.ui.commands.util.IAutoUndoRecorder#isRecordingChanges()
*/
public boolean isRecordingChanges() {
return (currentChangeList != null);
}
/**
* @see org.eclipse.bpel.ui.commands.util.IAutoUndoRecorder#insertUndoHandler(org.eclipse.bpel.ui.commands.util.IUndoHandler)
*/
public void insertUndoHandler(IUndoHandler undoHandler) {
if (currentChangeList != null) {
currentChangeList.add(undoHandler);
} else {
if (DEBUG) System.out.println("WARNING: insertUndoHandler() while not recording changes!"); //$NON-NLS-1$
}
}
protected void undoNotification(Notification n) {
List list;
// hack to work around side-effect ordering problems!
if (n.getNotifier() instanceof ExtensionImpl) {
if (DEBUG) System.out.println("ignore ExtensionImpl change: "+BPELUtil.debug(n)); //$NON-NLS-1$
return;
}
if (DEBUG) System.out.println((n.isTouch()? "<t> " : "undo: ")+BPELUtil.debug(n)); //$NON-NLS-1$ //$NON-NLS-2$
EStructuralFeature feature = (EStructuralFeature)n.getFeature();
if (n.getNotifier() instanceof EObject) {
EObject obj = (EObject)n.getNotifier();
switch (n.getEventType()) {
case Notification.ADD_MANY:
case Notification.REMOVE_MANY:
Assert.isTrue(feature.isMany());
obj.eSet(feature, n.getOldValue());
break;
case Notification.ADD:
Assert.isTrue(feature.isMany());
list = (List)obj.eGet(feature, true);
try {
list.remove(n.getPosition());
} catch (ClassCastException e) {
// it shouldn't be happening but there
// is a bug in the WSDL model
if (DEBUG) e.printStackTrace();
}
break;
case Notification.REMOVE:
Assert.isTrue(feature.isMany());
list = (List)obj.eGet(feature, true);
if (n.getPosition() == Notification.NO_INDEX) {
list.add(n.getOldValue());
} else {
list.add(n.getPosition(), n.getOldValue());
}
break;
case Notification.SET:
case Notification.UNSET:
if (feature.isMany() && n.getPosition() >= 0) {
list = (List)obj.eGet(feature, true);
list.set(n.getPosition(), n.getOldValue());
} else if (n.wasSet()) {
obj.eSet(feature, n.getOldValue());
} else {
obj.eUnset(feature);
}
break;
case Notification.MOVE:
Assert.isTrue(feature.isMany());
// TODO: does this even work?!
obj.eSet(feature, n.getOldValue());
break;
default: throw new IllegalStateException();
}
} else {
// TODO: adding and removing things from resources ??
System.err.println("undoNotification on non-EObject not implemented yet"); //$NON-NLS-1$
(new Exception()).printStackTrace(System.err);
}
}
// TODO: this stuff should be refactored/moved somewhere else.
protected void redoNotification(Notification n) {
List<Object> list;
if (DEBUG) System.out.println((n.isTouch()? "<t> " : "redo: ")+BPELUtil.debug(n)); //$NON-NLS-1$ //$NON-NLS-2$
EStructuralFeature feature = (EStructuralFeature)n.getFeature();
if (n.getNotifier() instanceof EObject) {
EObject obj = (EObject)n.getNotifier();
switch (n.getEventType()) {
case Notification.ADD_MANY:
case Notification.REMOVE_MANY:
Assert.isTrue(feature.isMany());
obj.eSet(feature, n.getNewValue());
break;
case Notification.ADD:
Assert.isTrue(feature.isMany());
list = (List)obj.eGet(feature, true);
list.add(n.getPosition(), n.getNewValue());
break;
case Notification.REMOVE:
Assert.isTrue(feature.isMany());
list = (List)obj.eGet(feature, true);
if (n.getPosition() == Notification.NO_INDEX) {
list.remove(n.getOldValue());
} else {
list.remove(n.getPosition());
}
break;
case Notification.SET:
if (feature.isMany() && n.getPosition() >= 0) {
list = (List)obj.eGet(feature, true);
list.set(n.getPosition(), n.getNewValue());
} else {
obj.eSet(feature, n.getNewValue());
}
break;
case Notification.UNSET:
obj.eUnset(feature);
break;
case Notification.MOVE:
Assert.isTrue(feature.isMany());
// TODO: does this even work?!
obj.eSet(feature, n.getNewValue());
break;
default: throw new IllegalStateException();
}
} else {
// TODO: adding and removing things from resources ??
System.err.println("redoNotification on non-EObject not implemented yet"); //$NON-NLS-1$
(new Exception()).printStackTrace(System.err);
}
}
/**
* @see org.eclipse.bpel.ui.commands.util.IAutoUndoRecorder#undo(java.util.List)
*/
public void undo (List<Object> changes) {
if (VERBOSE_DEBUG) System.out.println("UNDOING "+changes.size()+" changes"); //$NON-NLS-1$ //$NON-NLS-2$
for (int i = changes.size(); --i >= 0; ) {
Object change = changes.get(i);
if (change instanceof Notification) {
try {
undoNotification((Notification)change);
} catch (RuntimeException e) {
BPELUIPlugin.log(e);
}
} else if (change instanceof IUndoHandler) {
((IUndoHandler)change).undo();
}
}
}
/**
* @see org.eclipse.bpel.ui.commands.util.IAutoUndoRecorder#redo(java.util.List)
*/
public void redo (List<Object> changes) {
if (VERBOSE_DEBUG) System.out.println("REDOING "+changes.size()+" changes"); //$NON-NLS-1$ //$NON-NLS-2$
for(Object change : changes) {
if (change instanceof Notification) {
try {
redoNotification((Notification)change);
} catch (RuntimeException e) {
BPELUIPlugin.log(e);
}
} else if (change instanceof IUndoHandler) {
((IUndoHandler)change).redo();
}
}
}
/**
* @param notifier
* @return the undo recorder from the adapter.
*/
public static IAutoUndoRecorder getFromAdapter (Notifier notifier) {
for(Adapter a : notifier.eAdapters()) {
if (a instanceof ModelAutoUndoAdapter) {
return ((ModelAutoUndoAdapter)a).getAutoUndoRecorder();
}
}
return null;
}
}