/**
* Copyright (c) 2002-2010 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
*/
package org.eclipse.emf.common.command;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;
import org.eclipse.emf.common.CommonPlugin;
import org.eclipse.emf.common.util.WrappedException;
/**
* A command that comprises a sequence of subcommands.
* Derived classes can control the way results are accumulated from the individual commands;
* the default behaviour is to return the result of the last command.
*/
public class CompoundCommand extends AbstractCommand
{
/**
* The list of subcommands.
*/
protected List<Command> commandList;
/**
* When {@link #resultIndex} is set to this,
* {@link #getResult} and {@link #getAffectedObjects} are delegated to the last command, if any, in the list.
*/
public static final int LAST_COMMAND_ALL = Integer.MIN_VALUE;
/**
* When {@link #resultIndex} is set to this,
* {@link #getResult} and {@link #getAffectedObjects}
* are set to the result of merging the corresponding collection of each command in the list.
*/
public static final int MERGE_COMMAND_ALL = Integer.MIN_VALUE - 1;
/**
* The index of the command whose result and affected objects are forwarded.
* Negative values have special meaning, as defined by the static constants.
* A value of -1 indicates that the last command in the list should be used.
* We could have more special behaviours implemented for other negative values.
*/
protected int resultIndex = MERGE_COMMAND_ALL;
/**
* Creates an empty instance.
*/
public CompoundCommand()
{
super();
commandList = new ArrayList<Command>();
}
/**
* Creates an instance with the given label.
* @param label the label.
*/
public CompoundCommand(String label)
{
super(label);
commandList = new ArrayList<Command>();
}
/**
* Creates an instance with the given label and description.
* @param label the label.
* @param description the description.
*/
public CompoundCommand(String label, String description)
{
super(label, description);
commandList = new ArrayList<Command>();
}
/**
* Creates an instance with the given list.
* @param commandList the list of commands.
*/
public CompoundCommand(List<Command> commandList)
{
super();
this.commandList = commandList;
}
/**
* Creates instance with the given label and list.
* @param label the label.
* @param commandList the list of commands.
*/
public CompoundCommand(String label, List<Command> commandList)
{
super(label);
this.commandList = commandList;
}
/**
* Creates an instance with the given label, description, and list.
* @param label the label.
* @param description the description.
* @param commandList the list of commands.
*/
public CompoundCommand(String label, String description, List<Command> commandList)
{
super(label, description);
this.commandList = commandList;
}
/**
* Creates an empty instance with the given result index.
* @param resultIndex the {@link #resultIndex}.
*/
public CompoundCommand(int resultIndex)
{
super();
this.resultIndex = resultIndex;
commandList = new ArrayList<Command>();
}
/**
* Creates an instance with the given result index and label.
* @param resultIndex the {@link #resultIndex}.
* @param label the label.
*/
public CompoundCommand(int resultIndex, String label)
{
super(label);
this.resultIndex = resultIndex;
commandList = new ArrayList<Command>();
}
/**
* Creates an instance with the given result index, label, and description.
* @param resultIndex the {@link #resultIndex}.
* @param label the label.
* @param description the description.
*/
public CompoundCommand(int resultIndex, String label, String description)
{
super(label, description);
this.resultIndex = resultIndex;
commandList = new ArrayList<Command>();
}
/**
* Creates an instance with the given result index and list.
* @param resultIndex the {@link #resultIndex}.
* @param commandList the list of commands.
*/
public CompoundCommand(int resultIndex, List<Command> commandList)
{
super();
this.resultIndex = resultIndex;
this.commandList = commandList;
}
/**
* Creates an instance with the given resultIndex, label, and list.
* @param resultIndex the {@link #resultIndex}.
* @param label the label.
* @param commandList the list of commands.
*/
public CompoundCommand(int resultIndex, String label, List<Command> commandList)
{
super(label);
this.resultIndex = resultIndex;
this.commandList = commandList;
}
/**
* Creates an instance with the given result index, label, description, and list.
* @param resultIndex the {@link #resultIndex}.
* @param label the label.
* @param description the description.
* @param commandList the list of commands.
*/
public CompoundCommand(int resultIndex, String label, String description, List<Command> commandList)
{
super(label, description);
this.resultIndex = resultIndex;
this.commandList = commandList;
}
/**
* Returns whether there are commands in the list.
* @return whether there are commands in the list.
*/
public boolean isEmpty()
{
return commandList.isEmpty();
}
/**
* Returns an unmodifiable view of the commands in the list.
* @return an unmodifiable view of the commands in the list.
*/
public List<Command> getCommandList()
{
return Collections.unmodifiableList(commandList);
}
/**
* Returns the index of the command whose result and affected objects are forwarded.
* Negative values have special meaning, as defined by the static constants.
* @return the index of the command whose result and affected objects are forwarded.
* @see #LAST_COMMAND_ALL
* @see #MERGE_COMMAND_ALL
*/
public int getResultIndex()
{
return resultIndex;
}
/**
* Returns whether all the commands can execute so that {@link #isExecutable} can be cached.
* An empty command list causes <code>false</code> to be returned.
* @return whether all the commands can execute.
*/
@Override
protected boolean prepare()
{
if (commandList.isEmpty())
{
return false;
}
else
{
for (Command command : commandList)
{
if (!command.canExecute())
{
return false;
}
}
return true;
}
}
/**
* Calls {@link Command#execute} for each command in the list.
*/
public void execute()
{
for (ListIterator<Command> commands = commandList.listIterator(); commands.hasNext(); )
{
try
{
Command command = commands.next();
command.execute();
}
catch (RuntimeException exception)
{
// Skip over the command that threw the exception.
//
commands.previous();
try
{
// Iterate back over the executed commands to undo them.
//
while (commands.hasPrevious())
{
Command command = commands.previous();
if (command.canUndo())
{
command.undo();
}
else
{
break;
}
}
}
catch (RuntimeException nestedException)
{
CommonPlugin.INSTANCE.log
(new WrappedException
(CommonPlugin.INSTANCE.getString("_UI_IgnoreException_exception"), nestedException).fillInStackTrace());
}
throw exception;
}
}
}
/**
* Returns <code>false</code> if any of the commands return <code>false</code> for {@link Command#canUndo}.
* @return <code>false</code> if any of the commands return <code>false</code> for <code>canUndo</code>.
*/
@Override
public boolean canUndo()
{
for (Command command : commandList)
{
if (!command.canUndo())
{
return false;
}
}
return true;
}
/**
* Calls {@link Command#undo} for each command in the list, in reverse order.
*/
@Override
public void undo()
{
for (ListIterator<Command> commands = commandList.listIterator(commandList.size()); commands.hasPrevious(); )
{
try
{
Command command = commands.previous();
command.undo();
}
catch (RuntimeException exception)
{
// Skip over the command that threw the exception.
//
commands.next();
try
{
// Iterate forward over the undone commands to redo them.
//
while (commands.hasNext())
{
Command command = commands.next();
command.redo();
}
}
catch (RuntimeException nestedException)
{
CommonPlugin.INSTANCE.log
(new WrappedException
(CommonPlugin.INSTANCE.getString("_UI_IgnoreException_exception"), nestedException).fillInStackTrace());
}
throw exception;
}
}
}
/**
* Calls {@link Command#redo} for each command in the list.
*/
public void redo()
{
for (ListIterator<Command> commands = commandList.listIterator(); commands.hasNext(); )
{
try
{
Command command = commands.next();
command.redo();
}
catch (RuntimeException exception)
{
// Skip over the command that threw the exception.
//
commands.previous();
try
{
// Iterate back over the executed commands to undo them.
//
while (commands.hasPrevious())
{
Command command = commands.previous();
command.undo();
}
}
catch (RuntimeException nestedException)
{
CommonPlugin.INSTANCE.log
(new WrappedException
(CommonPlugin.INSTANCE.getString("_UI_IgnoreException_exception"), nestedException).fillInStackTrace());
}
throw exception;
}
}
}
/**
* Determines the result by composing the results of the commands in the list;
* this is affected by the setting of {@link #resultIndex}.
* @return the result.
*/
@Override
public Collection<?> getResult()
{
if (commandList.isEmpty())
{
return Collections.EMPTY_LIST;
}
else if (resultIndex == LAST_COMMAND_ALL)
{
return commandList.get(commandList.size() - 1).getResult();
}
else if (resultIndex == MERGE_COMMAND_ALL)
{
return getMergedResultCollection();
}
else if (resultIndex < commandList.size())
{
return commandList.get(resultIndex).getResult();
}
else
{
return Collections.EMPTY_LIST;
}
}
/**
* Returns the merged collection of all command results.
* @return the merged collection of all command results.
*/
protected Collection<?> getMergedResultCollection()
{
Collection<Object> result = new ArrayList<Object>();
for (Command command : commandList)
{
result.addAll(command.getResult());
}
return result;
}
/**
* Determines the affected objects by composing the affected objects of the commands in the list;
* this is affected by the setting of {@link #resultIndex}.
* @return the affected objects.
*/
@Override
public Collection<?> getAffectedObjects()
{
if (commandList.isEmpty())
{
return Collections.EMPTY_LIST;
}
else if (resultIndex == LAST_COMMAND_ALL)
{
return commandList.get(commandList.size() - 1).getAffectedObjects();
}
else if (resultIndex == MERGE_COMMAND_ALL)
{
return getMergedAffectedObjectsCollection();
}
else if (resultIndex < commandList.size())
{
return commandList.get(resultIndex).getAffectedObjects();
}
else
{
return Collections.EMPTY_LIST;
}
}
/**
* Returns the merged collection of all command affected objects.
* @return the merged collection of all command affected objects.
*/
protected Collection<?> getMergedAffectedObjectsCollection()
{
Collection<Object> result = new ArrayList<Object>();
for (Command command : commandList)
{
result.addAll(command.getAffectedObjects());
}
return result;
}
/**
* Determines the label by composing the labels of the commands in the list;
* this is affected by the setting of {@link #resultIndex}.
* @return the label.
*/
@Override
public String getLabel()
{
if (label != null)
{
return label;
}
else if (commandList.isEmpty())
{
return CommonPlugin.INSTANCE.getString("_UI_CompoundCommand_label");
}
else if (resultIndex == LAST_COMMAND_ALL || resultIndex == MERGE_COMMAND_ALL)
{
return commandList.get(commandList.size() - 1).getLabel();
}
else if (resultIndex < commandList.size())
{
return commandList.get(resultIndex).getLabel();
}
else
{
return CommonPlugin.INSTANCE.getString("_UI_CompoundCommand_label");
}
}
/**
* Determines the description by composing the descriptions of the commands in the list;
* this is affected by the setting of {@link #resultIndex}.
* @return the description.
*/
@Override
public String getDescription()
{
if (description != null)
{
return description;
}
else if (commandList.isEmpty())
{
return CommonPlugin.INSTANCE.getString("_UI_CompoundCommand_description");
}
else if (resultIndex == LAST_COMMAND_ALL || resultIndex == MERGE_COMMAND_ALL)
{
return commandList.get(commandList.size() - 1).getDescription();
}
else if (resultIndex < commandList.size())
{
return commandList.get(resultIndex).getDescription();
}
else
{
return CommonPlugin.INSTANCE.getString("_UI_CompoundCommand_description");
}
}
/**
* Adds a command to this compound command's list of commands.
* @param command the command to append.
*/
public void append(Command command)
{
if (isPrepared)
{
throw new IllegalStateException("The command is already prepared");
}
if (command != null)
{
commandList.add(command);
}
}
/**
* Checks if the command can execute;
* if so, it is executed, appended to the list, and true is returned,
* if not, it is just disposed and false is returned.
* A typical use for this is to execute commands created during the execution of another command, e.g.,
* <pre>
* class MyCommand extends CommandBase
* {
* protected Command subcommand;
*
* //...
*
* public void execute()
* {
* // ...
* Compound subcommands = new CompoundCommand();
* subcommands.appendAndExecute(new AddCommand(...));
* if (condition) subcommands.appendAndExecute(new AddCommand(...));
* subcommand = subcommands.unwrap();
* }
*
* public void undo()
* {
* // ...
* subcommand.undo();
* }
*
* public void redo()
* {
* // ...
* subcommand.redo();
* }
*
* public void dispose()
* {
* // ...
* if (subcommand != null)
* {
* subcommand.dispose();
* }
* }
* }
* </pre>
* Another use is in an execute override of compound command itself:
* <pre>
* class MyCommand extends CompoundCommand
* {
* public void execute()
* {
* // ...
* appendAndExecute(new AddCommand(...));
* if (condition) appendAndExecute(new AddCommand(...));
* }
* }
* </pre>
* Note that appending commands will modify what getResult and getAffectedObjects return,
* so you may want to set the resultIndex flag.
* @param command the command.
* @return whether the command was successfully executed and appended.
*/
public boolean appendAndExecute(Command command)
{
if (command != null)
{
if (!isPrepared)
{
if (commandList.isEmpty())
{
isPrepared = true;
isExecutable = true;
}
else
{
isExecutable = prepare();
isPrepared = true;
if (isExecutable)
{
execute();
}
}
}
if (command.canExecute())
{
try
{
command.execute();
commandList.add(command);
return true;
}
catch (RuntimeException exception)
{
CommonPlugin.INSTANCE.log
(new WrappedException
(CommonPlugin.INSTANCE.getString("_UI_IgnoreException_exception"), exception).fillInStackTrace());
}
}
command.dispose();
}
return false;
}
/**
* Adds a command to this compound command's the list of commands and returns <code>true</code>,
* if <code>command.{@link org.eclipse.emf.common.command.Command#canExecute() canExecute()}</code> returns true;
* otherwise, it simply calls <code>command.{@link org.eclipse.emf.common.command.Command#dispose() dispose()}</code>
* and returns <code>false</code>.
* @param command the command.
* @return whether the command was executed and appended.
*/
public boolean appendIfCanExecute(Command command)
{
if (command == null)
{
return false;
}
else if (command.canExecute())
{
commandList.add(command);
return true;
}
else
{
command.dispose();
return false;
}
}
/**
* Calls {@link Command#dispose} for each command in the list.
*/
@Override
public void dispose()
{
for (Command command : commandList)
{
command.dispose();
}
}
/**
* Returns one of three things:
* {@link org.eclipse.emf.common.command.UnexecutableCommand#INSTANCE}, if there are no commands,
* the one command, if there is exactly one command,
* or <code>this</code>, if there are multiple commands;
* this command is {@link #dispose}d in the first two cases.
* You should only unwrap a compound command if you created it for that purpose, e.g.,
* <pre>
* CompoundCommand subcommands = new CompoundCommand();
* subcommands.append(x);
* if (condition) subcommands.append(y);
* Command result = subcommands.unwrap();
* </pre>
* is a good way to create an efficient accumulated result.
* @return the unwrapped command.
*/
public Command unwrap()
{
switch (commandList.size())
{
case 0:
{
dispose();
return UnexecutableCommand.INSTANCE;
}
case 1:
{
Command result = commandList.remove(0);
dispose();
return result;
}
default:
{
return this;
}
}
}
@Override
public String toString()
{
StringBuffer result = new StringBuffer(super.toString());
result.append(" (commandList: #" + commandList.size() + ")");
result.append(" (resultIndex: " + resultIndex + ")");
return result.toString();
}
}