/******************************************************************************* * 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; } }