/** * <copyright> * * Copyright (c) 2002, 2009 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 - Initial API and implementation * * </copyright> * * $Id: SetCommand.java,v 1.15 2008/04/22 19:46:16 emerks Exp $ */ package net.enilink.komma.edit.command; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.ListIterator; import java.util.Set; import org.eclipse.core.commands.ExecutionException; import org.eclipse.core.runtime.IAdaptable; import org.eclipse.core.runtime.IProgressMonitor; import net.enilink.vocab.owl.InverseFunctionalProperty; import net.enilink.vocab.owl.ObjectProperty; import net.enilink.vocab.rdf.Property; import net.enilink.komma.common.command.CommandResult; import net.enilink.komma.common.command.ExtendedCompositeCommand; import net.enilink.komma.common.command.ICommand; import net.enilink.komma.common.command.IdentityCommand; import net.enilink.komma.edit.KommaEditPlugin; import net.enilink.komma.edit.domain.AdapterFactoryEditingDomain; import net.enilink.komma.edit.domain.IEditingDomain; import net.enilink.komma.em.concepts.IProperty; import net.enilink.komma.em.concepts.IResource; import net.enilink.komma.model.IModel; import net.enilink.komma.model.IObject; import net.enilink.komma.core.IReference; /** * The set command logically acts upon an owner object to set a particular * feature to a specified value or to unset a feature. The static create methods * delegate command creation to {@link IEditingDomain#createCommand * EditingDomain.createCommand}, which may or may not result in the actual * creation of an instance of this class. * * <p> * The implementation of this class is low-level and EMF specific; it allows a * value to be set to a single-valued feature of an owner, i.e., it is * equivalent of the call * * <pre> * ((EObject) object).eSet((EStructuralFeature) feature, value); * </pre> * * or to * * <pre> * ((EObject) object).eUnset((EStructuralFeature) feature); * </pre> * * if the value is {@link #UNSET_VALUE}. * <p> * Setting a feature that is a bidirectional reference with a multiplicity-many * reverse or with a multiplicity-1 reverse that is already set (on value), is * not undoable. In this case, the SetCommand static create function will not * return an instance of this class, but instead will return a compound command * (e.g., a {@link RemoveCommand} followed by an {@link AddCommand} for the * other end of the relation) which could not be undone. * <p> * The exception to the above is when an empty list is being set to empty or * unset. Such commands are undoable and represent the only way to toggle * whether the feature is set. * <p> * When setting a containment (or container) feature, we always assume that the * object that will be contained is not already in a container, but take no * action in this class to ensure this is the case. * <p> * A set command is an {@link IOverrideableCommand}. */ public class SetCommand extends AbstractOverrideableCommand { /** * Specify this as the value in order to unset a feature. Note that this * value can be specified for a multiplicity-1 feature or for a * multiplicity-many feature with no index given. Unsetting a single value * within a list is not possible. */ public static final Object UNSET_VALUE = new Object(); /** * This creates a command to set the owner's feature to the specified value. */ public static ICommand create(IEditingDomain domain, final Object owner, Object feature, Object value) { return create(domain, owner, feature, value, CommandParameter.NO_INDEX); } /** * This creates a command to set the owner's feature to the specified value * at the specified index. */ public static ICommand create(IEditingDomain domain, final Object owner, Object property, Object value, int index) { if (owner instanceof IResource && property != null && ((IResource) owner) .hasApplicableProperty((IReference) property)) { if (((IResource) owner).getApplicableCardinality( (IReference) property).getSecond() != 1 && index == CommandParameter.NO_INDEX) { // We never directly set a multiplicity-many feature to a list // directly. Instead, we remove the old values // values, move the values that remain, and insert the new // values. If all old values are removed, we'll still // set it to an empty list, or unset it, as appropriate. // List<?> values = value == UNSET_VALUE ? Collections.EMPTY_LIST : (List<?>) value; Collection<?> oldValues = (Collection<?>) ((IResource) owner) .get((IReference) property); ExtendedCompositeCommand compound = null; compound = new ExtendedCompositeCommand( ExtendedCompositeCommand.LAST_COMMAND_ALL, LABEL, DESCRIPTION) { @Override public Collection<?> getAffectedObjects() { return Collections.singleton(owner); } }; if (!oldValues.isEmpty()) { if (!values.isEmpty()) { Set<Object> removedValues = new HashSet<Object>( oldValues); removedValues.removeAll(values); // If we aren't simply removing all the old values... // if (!removedValues.equals(oldValues)) { // If there are values to remove, append a command // for them. // if (!removedValues.isEmpty()) { compound.add(RemoveCommand.create(domain, owner, property, new HashSet<Object>( removedValues))); } // Determine the values that will remain and move // them into the right order, if necessary. // List<Object> remainingValues = new ArrayList<Object>( oldValues); remainingValues.removeAll(removedValues); int count = -1; for (Object object : values) { int position = remainingValues.indexOf(object); if (position != -1 && position != ++count) { compound.add(MoveCommand.create(domain, owner, property, object, count)); } } // Determine the values to be added and add them at // the right position. // Set<Object> addedValues = new HashSet<Object>( values); addedValues.removeAll(remainingValues); for (ListIterator<?> i = values.listIterator(); i .hasNext();) { Object object = i.next(); if (addedValues.contains(object)) { int addIndex = i.previousIndex(); if (addIndex > oldValues.size()) { addIndex = -1; } compound.add(AddCommand.create(domain, owner, property, object, addIndex)); } } return compound; } } compound.add(RemoveCommand.create(domain, owner, property, new HashSet<Object>(oldValues))); } if (!values.isEmpty()) { compound.add(AddCommand.create(domain, owner, property, values)); } else if (value == UNSET_VALUE) {// && // eReference.isUnsettable()) // { compound.add(domain.createCommand(SetCommand.class, new CommandParameter(owner, property, value))); } else if (compound.getCommandList().isEmpty()) { return IdentityCommand.INSTANCE; } return compound; } // end setting whole list // else if (resolvedProperty instanceof ObjectProperty) { // for (ObjectProperty otherEnd : ((ObjectProperty) // resolvedProperty) // .getOwlInverseOf()) { // if (((IProperty)otherEnd).isMany()) { // if (eReference.isMany()) { // // For a many-to-many association, the command can // // only // // be undoable if the value or owner is last in its // // respective list, since the undo will include an // // inverse add. So, if the value is last, but the // // owner // // is // // not, we create an undoable compound command that // // removes from the opposite end and then inserts // // the // // new // // value. // // // EList<?> list = (EList<?>) ((EObject) owner) // .eGet(eReference); // if (index == list.size() - 1) { // EObject oldValue = (EObject) list.get(index); // EList<?> oppositeList = (EList<?>) oldValue // .eGet(eOtherEnd); // if (oppositeList.get(oppositeList.size() - 1) != owner) { // CompoundCommand compound = new CompoundCommand( // CompoundCommand.LAST_COMMAND_ALL, // LABEL, DESCRIPTION) { // @Override // public Collection<?> getAffectedObjects() { // return Collections.singleton(owner); // } // }; // compound // .append(RemoveCommand.create( // domain, oldValue, // eOtherEnd, owner)); // compound.append(AddCommand.create(domain, // owner, property, value)); // return compound; // } // } // } else { // // For a 1-to-many association, doing the set as a // // remove and add from the other end will make it // // undoable. // // In particular, if there is an existing non-null // // value, we first need to remove it from the other // // end, // // so // // that it will be reinserted at the correct index // // on // // undo. // // // Object oldValue = ((EObject) owner) // .eGet(eReference); // // if (value == null || value == UNSET_VALUE) { // if (oldValue == null) { // (value == null) && // // (oldValue == null) // // A simple set/unset will suffice. // // // return domain.createCommand( // SetCommand.class, // new CommandParameter(owner, // eReference, value)); // } else { // (value == null) && (oldValue != // // null) // // Remove owner from the old value and unset // // if // // necessary. // // // Command removeCommand = RemoveCommand // .create(domain, oldValue, // eOtherEnd, Collections // .singleton(owner)); // // if (value != UNSET_VALUE // || !eReference.isUnsettable()) { // return removeCommand; // } else { // CompoundCommand compound = new CompoundCommand( // LABEL, DESCRIPTION); // compound.append(removeCommand); // compound.append(domain.createCommand( // SetCommand.class, // new CommandParameter(owner, // eReference, value))); // return compound; // } // } // } else { // ((value != null) // Command addCommand = new CommandWrapper( // AddCommand.create(domain, value, // eOtherEnd, Collections // .singleton(owner))) { // @Override // public Collection<?> getAffectedObjects() { // return Collections.singleton(owner); // } // }; // // if (oldValue == null) { // (value != null) && // // (oldValue == null) // // Add owner to new value. // // // return addCommand; // } else { // ((value != null) && (oldValue != // // null)) // // Need a compound command to remove owner // // from // // old value and add it to new value. // // // CompoundCommand compound = new CompoundCommand( // CompoundCommand.LAST_COMMAND_ALL, // LABEL, DESCRIPTION); // compound.append(RemoveCommand.create( // domain, oldValue, eOtherEnd, // Collections.singleton(owner))); // compound.append(addCommand); // return compound; // } // } // } // } else if (eOtherEnd.isContainment()) { // if (value != null && value != UNSET_VALUE) { // // For consistency, we always set 1-1 container // // relations from the container end. // // // return new CommandWrapper(SetCommand.create(domain, // value, eOtherEnd, owner)) { // @Override // public Collection<?> getResult() { // return Collections.singleton(owner); // } // // @Override // public Collection<?> getAffectedObjects() { // return Collections.singleton(owner); // } // }; // } // } else { // // For a many-to-1 or 1-to-1 association, if the // // opposite // // reference on the new value is already set to // // something, we need a compound command that first // // explicitly removes that reference, so that it will be // // restored in the undo. // // // if (value instanceof EObject) { // EObject otherEObject = (EObject) ((EObject) value) // .eGet(eOtherEnd); // if (otherEObject != null) { // CompoundCommand compound = new CompoundCommand( // CompoundCommand.LAST_COMMAND_ALL) { // @Override // public boolean canUndo() { // return true; // } // }; // if (eReference.isMany()) { // // For a many-to-1, we use // // SetCommand.create() // // to create the command to remove the // // opposite // // reference; // // a RemoveCommand on its opposite will // // actually // // result. // // // compound.append(SetCommand.create(domain, // value, eOtherEnd, null)); // } else { // // For a 1-to-1, we can directly create a // // SetCommand. // // // compound // .append(domain // .createCommand( // SetCommand.class, // eOtherEnd // .isChangeable() ? new CommandParameter( // value, // eOtherEnd, // null) // : new CommandParameter( // otherEObject, // eReference, // null))); // } // compound.append(domain.createCommand( // SetCommand.class, // new CommandParameter(owner, eReference, // value, index))); // return compound; // } // } // } // } // } } return domain.createCommand(SetCommand.class, new CommandParameter( owner, property, value, index)); } /** * This caches the label. */ protected static final String LABEL = KommaEditPlugin.INSTANCE .getString("_UI_SetCommand_label"); /** * This caches the description. */ protected static final String DESCRIPTION = KommaEditPlugin.INSTANCE .getString("_UI_SetCommand_description"); /** * This is the owner object upon which the command will act. */ protected IResource owner; /** * This is the feature of the owner object upon the command will act. */ protected IReference property; /** * If non-null, this is the list in which the command will set a value. If * null, feature is single-valued or no index was specified. */ protected Collection<Object> ownerList; /** * This is the value to be set. */ protected Object value; /** * This is the old value of the feature which must be restored during undo. */ protected Object oldValue; /** * This is the position at which the object will be set. */ protected int index; /** * This specified whether or not this command can be undone. */ protected boolean canUndo = true; /** * This is any remove commands needed to clear this many valued list or to * update the opposite properly. */ protected ICommand removeCommand; /** * This constructs a primitive command to set the owner's feature to the * specified value. */ public SetCommand(IEditingDomain domain, IResource owner, IReference feature, Object value) { super(domain, LABEL, DESCRIPTION); // Initialize all the fields from the command parameter. // this.owner = owner; this.property = feature; this.value = value; this.index = CommandParameter.NO_INDEX; } /** * This constructs a primitive command to set the owner's feature to the * specified value at the given index. */ public SetCommand(IEditingDomain domain, IResource owner, IReference feature, Object value, int index) { super(domain, LABEL, DESCRIPTION); // Initialize all the fields from the command parameter. // this.owner = owner; this.property = feature; this.value = value; this.index = index; if (index != CommandParameter.NO_INDEX) { ownerList = getOwnerList(owner, feature); } } /** * This returns the owner object upon which the command will act. */ public IResource getOwner() { return owner; } /** * This returns the feature of the owner object upon the command will act. */ public IReference getProperty() { return property; } /** * If the command will set a single value in a list, this returns the list * in which it will set; null otherwise. */ public Collection<Object> getOwnerList() { return ownerList; } /** * This returns the position at which the objects will be added. */ public int getIndex() { return index; } /** * This returns the value to be set. */ public Object getValue() { return value; } /** * This returns the old value of the feature which must be restored during * undo. */ public Object getOldValue() { return oldValue; } @Override protected boolean prepare() { boolean result = false; // If there is an owner. // if (owner != null) { if (getDomain().isReadOnly(owner) || property == null) { return false; } IProperty resolvedProperty = (IProperty) owner.getEntityManager() .find(property); // Is the feature an attribute of the owner... // if (resolvedProperty.isDomainCompatible(owner)) { // If must be of this type then. if (ownerList != null) { // Setting at an index. Make sure the index is valid, // the // type is valid, and the value isn't already in a // unique feature. Record the old value. // if (index >= 0 && ownerList instanceof List && index < ownerList.size() && resolvedProperty.isRangeCompatible(value) && (!(resolvedProperty instanceof InverseFunctionalProperty) || !ownerList .contains(value))) { oldValue = ((List<?>) ownerList).get(index); result = true; } } else if (resolvedProperty.isMany(owner)) { // If the attribute is set, record it's old value. if (owner.isPropertySet(property, true)) { oldValue = owner.get(property); } else { oldValue = UNSET_VALUE; } if (value == UNSET_VALUE) { result = true; } else if (value instanceof Collection<?>) { Collection<?> collection = (Collection<?>) value; result = true; if (!resolvedProperty.hasListRange()) { for (Object object : collection) { if (!resolvedProperty.isRangeCompatible(object)) { result = false; break; } } } } } else { // If the attribute is set, record it's old value. if (owner.isPropertySet(property, true)) { oldValue = owner.get(property); } else { oldValue = UNSET_VALUE; } result = value == null || value == UNSET_VALUE || resolvedProperty.isRangeCompatible(value); } // Make sure the container is not being put into a contained // object. if (result && value != null && resolvedProperty instanceof ObjectProperty && resolvedProperty.isContainment()) { // use seen to prevent infinite loops due to invalid usage // of komma:contains Set<IResource> seen = new HashSet<IResource>(); for (IResource container = owner; container != null; container = container .getContainer()) { if (!seen.add(container) || value.equals(container)) { result = false; break; } } } // } } return result; } @Override protected CommandResult doExecuteWithResult( IProgressMonitor progressMonitor, IAdaptable info) throws ExecutionException { property = owner.getEntityManager().find(property); // Check whether there is an opposite that needs attention. if (property instanceof ObjectProperty && !((ObjectProperty) property).getOwlInverseOf().isEmpty()) { // Because of the old factoring approach in the create method, // it might be the case that the state of the old value has // changed by the time we get here, // and in that case, we don't want to duplicate the removals in // this code. // if (oldValue instanceof Collection<?>) { oldValue = new ArrayList<Object>( (Collection<?>) owner.get(property)); } else if (oldValue != UNSET_VALUE && index == CommandParameter.NO_INDEX) { oldValue = owner.get(property); } for (Property otherProperty : ((ObjectProperty) property) .getOwlInverseOf()) { if (oldValue instanceof IResource && ((IProperty) otherProperty) .isMany((IResource) oldValue)) { // If the other end is a many, then we should remove the // owner from the old value's opposite feature so that // undo // will put it back. if (oldValue instanceof Collection<?>) { @SuppressWarnings("unchecked") Collection<IResource> oldValues = (Collection<IResource>) oldValue; if (oldValues != null && !oldValues.isEmpty()) { ExtendedCompositeCommand compoundCommand = new ExtendedCompositeCommand(); for (IResource oldValueObject : oldValues) { compoundCommand .appendIfCanExecute(new RemoveCommand( getDomain(), oldValueObject, otherProperty, owner)); } removeCommand = compoundCommand; } } } else { // If the other end is single, then we should unset the // owner from the old value's opposite feature so that // undo will put it back. if (value instanceof Collection<?>) { @SuppressWarnings("unchecked") Collection<IResource> newValues = (Collection<IResource>) value; if (!newValues.isEmpty()) { ExtendedCompositeCommand compoundCommand = new ExtendedCompositeCommand(); for (IResource newValueObject : newValues) { compoundCommand .appendIfCanExecute(new SetCommand( getDomain(), newValueObject, otherProperty, UNSET_VALUE)); IResource otherObject = (IResource) newValueObject .get(otherProperty); if (otherObject != null) { compoundCommand .appendIfCanExecute(new SetCommand( getDomain(), newValueObject, otherProperty, UNSET_VALUE)); } } removeCommand = compoundCommand; } } else if (value instanceof IResource) { IResource eObject = (IResource) value; IResource otherEObject = (IResource) eObject .get(otherProperty); if (otherEObject != null) { removeCommand = new SetCommand(getDomain(), eObject, otherProperty, UNSET_VALUE); } } } if (removeCommand != null) { if (removeCommand.canExecute()) { removeCommand.execute(progressMonitor, info); } else { removeCommand = null; } } } } // Either set or unset the feature. if (ownerList instanceof List<?>) { if (removeCommand == null || ownerList.size() > index && ((List<?>) ownerList).get(index) == oldValue) { ((List<Object>) ownerList).set(index, value); } else { ((List<Object>) ownerList).add(index, value); } } else if (value == UNSET_VALUE) { owner.removeProperty(property); } else { owner.set(property, value); } return CommandResult.newOKCommandResult(Collections.singleton(owner)); } @Override public boolean doCanUndo() { return canUndo; } @Override public Collection<?> doGetAffectedObjects() { return Collections.singleton(owner); } @Override public Collection<?> doGetAffectedResources(Object type) { if (IModel.class.equals(type) && (owner != null || ownerList != null)) { Collection<Object> affected = new HashSet<Object>( super.doGetAffectedResources(type)); if (owner instanceof IObject) { affected.add(((IObject) owner).getModel()); } if (ownerList != null) { for (Object element : ownerList) { Object object = AdapterFactoryEditingDomain.unwrap(element); if (object instanceof IObject) { affected.add(((IObject) object).getModel()); } } } return affected; } return super.doGetAffectedResources(type); } /** * This gives an abbreviated name using this object's own class' name, * without package qualification, followed by a space separated list of * <tt>field:value</tt> pairs. */ @Override public String toString() { StringBuffer result = new StringBuffer(super.toString()); result.append(" (owner: " + owner + ")"); result.append(" (feature: " + property + ")"); if (ownerList != null) { result.append(" (ownerList: " + ownerList + ")"); result.append(" (index: " + index + ")"); } result.append(" (value: " + value + ")"); result.append(" (oldValue: " + oldValue + ")"); return result.toString(); } }