All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.emf.edit.command.RemoveCommand Maven / Gradle / Ivy

/**
 * Copyright (c) 2002-2006 IBM Corporation and others.
 * All rights reserved.   This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v2.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v20.html
 * 
 * Contributors: 
 *   IBM - Initial API and implementation
 */
package org.eclipse.emf.edit.command;


import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;

import org.eclipse.emf.common.command.Command;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.edit.EMFEditPlugin;
import org.eclipse.emf.edit.domain.EditingDomain;


/**
 * The remove command logically acts upon an owner object that has a collection-type feature from which objects can be removed.
 * The static create methods delegate command creation to {@link EditingDomain#createCommand EditingDomain.createCommand},
 * which may or may not result in the actual creation of an instance of this class.
 *
 * 

* The implementation of this class is low-level and EMF specific; * it allows one or more objects to be removed from a many-valued feature of an owner. * i.e., it is almost equivalent of the call *

 *   ((EList)((EObject)owner).eGet((EStructuralFeature)feature)).removeAll((Collection)collection);
 * 
* *

* It can also be used as a near-equivalent to the call *

 *   ((EList)list).removeAll((Collection)collection);
 * 
* which is how root objects are removed from the contents of a resource. * *

* The one difference is that, while EList.removeAll(Collection) removes all values equal to a value in * the collection, this command will remove no more than one value per value in the collection. When duplicates are * allowed and present in the list, this command will first look for identical (==) values, in order, and * failing that, equal values (.equals()). * *

* Like all the low-level commands in this package, the remove command is undoable. * *

* A remove command is an {@link OverrideableCommand}. */ public class RemoveCommand extends AbstractOverrideableCommand { /** * This creates a command to remove an object. */ public static Command create(EditingDomain domain, Object value) { return create(domain, null, null, Collections.singleton(value)); } /** * This creates a command to remove a particular value from the specified feature of the owner. */ public static Command create(EditingDomain domain, Object owner, Object feature, Object value) { return create(domain, owner, feature, Collections.singleton(value)); } /** * This creates a command to remove multiple objects. */ public static Command create(final EditingDomain domain, final Collection collection) { return create(domain, null, null, collection); } /** * This creates a command to remove the value at the specified index from the specified feature of the owner. * @since 2.15 */ public static Command create(EditingDomain domain, Object owner, Object feature, int index) { return create(domain, owner, feature, Collections.singleton(CommandParameter.Indices.create(index))); } /** * This creates a command to remove the values at the specified indices from the specified feature of the owner. * @since 2.15 */ public static Command create(EditingDomain domain, Object owner, Object feature, int... indices) { return create(domain, owner, feature, Collections.singleton(CommandParameter.Indices.create(indices))); } /** * This creates a command to remove a collection of values from the specified feature of the owner. */ public static Command create(final EditingDomain domain, final Object owner, final Object feature, final Collection collection) { return domain.createCommand(RemoveCommand.class, new CommandParameter(owner, feature, collection)); } /** * This caches the label. */ protected static final String LABEL = EMFEditPlugin.INSTANCE.getString("_UI_RemoveCommand_label"); /** * This caches the description. */ protected static final String DESCRIPTION = EMFEditPlugin.INSTANCE.getString("_UI_RemoveCommand_description"); /** * This caches the description for a list-based command. */ protected static final String DESCRIPTION_FOR_LIST = EMFEditPlugin.INSTANCE.getString("_UI_RemoveCommand_description_for_list"); /** * This is the owner object upon which the command will act. * It could be null, in the case that we are dealing with an {@link org.eclipse.emf.common.util.EList}. */ protected EObject owner; /** * This is the feature of the owner object upon the command will act. * It could be null, in the case that we are dealing with an {@link org.eclipse.emf.common.util.EList}. */ protected EStructuralFeature feature; /** * This is the list from which the command will remove. */ protected EList ownerList; /** * This is the collection of objects being removed. */ protected Collection collection; /** * These are the indices at which to reinsert the removed objects during an undo so as to achieve the original list order. */ protected int[] indices; /** * The is the value returned by {@link Command#getAffectedObjects}. * The affected objects are different after an execute than after an undo, so we record it. */ protected Collection affectedObjects; /** * This constructs a primitive command to remove a particular value from the specified feature of the owner. */ public RemoveCommand(EditingDomain domain, EObject owner, EStructuralFeature feature, Object value) { this(domain, owner, feature, Collections.singleton(value)); } /** * This constructs a primitive command to remove the value at the specified index from the specified feature of the owner. * @since 2.15 */ public RemoveCommand(EditingDomain domain, EObject owner, EStructuralFeature feature, int index) { this(domain, owner, feature, Collections.singleton(CommandParameter.Indices.create(index))); } /** * This constructs a primitive command to remove the values at the specified indices from the specified feature of the owner. * @since 2.15 */ public RemoveCommand(EditingDomain domain, EObject owner, EStructuralFeature feature, int... indices) { this(domain, owner, feature, Collections.singleton(CommandParameter.Indices.create(indices))); } /** * This constructs a primitive command to remove a collection of values from the specified feature of the owner. */ public RemoveCommand(EditingDomain domain, EObject owner, EStructuralFeature feature, Collection collection) { super(domain, LABEL, DESCRIPTION); // Initialize all the fields from the command parameter. // this.owner = owner; this.feature = feature; this.collection = collection == null ? null : new ArrayList(collection); ownerList = getOwnerList(this.owner, feature); } /** * This constructs a primitive command to remove a particular value from the specified list. */ public RemoveCommand(EditingDomain domain, EList list, Object value) { this(domain, list, Collections.singleton(value)); } /** * This constructs a primitive command to remove the value at the specified index from the specified list. * @since 2.15 */ public RemoveCommand(EditingDomain domain, EList list, int index) { this(domain, list, Collections.singleton(CommandParameter.Indices.create(index))); } /** * This constructs a primitive command to remove the values at the specified indices from the specified list. * @since 2.15 */ public RemoveCommand(EditingDomain domain, EList list, int... indices) { this(domain, list, Collections.singleton(CommandParameter.Indices.create(indices))); } /** * This constructs a primitive command to remove a collection of values from the specified list. */ public RemoveCommand(EditingDomain domain, EList list, Collection collection) { super(domain, LABEL, DESCRIPTION_FOR_LIST); // Initialize all the fields from the command parameter. // this.collection = collection == null ? null : new ArrayList(collection); @SuppressWarnings("unchecked") EList untypedList = (EList)list; ownerList = untypedList; } /** * This returns the owner object upon which the command will act. * It could be null, in the case that we are dealing with an {@link org.eclipse.emf.common.util.EList}. */ public EObject getOwner() { return owner; } /** * This returns the feature of the owner object upon the command will act. * It could be null, in the case that we are dealing with an {@link org.eclipse.emf.common.util.EList}. */ public EStructuralFeature getFeature() { return feature; } /** * This returns the list from which the command will remove. */ public EList getOwnerList() { return ownerList; } /** * This returns the collection of objects being removed. */ public Collection getCollection() { return collection; } /** * These returns the indices at which to reinsert the removed objects during an undo so as to achieve the original list order. */ public int[] getIndices() { return indices; } @Override protected boolean prepare() { // This can execute if there is an owner list and a collection and the owner list contains all the objects of the collection. // boolean result = ownerList != null && collection != null && prepareCollection() && (owner == null || !domain.isReadOnly(owner.eResource())); return result; } /** * Called by {@link #prepare()} if the {@link #ownerList} and the {@link #collection} are both not {@code null}. * * @return {@code} true if the collection is valid, * i.e., either all its values are contained by the owner list, * or it contains a single {@link CommandParameter.Indices} instance all of whose {@link CommandParameter.Indices#getClass() indices} are within the bounds of the list. * * @since 2.15 */ protected boolean prepareCollection() { if (ownerList.containsAll(collection)) { return true; } else if (collection.size() != 1) { return false; } else { Object value = collection.iterator().next(); if (value instanceof CommandParameter.Indices) { int[] indices = ((CommandParameter.Indices)value).getIndices(); Arrays.sort(indices); Collection collection = new ArrayList(); int size = ownerList.size(); for (int i : indices) { if (i < 0 || i >= size) { return false; } collection.add(ownerList.get(i)); } this.indices = indices; this.collection = collection; return true; } else { return false; } } } @Override public void doExecute() { if (indices == null) { // Iterate over the owner list twice, first matching objects from the collection by identity (==), then matching // objects by value equality (.equals()). The positions of matched objects in the owner list are recorded, and // the objects are stored in the same order. The lists are then merged to form a final, in-order list of objects // and corresponding indices in ownerList. This is very important for undo to interpret the indices correctly. // Also, this yields exactly one object removed for each object in the collection, with preference given to // identity over value equality. // List identity = new ArrayList(collection.size()); int[] identityIndices = new int[collection.size()]; int i = 0; for (ListIterator ownedObjects = ownerList.listIterator(); ownedObjects.hasNext(); ) { Object ownedObject = ownedObjects.next(); // If this owned object is one from the collection... // if (containsExact(collection, ownedObject)) { // Remove the object from the collection and add it to the identity list. // removeExact(collection, ownedObject); identity.add(ownedObject); // Record the index. // identityIndices[i++] = ownedObjects.previousIndex(); } } while (i < identityIndices.length) { identityIndices[i++] = -1; } // Second pass: match by value equality. // List equality = new ArrayList(collection.size()); int[] equalityIndices = new int[collection.size()]; i = 0; for (ListIterator ownedObjects = ownerList.listIterator(); ownedObjects.hasNext(); ) { Object ownedObject = ownedObjects.next(); int index = ownedObjects.previousIndex(); // If this owned object is equal to one from the collection... // if (collection.contains(ownedObject) && !contains(identityIndices, index)) { // Remove the object from the collection and add it to the equality list. // collection.remove(ownedObject); equality.add(ownedObject); // Record the index. // equalityIndices[i++] = index; } } // Merge the lists. // merge(identity, identityIndices, equality, equalityIndices); } // Remove objects from the owner list by index, starting from the end. // for (int i = indices.length - 1; i >= 0; i--) { ownerList.remove(indices[i]); } // Update the containing map, if necessary. // updateEMap(owner, feature); // We'd like the owner selected after this remove completes. // affectedObjects = owner == null ? Collections.EMPTY_SET : Collections.singleton(owner); } /** * Returns whether the given collection contains the given target object itself (according to ==, not .equals()). */ protected boolean containsExact(Collection collection, Object target) { for (Object object : collection) { if (object == target) return true; } return false; } /** * Returns whether the given int array contains the given target value. */ protected boolean contains(int[] values, int target) { for (int i = 0, len = values.length; i < len; i++) { if (values[i] == target) return true; } return false; } /** * Removes the first occurrence of the given target object, itself, from the collection. */ protected boolean removeExact(Collection collection, Object target) { for (Iterator i = collection.iterator(); i.hasNext(); ) { if (i.next() == target) { i.remove(); return true; } } return false; } /** * Merges two sets of object lists and index arrays, such that both are ordered by increasing indices. The results * are stored as the {@link #collection} and {@link #indices}. The two input sets must already be in increasing index * order, with the corresponding object-index pairs in the same positions. */ protected void merge(List objects1, int[] indices1, List objects2, int[] indices2) { // If either list is empty, the result is simply the other. // if (objects2.isEmpty()) { collection = objects1; indices = indices1; return; } if (objects1.isEmpty()) { collection = objects2; indices = indices2; return; } // Allocate list and array for objects and indices. // int size = objects1.size() + objects2.size(); collection = new ArrayList(size); indices = new int[size]; // Index counters into indices1, indices2, and indices. // int i1 = 0; int i2 = 0; int i = 0; // Object iterators. // Iterator iter1 = objects1.iterator(); Iterator iter2 = objects2.iterator(); Object o1 = iter1.hasNext() ? iter1.next() : null; Object o2 = iter2.hasNext() ? iter2.next() : null; // Repeatedly select the lower index and corresponding object, and advance past the selected pair. // while (o1 != null && o2 != null) { if (indices1[i1] < indices2[i2]) { indices[i++] = indices1[i1++]; collection.add(o1); o1 = iter1.hasNext() ? iter1.next() : null; } else { indices[i++] = indices2[i2++]; collection.add(o2); o2 = iter2.hasNext() ? iter2.next() : null; } } // Add any remaining object-index pairs from either set. // while (o1 != null) { indices[i++] = indices1[i1++]; collection.add(o1); o1 = iter1.hasNext() ? iter1.next() : null; } while (o2 != null) { indices[i++] = indices2[i2++]; collection.add(o2); o2 = iter2.hasNext() ? iter2.next() : null; } } @Override public void doUndo() { // Note that the way they are sorted, the values of index[i++] always increase, // so the objects are added from right to left in the list. // // EATM TODO // // We could make this more efficient by grouping the adds when indices increment by one, // so that a single grouped notification would result. // int i = 0; for (Object object : collection) { ownerList.add(indices[i++], object); } // Update the containing map, if necessary. // updateEMap(owner, feature); // We'd like the collection of things added to be selected after this command completes. // affectedObjects = collection; } @Override public void doRedo() { // Remove objects from the owner list by index, starting from the end. // for (int i = indices.length - 1; i >= 0; i--) { ownerList.remove(indices[i]); } // Update the containing map, if necessary. // updateEMap(owner, feature); // We'd like the owner selected after this remove completes. // affectedObjects = owner == null ? Collections.EMPTY_SET : Collections.singleton(owner); } @Override public Collection doGetResult() { return collection; } @Override public Collection doGetAffectedObjects() { return affectedObjects; } /** * This gives an abbreviated name using this object's own class' name, without package qualification, * followed by a space separated list of field:value pairs. */ @Override public String toString() { StringBuffer result = new StringBuffer(super.toString()); result.append(" (owner: " + owner + ")"); result.append(" (feature: " + feature + ")"); result.append(" (ownerList: " + ownerList + ")"); result.append(" (collection: " + collection + ")"); result.append(" (indices: " + Arrays.toString(indices) + ")"); result.append(" (affectedObjects: " + affectedObjects + ")"); return result.toString(); } }