/***************************************************************************** * Copyright (c) 2009 CEA LIST. * * 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: * Patrick Tessier (CEA LIST) patrick.tessier@cea.fr - Initial API and implementation * Remi Schnekenburger (CEA LIST) remi.schnekenburger@cea.fr - additional features * *****************************************************************************/ package org.eclipse.papyrus.uml.diagram.common.editpolicies; import static org.eclipse.papyrus.uml.diagram.common.Activator.log; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.core.commands.ExecutionException; import org.eclipse.core.runtime.IAdaptable; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.Status; import org.eclipse.emf.common.notify.Notification; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.transaction.Transaction; import org.eclipse.emf.transaction.TransactionalEditingDomain; import org.eclipse.emf.workspace.AbstractEMFOperation; import org.eclipse.gef.EditPart; import org.eclipse.gef.EditPartViewer; import org.eclipse.gef.commands.Command; import org.eclipse.gef.commands.CompoundCommand; import org.eclipse.gef.editpolicies.AbstractEditPolicy; import org.eclipse.gmf.runtime.common.core.util.StringStatics; import org.eclipse.gmf.runtime.diagram.core.commands.DeleteCommand; import org.eclipse.gmf.runtime.diagram.core.listener.DiagramEventBroker; import org.eclipse.gmf.runtime.diagram.core.listener.NotificationListener; import org.eclipse.gmf.runtime.diagram.ui.commands.ICommandProxy; import org.eclipse.gmf.runtime.diagram.ui.editparts.IGraphicalEditPart; import org.eclipse.gmf.runtime.diagram.ui.l10n.DiagramUIMessages; import org.eclipse.gmf.runtime.diagram.ui.parts.DiagramGraphicalViewer; import org.eclipse.gmf.runtime.diagram.ui.util.EditPartUtil; import org.eclipse.gmf.runtime.notation.View; import org.eclipse.papyrus.infra.core.listenerservice.IPapyrusListener; /** * Edit Policy in charge of the removal of views that are not linked to semantic * elements, when they should be linked to semantic elements. * <P> * Sometimes, {@link View}s are not linked to semantic elements, like the comment link. This link represents the annotated element feature for a * {@link Comment}, but they are not themselves connected to a semantic element. <BR/> * Thus, when this policy is created, it reads a table of integer that corresponds to the set of visual IDs, ids linked to views that are never linked * to a semantic element. * </P> * <P> * This view checks that the host edit part, usually a container edit part, has child views that are not orphaned. It listens for model notifications. * As soon as it receives a add or remove event, it checks that the child views are still valid, ie they are connected to a semantic element if they * should.<BR/> * There is then an invariant: The edit part, by the intermediate of his orphan edit policy, must always know the list of semantic parents of the * semantic elements displayed by views children of the view associated to this edit part. So it knows when a child element of this parent is deleted, * and then can react to this destruction.<BR/> * To respect this condition, the policy keeps a list of parent elements, which is updated as soon as the list receives notification about this parent * semantic elements. * </P> */ public class OrphanViewPolicy extends AbstractEditPolicy implements NotificationListener, IPapyrusListener { /** * array list of visual id that correspond to views that are not linked to * semantic elements. For example, comment links */ private ArrayList<Integer> notOrphanList = new ArrayList<Integer>(); /** the pattern that checks visual ids are valid integers */ private static Pattern digit = Pattern.compile("\\d+"); /** list of element to listen */ protected HashMap<EObject, List<View>> additionalParentToListen = new HashMap<EObject, List<View>>(); /** stores the host associated semantic element */ protected EObject hostSemanticElement; /** * Adds additional listeners to the diagram event broker. */ @SuppressWarnings("unchecked") @Override public void activate() { // retrieve the view and the element associated to the host edit part final View hostView = (View)getHost().getModel(); hostSemanticElement = hostView.getElement(); // adds listener to the event broker, listening for the view and the // semantic element associated to the host edit part getDiagramEventBroker().addNotificationListener(hostView, this); getDiagramEventBroker().addNotificationListener(hostSemanticElement, this); // retrieve the list of parent semantic element to listen in case of // modification // for each child views, checks which parent has the semantic element // associated to the view // if dif Iterator<View> it = hostView.getChildren().listIterator(); while(it.hasNext()) { View childView = it.next(); addListenerForView(childView); } // debug purpose if(log.isDebugEnabled()) { log.debug("Activate: " + prettyPrint(additionalParentToListen)); } super.activate(); } /** * Adds this edit policy as listener for changes to the specified * semanticParent * * @param semanticParent * the semantic parent to listen * @param childView * the view that requires this additional listener */ protected void addAdditionalParentToListen(EObject semanticParent, View childView) { // check if the list already contains the semantic Parent // if it already contains the parent, adds the view to the list of views // responsible of this parent // if not, it creates a new entry in the map if(additionalParentToListen.containsKey(semanticParent)) { List<View> views = additionalParentToListen.get(semanticParent); assert (views != null) : "list should not be null"; views.add(childView); // no need to add listener, it should already be done } else { // creates the list of views associated to this parent and adds it // to the additional parent to listen, with the key // "semantic parent" ArrayList<View> views = new ArrayList<View>(); views.add(childView); additionalParentToListen.put(semanticParent, views); getDiagramEventBroker().addNotificationListener(semanticParent, this); } } /** * Removes this edit policy as listener for changes to the specified * semanticParent * * @param semanticParent * the semantic parent to stop listen * @param childView * the view that does not requires this additional listener */ protected void removeAdditionalParentToListen(EObject semanticParent, View childView) { // removes the view from the list of views that requires a listener for // the semantic parent if(additionalParentToListen.containsKey(semanticParent)) { List<View> views = additionalParentToListen.get(semanticParent); assert (views != null) : "list should not be null"; views.remove(childView); if(views.isEmpty()) { additionalParentToListen.remove(semanticParent); // check this is not the parent semantic element of the host's // semantic element if(!semanticParent.equals(((View)getHost().getModel()).getElement())) { getDiagramEventBroker().removeNotificationListener(semanticParent, this); } } } } /** * {@inheritDoc} */ @Override public void deactivate() { // retrieve the view and the element associated to the host edit part final View hostView = (View)getHost().getModel(); // removes all notification listeners for the additional parent to // listen for(EObject parent : additionalParentToListen.keySet()) { getDiagramEventBroker().removeNotificationListener(parent, this); } additionalParentToListen.clear(); additionalParentToListen = null; getDiagramEventBroker().removeNotificationListener(hostView, this); getDiagramEventBroker().removeNotificationListener(hostSemanticElement, this); // removes the reference to the semantic element hostSemanticElement = null; super.deactivate(); } /** * Deletes a list of views. The views will be deleted <tt>iff</tt> their * semantic element has also been deleted. * * @param views * an iterator on a list of views. * @return <tt>true</tt> if the host editpart should be refreshed; either * one one of the supplied views was deleted or has been reparented. */ protected final boolean deleteViews(Iterator<View> views) { final CompoundCommand cc = new CompoundCommand(DiagramUIMessages.DeleteCommand_Label); while(views.hasNext()) { View view = (View)views.next(); cc.add(getDeleteViewCommand(view)); } boolean doDelete = !cc.isEmpty() && cc.canExecute(); if(doDelete) { executeCommand(cc); } return doDelete; } /** * Executes the supplied command inside an <code>unchecked action</code> * * @param cmd * command that can be executed (i.e., cmd.canExecute() == true) */ protected void executeCommand(final Command cmd) { Map<String, Boolean> options = null; EditPart ep = getHost(); boolean isActivating = true; // use the viewer to determine if we are still initializing the diagram // do not use the DiagramEditPart.isActivating since // ConnectionEditPart's // parent will not be a diagram edit part EditPartViewer viewer = ep.getViewer(); if(viewer instanceof DiagramGraphicalViewer) { isActivating = ((DiagramGraphicalViewer)viewer).isInitializing(); } if(isActivating || !EditPartUtil.isWriteTransactionInProgress((IGraphicalEditPart)getHost(), false, false)) options = Collections.singletonMap(Transaction.OPTION_UNPROTECTED, Boolean.TRUE); AbstractEMFOperation operation = new AbstractEMFOperation(((IGraphicalEditPart)getHost()).getEditingDomain(), StringStatics.BLANK, options) { protected IStatus doExecute(IProgressMonitor monitor, IAdaptable info) throws ExecutionException { cmd.execute(); return Status.OK_STATUS; } }; try { operation.execute(new NullProgressMonitor(), null); } catch (ExecutionException e) { log.error(e); } } /** * Retrieve the list of orphaned views among the specified list view * * @param viewChildrenIterator * the iterator on the list of views to check * @return the list of views that are orphaned */ protected List<View> findOrphanView(Iterator<? extends EObject> viewChildrenIterator) { ArrayList<View> orphanView = new ArrayList<View>(); while(viewChildrenIterator.hasNext()) { EObject view = viewChildrenIterator.next(); if(view instanceof View) { if(isOrphaned((View)view)) { orphanView.add((View)view); } } } return orphanView; } /** * Returns a {@link Command} to delete the supplied {@link View}. * * @param view * view to delete * @return the command that destroys the specified view */ protected Command getDeleteViewCommand(View view) { TransactionalEditingDomain editingDomain = ((IGraphicalEditPart)getHost()).getEditingDomain(); return new ICommandProxy(new DeleteCommand(editingDomain, view)); } /** * Gets the diagram event broker from the editing domain. * * @return the diagram event broker */ protected DiagramEventBroker getDiagramEventBroker() { TransactionalEditingDomain theEditingDomain = ((IGraphicalEditPart)getHost()).getEditingDomain(); if(theEditingDomain != null) { return DiagramEventBroker.getInstance(theEditingDomain); } return null; } /** * Inits the list of children that should never be attached to a semantic * element, for example comment links. * * @param notOrphanVisualID * the list of visual id of views that should never be attached * to a semantic element */ public void init(int[] notOrphanVisualID) { for(int i = 0; i < notOrphanVisualID.length; i++) { notOrphanList.add(new Integer(notOrphanVisualID[i])); } } /** * Tests if the view is orphaned, i.e. it is still attached to a semantic * element. * <P> * It checks also that it is not a view that is never attached to a semantic element (For example, comment links do not have semantic elements * attached...) * <P/> * * @param view * the view to check * @return <code>true</code> if the view is not attached to a semantic * element. <code>false</code> if it is still attached to a semantic * element or if it should never be attached to a semantic element */ protected boolean isOrphaned(View view) { String semanticHint = view.getType(); if(isInteger(semanticHint) && notOrphanList.contains(new Integer(semanticHint))) { return false; } return !view.isSetElement(); } /** * Checks if the string is an integer. * * @param s * the specified string * * @return true, if is integer */ public static boolean isInteger(String s) { boolean result = false; Matcher matcher = digit.matcher(s); if(matcher != null) { result = matcher.matches(); } return result; } /** * {@inheritDoc} */ public void notifyChanged(Notification notification) { // something has change. What ? :) // check who is responsible of notification. If this is host edit part // related semantic element, act as standard Object notifier = notification.getNotifier(); // View hostView = (View)getHost().getModel(); if(notifier.equals(hostSemanticElement)) { if(Notification.REMOVE == notification.getEventType() || Notification.SET == notification.getEventType()) { refreshViews(); } } else { // this is perhaps one of the scoped semantic parent if(notifier instanceof EObject) { if(!(notifier instanceof View)) { if(Notification.REMOVE == notification.getEventType()) { // 2 cases... remove or simple move ? // this can be checked with the view, if it is now // orphaned or not // if it is orphaned, element has to be destroyed, // remove from parent listener, etc. // if not, this was just a move => change listener using // new parent // checks also for whole hierarchy... EObject parentNotifier = (EObject)notifier; if(additionalParentToListen.containsKey(parentNotifier)) { // this should be one of the elements that are // inside the List<View> views = additionalParentToListen.get(parentNotifier); List<View> orphaned = findOrphanView(views.iterator()); // // delete all the remaining views deleteViews(orphaned.iterator()); removeListeners(orphaned); } } } else { // Notifier is a View // REMOVE or ADD are interesting events: // if remove, remove the list (perhaps) from the views // contributing to the delete action // if add, checks it does not need to be watched if(Notification.REMOVE == notification.getEventType()) { /* apex improved start */ if(notification.getOldValue() instanceof View) { /* apex improved end */ /* apex replaced if(notification.getNewValue() instanceof View) { */ View oldView = (View)notification.getOldValue(); removeListenerForView(oldView); } } else if(Notification.ADD == notification.getEventType()) { // check the parent of the associated semantic element if(notification.getNewValue() instanceof View) { View newView = (View)notification.getNewValue(); addListenerForView(newView); } } } } } } /** * Adds a listener for the specified view, if necessary. * <P> * It has to be added if the parent of the semantic element attached to the view is not the host semantic element. * </P> * * @param newView * the new view to check */ protected void addListenerForView(View newView) { // get semantic element attached to the host edit part View hostView = (View)getHost().getModel(); // get the parent of the new view. if it is the same as current parent, // does not add additional listeners if(newView.getElement() != null) { EObject semanticParent = newView.getElement().eContainer(); if(semanticParent != null) { if(!semanticParent.equals(hostSemanticElement)) { // add each parent of the semantic parent to the list of // additional listeners for(EObject parent : getElementHierarchy(semanticParent)) { addAdditionalParentToListen(parent, newView); } } } } } /** * Removes a listener for the specified view, if it exists. * * @param oldView * the old view to check */ protected void removeListenerForView(View oldView) { // create a temp list of elements to delete (iterator concurrent // modification..) Map<EObject, List<View>> parentsToDelete = new HashMap<EObject, List<View>>(); for(EObject parent : additionalParentToListen.keySet()) { List<View> parentViews = additionalParentToListen.get(parent); if(parentViews.contains(oldView)) { List<View> views = parentsToDelete.get(parent); if(views == null) { views = new ArrayList<View>(); } views.add(oldView); parentsToDelete.put(parent, views); } } } /** * Updates the listeners for the specified semantic parent */ protected void removeListeners(List<View> impactedViews) { // create a temp list of elements to delete (iterator concurrent // modification..) Map<EObject, List<View>> parentsToDelete = new HashMap<EObject, List<View>>(); // collect the elements to delete for(View view : impactedViews) { for(EObject parent : additionalParentToListen.keySet()) { List<View> parentViews = additionalParentToListen.get(parent); if(parentViews.contains(view)) { List<View> views = parentsToDelete.get(parent); if(views == null) { views = new ArrayList<View>(); } views.add(view); parentsToDelete.put(parent, views); } } } // do the job for(EObject object : parentsToDelete.keySet()) { List<View> views = parentsToDelete.get(object); for(View view : views) { removeAdditionalParentToListen(object, view); } } } /** * Returns the list of parent for the specified element * * @return the list of parent for the specified element, with the element * itself */ public List<EObject> getElementHierarchy(EObject eObject) { List<EObject> list = new ArrayList<EObject>(); EObject currentObject = eObject; while(currentObject != null) { list.add(currentObject); currentObject = currentObject.eContainer(); } return list; } /** * Refresh the child views of the host edit part * * @return an empty list? */ protected List refreshViews() { // current connection views Iterator<EObject> viewChildrenIterator = ((EObject)getHost().getModel()).eContents().iterator(); List<View> orphaned = findOrphanView(viewChildrenIterator); // // delete all the remaining views deleteViews(orphaned.iterator()); // update listeners removeListeners(orphaned); // create a view for each remaining semantic element. // FIXME why returning an empty list ? List viewDescriptors = new ArrayList(); return viewDescriptors; } /** * launch a weak synchronization. It could be useful in order to clean a * diagram by an external tool. */ public void forceRefresh() { refreshViews(); } /** * Debug method. Displays the additionalParentToListen map correclty * * @param additionalParentToListen * the map to display * @return the pretty - print string */ private String prettyPrint(Map<EObject, List<View>> additionalParentToListen) { StringBuffer buffer = new StringBuffer(); if(additionalParentToListen == null) { return "Empty listener for " + getHost(); } buffer.append("addtionnal listeners for "); buffer.append(getHost()); buffer.append("\n"); for(EObject object : additionalParentToListen.keySet()) { buffer.append("\n"); buffer.append(object); buffer.append("("); buffer.append(additionalParentToListen.get(object).size()); buffer.append("):\n"); for(View view : additionalParentToListen.get(object)) { buffer.append("- "); buffer.append(view); buffer.append("\n"); } buffer.append("\n"); } return buffer.toString(); } }