blob: df1b449705e618b8bd135d318ba59eb4b1fae5a7 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2004, 2005 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.core.commands;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import org.eclipse.core.commands.common.HandleObjectManager;
import org.eclipse.core.commands.common.NotDefinedException;
import org.eclipse.core.runtime.ListenerList;
/**
* <p>
* A central repository for commands -- both in the defined and undefined
* states. Commands can be created and retrieved using this manager. It is
* possible to listen to changes in the collection of commands by attaching a
* listener to the manager.
* </p>
*
* @see CommandManager#getCommand(String)
* @since 3.1
*/
public final class CommandManager extends HandleObjectManager implements
ICategoryListener, ICommandListener, IParameterTypeListener {
/**
* A listener that forwards incoming execution events to execution listeners
* on this manager. The execution events will come from any command on this
* manager.
*
* @since 3.1
*/
private final class ExecutionListener implements
IExecutionListenerWithChecks {
public void notDefined(String commandId, NotDefinedException exception) {
if (executionListeners != null) {
final Object[] listeners = executionListeners.getListeners();
for (int i = 0; i < listeners.length; i++) {
final Object object = listeners[i];
if (object instanceof IExecutionListenerWithChecks) {
final IExecutionListenerWithChecks listener = (IExecutionListenerWithChecks) object;
listener.notDefined(commandId, exception);
}
}
}
}
public void notEnabled(String commandId, NotEnabledException exception) {
if (executionListeners != null) {
final Object[] listeners = executionListeners.getListeners();
for (int i = 0; i < listeners.length; i++) {
final Object object = listeners[i];
if (object instanceof IExecutionListenerWithChecks) {
final IExecutionListenerWithChecks listener = (IExecutionListenerWithChecks) object;
listener.notEnabled(commandId, exception);
}
}
}
}
public final void notHandled(final String commandId,
final NotHandledException exception) {
if (executionListeners != null) {
final Object[] listeners = executionListeners.getListeners();
for (int i = 0; i < listeners.length; i++) {
final Object object = listeners[i];
if (object instanceof IExecutionListener) {
final IExecutionListener listener = (IExecutionListener) object;
listener.notHandled(commandId, exception);
}
}
}
}
public final void postExecuteFailure(final String commandId,
final ExecutionException exception) {
if (executionListeners != null) {
final Object[] listeners = executionListeners.getListeners();
for (int i = 0; i < listeners.length; i++) {
final Object object = listeners[i];
if (object instanceof IExecutionListener) {
final IExecutionListener listener = (IExecutionListener) object;
listener.postExecuteFailure(commandId, exception);
}
}
}
}
public final void postExecuteSuccess(final String commandId,
final Object returnValue) {
if (executionListeners != null) {
final Object[] listeners = executionListeners.getListeners();
for (int i = 0; i < listeners.length; i++) {
final Object object = listeners[i];
if (object instanceof IExecutionListener) {
final IExecutionListener listener = (IExecutionListener) object;
listener.postExecuteSuccess(commandId, returnValue);
}
}
}
}
public final void preExecute(final String commandId,
final ExecutionEvent event) {
if (executionListeners != null) {
final Object[] listeners = executionListeners.getListeners();
for (int i = 0; i < listeners.length; i++) {
final Object object = listeners[i];
if (object instanceof IExecutionListener) {
final IExecutionListener listener = (IExecutionListener) object;
listener.preExecute(commandId, event);
}
}
}
}
}
/**
* The identifier of the category in which all auto-generated commands will
* appear. This value must never be <code>null</code>.
*/
public static final String AUTOGENERATED_CATEGORY_ID = "org.eclipse.core.commands.categories.autogenerated"; //$NON-NLS-1$
/**
* The escape character to use for serialization and deserialization of
* parameterized commands.
*/
static final char ESCAPE_CHAR = '%';
/**
* The character that separates a parameter id from its value.
*/
static final char ID_VALUE_CHAR = '=';
/**
* The character that indicates the end of a list of parameters.
*/
static final char PARAMETER_END_CHAR = ')';
/**
* The character that separators parameters from each other.
*/
static final char PARAMETER_SEPARATOR_CHAR = ',';
/**
* The character that indicates the start of a list of parameters.
*/
static final char PARAMETER_START_CHAR = '(';
/**
* Unescapes special characters in the command id, parameter ids and
* parameter values for {@link #deserialize()}. The special characters
* {@link #PARAMETER_START_CHAR}, {@link #PARAMETER_END_CHAR},
* {@link #ID_VALUE_CHAR}, {@link #PARAMETER_SEPARATOR_CHAR} and
* {@link #ESCAPE_CHAR} are escaped by prepending an {@link #ESCAPE_CHAR}
* character.
*
* @param escapedText
* a <code>String</code> that may contain escaped special
* characters for command serialization.
* @return a <code>String</code> representing <code>escapedText</code>
* with any escaped characters replaced by their literal values
* @throws SerializationException
* if <code>escapedText</code> contains an invalid escape
* sequence
* @see ParameterizedCommand#escape(String)
* @since 3.2
*/
private static final String unescape(final String escapedText)
throws SerializationException {
// defer initialization of a StringBuffer until we know we need one
StringBuffer buffer = null;
for (int i = 0; i < escapedText.length(); i++) {
char c = escapedText.charAt(i);
if (c != ESCAPE_CHAR) {
// normal unescaped character
if (buffer != null)
buffer.append(c);
} else {
if (buffer == null)
buffer = new StringBuffer(escapedText.substring(0, i));
if (++i < escapedText.length()) {
c = escapedText.charAt(i);
switch (c) {
case PARAMETER_START_CHAR:
case PARAMETER_END_CHAR:
case ID_VALUE_CHAR:
case PARAMETER_SEPARATOR_CHAR:
case ESCAPE_CHAR:
buffer.append(c);
break;
default:
throw new SerializationException(
"Invalid character '" + c + "' in escape sequence"); //$NON-NLS-1$ //$NON-NLS-2$
}
} else {
throw new SerializationException(
"Unexpected termination of escape sequence"); //$NON-NLS-1$
}
}
}
if (buffer == null)
return escapedText;
return buffer.toString();
}
/**
* The map of category identifiers (<code>String</code>) to categories (
* <code>Category</code>). This collection may be empty, but it is never
* <code>null</code>.
*/
private final Map categoriesById = new HashMap();
/**
* The set of identifiers for those categories that are defined. This value
* may be empty, but it is never <code>null</code>.
*/
private final Set definedCategoryIds = new HashSet();
/**
* The set of identifiers for those command parameter types that are
* defined. This value may be empty, but it is never <code>null</code>.
*
* @since 3.2
*/
private final Set definedParameterTypeIds = new HashSet();
/**
* The execution listener for this command manager. This just forwards
* events from commands controlled by this manager to listeners on this
* manager.
*/
private IExecutionListener executionListener = null;
/**
* The collection of execution listeners. This collection is
* <code>null</code> if there are no listeners.
*/
private ListenerList executionListeners = null;
/**
* The map of parameter type identifiers (<code>String</code>) to
* parameter types ( <code>ParameterType</code>). This collection may be
* empty, but it is never <code>null</code>.
*
* @since 3.2
*/
private final Map parameterTypesById = new HashMap();
/**
* Adds a listener to this command manager. The listener will be notified
* when the set of defined commands changes. This can be used to track the
* global appearance and disappearance of commands.
*
* @param listener
* The listener to attach; must not be <code>null</code>.
*/
public final void addCommandManagerListener(
final ICommandManagerListener listener) {
addListenerObject(listener);
}
/**
* Adds an execution listener to this manager. This listener will be
* notified if any of the commands controlled by this manager execute. This
* can be used to support macros and instrumentation of commands.
*
* @param listener
* The listener to attach; must not be <code>null</code>.
*/
public final void addExecutionListener(final IExecutionListener listener) {
if (listener == null) {
throw new NullPointerException(
"Cannot add a null execution listener"); //$NON-NLS-1$
}
if (executionListeners == null) {
executionListeners = new ListenerList(ListenerList.IDENTITY);
// Add an execution listener to every command.
executionListener = new ExecutionListener();
final Iterator commandItr = handleObjectsById.values().iterator();
while (commandItr.hasNext()) {
final Command command = (Command) commandItr.next();
command.addExecutionListener(executionListener);
}
}
executionListeners.add(listener);
}
/*
* (non-Javadoc)
*
* @see org.eclipse.core.commands.ICategoryListener#categoryChanged(org.eclipse.core.commands.CategoryEvent)
*/
public final void categoryChanged(CategoryEvent categoryEvent) {
if (categoryEvent.isDefinedChanged()) {
final Category category = categoryEvent.getCategory();
final String categoryId = category.getId();
final boolean categoryIdAdded = category.isDefined();
if (categoryIdAdded) {
definedCategoryIds.add(categoryId);
} else {
definedCategoryIds.remove(categoryId);
}
fireCommandManagerChanged(new CommandManagerEvent(this, null,
false, false, categoryId, categoryIdAdded, true));
}
}
/*
* (non-Javadoc)
*
* @see org.eclipse.commands.ICommandListener#commandChanged(org.eclipse.commands.CommandEvent)
*/
public final void commandChanged(final CommandEvent commandEvent) {
if (commandEvent.isDefinedChanged()) {
final Command command = commandEvent.getCommand();
final String commandId = command.getId();
final boolean commandIdAdded = command.isDefined();
if (commandIdAdded) {
definedHandleObjects.add(command);
} else {
definedHandleObjects.remove(command);
}
fireCommandManagerChanged(new CommandManagerEvent(this, commandId,
commandIdAdded, true, null, false, false));
}
}
/**
* Sets the name and description of the category for uncategorized commands.
* This is the category that will be returned if
* {@link #getCategory(String)} is called with <code>null</code>.
*
* @param name
* The name of the category for uncategorized commands; must not
* be <code>null</code>.
* @param description
* The description of the category for uncategorized commands;
* may be <code>null</code>.
* @since 3.2
*/
public final void defineUncategorizedCategory(final String name,
final String description) {
final Category category = getCategory(AUTOGENERATED_CATEGORY_ID);
category.define(name, description);
}
/**
* <p>
* Returns a {@link ParameterizedCommand} with a command and
* parameterizations as specified in the provided
* <code>serializedParameterizedCommand</code> string. The
* <code>serializedParameterizedCommand</code> must use the format
* returned by {@link ParameterizedCommand#serialize()} and described in the
* Javadoc for that method.
* </p>
* <p>
* If a parameter id encoded in the
* <code>serializedParameterizedCommand</code> does not exist in the
* encoded command, that parameter id and value are ignored. A given
* parameter id should not be used more than once in
* <code>serializedParameterizedCommand</code>. This will not result in
* an exception, but in this case the value of the parameter when the
* command is executed is unspecified.
* </p>
* <p>
* This method will never return <code>null</code>, however it may throw
* an exception if there is a problem processing the serialization string or
* the encoded command is undefined.
* </p>
*
* @param serializedParameterizedCommand
* a string representing a command id and parameter ids and
* values; must not be <code>null</code>
* @return a {@link ParameterizedCommand} with the command and
* parameterizations encoded in the
* <code>serializedParameterizedCommand</code>; never
* <code>null</code>.
* @throws NotDefinedException
* if the command indicated in
* <code>serializedParameterizedCommand</code> is not defined
* @throws SerializationException
* if there is an error deserializing
* <code>serializedParameterizedCommand</code>
* @see ParameterizedCommand#serialize()
* @since 3.2
*/
public final ParameterizedCommand deserialize(
final String serializedParameterizedCommand)
throws NotDefinedException, SerializationException {
final int lparenPosition = unescapedIndexOf(
serializedParameterizedCommand, PARAMETER_START_CHAR);
final String commandIdEscaped;
final String serializedParameters;
if (lparenPosition == -1) {
commandIdEscaped = serializedParameterizedCommand;
serializedParameters = null;
} else {
commandIdEscaped = serializedParameterizedCommand.substring(0,
lparenPosition);
if (serializedParameterizedCommand
.charAt(serializedParameterizedCommand.length() - 1) != PARAMETER_END_CHAR) {
throw new SerializationException(
"Parentheses must be balanced in serialized ParameterizedCommand"); //$NON-NLS-1$
}
serializedParameters = serializedParameterizedCommand.substring(
lparenPosition + 1, // skip PARAMETER_START_CHAR
serializedParameterizedCommand.length() - 1); // skip
// PARAMETER_END_CHAR
}
final String commandId = unescape(commandIdEscaped);
final Command command = getCommand(commandId);
final IParameter[] parameters = command.getParameters();
final Parameterization[] parameterizations = getParameterizations(
serializedParameters, parameters);
return new ParameterizedCommand(command, parameterizations);
}
/**
* Notifies all of the listeners to this manager that the set of defined
* command identifiers has changed.
*
* @param event
* The event to send to all of the listeners; must not be
* <code>null</code>.
*/
private final void fireCommandManagerChanged(final CommandManagerEvent event) {
if (event == null)
throw new NullPointerException();
final Object[] listeners = getListeners();
for (int i = 0; i < listeners.length; i++) {
final ICommandManagerListener listener = (ICommandManagerListener) listeners[i];
listener.commandManagerChanged(event);
}
}
/**
* Gets the category with the given identifier. If no such category
* currently exists, then the category will be created (but be undefined).
*
* @param categoryId
* The identifier to find; must not be <code>null</code>. If
* the category is <code>null</code>, then a category suitable
* for uncategorized items is defined and returned.
* @return The category with the given identifier; this value will never be
* <code>null</code>, but it might be undefined.
* @see Category
*/
public final Category getCategory(final String categoryId) {
if (categoryId == null) {
return getCategory(AUTOGENERATED_CATEGORY_ID);
}
checkId(categoryId);
Category category = (Category) categoriesById.get(categoryId);
if (category == null) {
category = new Category(categoryId);
categoriesById.put(categoryId, category);
category.addCategoryListener(this);
}
return category;
}
/**
* Gets the command with the given identifier. If no such command currently
* exists, then the command will be created (but will be undefined).
*
* @param commandId
* The identifier to find; must not be <code>null</code> and
* must not be zero-length.
* @return The command with the given identifier; this value will never be
* <code>null</code>, but it might be undefined.
* @see Command
*/
public final Command getCommand(final String commandId) {
checkId(commandId);
Command command = (Command) handleObjectsById.get(commandId);
if (command == null) {
command = new Command(commandId);
handleObjectsById.put(commandId, command);
command.addCommandListener(this);
if (executionListener != null) {
command.addExecutionListener(executionListener);
}
}
return command;
}
/**
* Returns the categories that are defined.
*
* @return The defined categories; this value may be empty, but it is never
* <code>null</code>.
* @since 3.2
*/
public final Category[] getDefinedCategories() {
final Category[] categories = new Category[definedCategoryIds.size()];
final Iterator categoryIdItr = definedCategoryIds.iterator();
int i = 0;
while (categoryIdItr.hasNext()) {
String categoryId = (String) categoryIdItr.next();
categories[i++] = getCategory(categoryId);
}
return categories;
}
/**
* Returns the set of identifiers for those category that are defined.
*
* @return The set of defined category identifiers; this value may be empty,
* but it is never <code>null</code>.
*/
public final Set getDefinedCategoryIds() {
return Collections.unmodifiableSet(definedCategoryIds);
}
/**
* Returns the set of identifiers for those commands that are defined.
*
* @return The set of defined command identifiers; this value may be empty,
* but it is never <code>null</code>.
*/
public final Set getDefinedCommandIds() {
return getDefinedHandleObjectIds();
}
/**
* Returns the commands that are defined.
*
* @return The defined commands; this value may be empty, but it is never
* <code>null</code>.
* @since 3.2
*/
public final Command[] getDefinedCommands() {
return (Command[]) definedHandleObjects
.toArray(new Command[definedHandleObjects.size()]);
}
/**
* Returns the set of identifiers for those parameter types that are
* defined.
*
* @return The set of defined command parameter type identifiers; this value
* may be empty, but it is never <code>null</code>.
* @since 3.2
*/
public final Set getDefinedParameterTypeIds() {
return Collections.unmodifiableSet(definedParameterTypeIds);
}
/**
* Returns the command parameter types that are defined.
*
* @return The defined command parameter types; this value may be empty, but
* it is never <code>null</code>.
* @since 3.2
*/
public final ParameterType[] getDefinedParameterTypes() {
final ParameterType[] parameterTypes = new ParameterType[definedParameterTypeIds
.size()];
final Iterator iterator = definedParameterTypeIds.iterator();
int i = 0;
while (iterator.hasNext()) {
final String parameterTypeId = (String) iterator.next();
parameterTypes[i++] = getParameterType(parameterTypeId);
}
return parameterTypes;
}
/**
* Returns an array of parameterizations for the provided command by
* deriving the parameter ids and values from the provided
* <code>serializedParameters</code> string.
*
* @param serializedParameters
* a String encoding parameter ids and values; must not be
* <code>null</code>.
* @param parameters
* array of parameters of the command being deserialized; may be
* <code>null</code>.
* @return an array of parameterizations; may be <code>null</code>.
* @throws NotDefinedException
* if the command is not defined
* @throws SerializationException
* if there is an error deserializing the parameters
* @since 3.2
*/
private final Parameterization[] getParameterizations(
String serializedParameters, final IParameter[] parameters)
throws SerializationException {
if (serializedParameters == null
|| (serializedParameters.length() == 0))
return null;
if ((parameters == null) || (parameters.length == 0))
return null;
final ArrayList paramList = new ArrayList();
int commaPosition; // split off each param by looking for ','
do {
commaPosition = unescapedIndexOf(serializedParameters, ',');
final String idEqualsValue;
if (commaPosition == -1) {
// no more parameters after this
idEqualsValue = serializedParameters;
} else {
// take the first parameter...
idEqualsValue = serializedParameters
.substring(0, commaPosition);
// ... and put the rest back into serializedParameters
serializedParameters = serializedParameters
.substring(commaPosition + 1);
}
final int equalsPosition = unescapedIndexOf(idEqualsValue, '=');
final String parameterId;
final String parameterValue;
if (equalsPosition == -1) {
// missing values are null
parameterId = unescape(idEqualsValue);
parameterValue = null;
} else {
parameterId = unescape(idEqualsValue.substring(0,
equalsPosition));
parameterValue = unescape(idEqualsValue
.substring(equalsPosition + 1));
}
for (int i = 0; i < parameters.length; i++) {
final IParameter parameter = parameters[i];
if (parameter.getId().equals(parameterId)) {
paramList.add(new Parameterization(parameter,
parameterValue));
break;
}
}
} while (commaPosition != -1);
return (Parameterization[]) paramList
.toArray(new Parameterization[paramList.size()]);
}
/**
* Gets the command {@link ParameterType} with the given identifier. If no
* such command parameter type currently exists, then the command parameter
* type will be created (but will be undefined).
*
* @param parameterTypeId
* The identifier to find; must not be <code>null</code> and
* must not be zero-length.
* @return The {@link ParameterType} with the given identifier; this value
* will never be <code>null</code>, but it might be undefined.
* @since 3.2
*/
public final ParameterType getParameterType(final String parameterTypeId) {
checkId(parameterTypeId);
ParameterType parameterType = (ParameterType) parameterTypesById
.get(parameterTypeId);
if (parameterType == null) {
parameterType = new ParameterType(parameterTypeId);
parameterTypesById.put(parameterTypeId, parameterType);
parameterType.addListener(this);
}
return parameterType;
}
/**
* {@inheritDoc}
*
* @since 3.2
*/
public final void parameterTypeChanged(
final ParameterTypeEvent parameterTypeEvent) {
if (parameterTypeEvent.isDefinedChanged()) {
final ParameterType parameterType = parameterTypeEvent
.getParameterType();
final String parameterTypeId = parameterType.getId();
final boolean parameterTypeIdAdded = parameterType.isDefined();
if (parameterTypeIdAdded) {
definedParameterTypeIds.add(parameterTypeId);
} else {
definedParameterTypeIds.remove(parameterTypeId);
}
fireCommandManagerChanged(new CommandManagerEvent(this,
parameterTypeId, parameterTypeIdAdded, true));
}
}
/**
* Removes a listener from this command manager.
*
* @param listener
* The listener to be removed; must not be <code>null</code>.
*/
public final void removeCommandManagerListener(
final ICommandManagerListener listener) {
removeListenerObject(listener);
}
/**
* Removes an execution listener from this command manager.
*
* @param listener
* The listener to be removed; must not be <code>null</code>.
*/
public final void removeExecutionListener(final IExecutionListener listener) {
if (listener == null) {
throw new NullPointerException("Cannot remove a null listener"); //$NON-NLS-1$
}
if (executionListeners == null) {
return;
}
executionListeners.remove(listener);
if (executionListeners.isEmpty()) {
executionListeners = null;
// Remove the execution listener to every command.
final Iterator commandItr = handleObjectsById.values().iterator();
while (commandItr.hasNext()) {
final Command command = (Command) commandItr.next();
command.removeExecutionListener(executionListener);
}
executionListener = null;
}
}
/**
* Block updates all of the handlers for all of the commands. If the handler
* is <code>null</code> or the command id does not exist in the map, then
* the command becomes unhandled. Otherwise, the handler is set to the
* corresponding value in the map.
*
* @param handlersByCommandId
* A map of command identifiers (<code>String</code>) to
* handlers (<code>IHandler</code>). This map may be
* <code>null</code> if all handlers should be cleared.
* Similarly, if the map is empty, then all commands will become
* unhandled.
*/
public final void setHandlersByCommandId(final Map handlersByCommandId) {
// Make that all the reference commands are created.
final Iterator commandIdItr = handlersByCommandId.keySet().iterator();
while (commandIdItr.hasNext()) {
getCommand((String) commandIdItr.next());
}
// Now, set-up the handlers on all of the existing commands.
final Iterator commandItr = handleObjectsById.values().iterator();
while (commandItr.hasNext()) {
final Command command = (Command) commandItr.next();
final String commandId = command.getId();
final Object value = handlersByCommandId.get(commandId);
if (value instanceof IHandler) {
command.setHandler((IHandler) value);
} else {
command.setHandler(null);
}
}
}
/**
* Searches for the index of a <code>char</code> in a <code>String</code>
* but disregards characters prefixed with the {@link #ESCAPE_CHAR} escape
* character. This is used by {@link #deserialize(String)} and
* {@link #getParameterizations(String, IParameter[])} to parse the
* serialized parameterized command string.
*
* @param escapedText
* the string to search for the index of <code>ch</code> in
* @param ch
* a character to search for in <code>escapedText</code>
* @return the index of the first unescaped occurrence of the character in
* <code>escapedText</code>, or <code>-1</code> if the
* character does not occur unescaped.
* @see String#indexOf(int)
*/
private final int unescapedIndexOf(final String escapedText, final char ch) {
int pos = escapedText.indexOf(ch);
// first char can't be escaped
if (pos == 0)
return pos;
while (pos != -1) {
// look back for the escape character
if (escapedText.charAt(pos - 1) != ESCAPE_CHAR)
return pos;
// scan for the next instance of ch
pos = escapedText.indexOf(ch, pos + 1);
}
return pos;
}
}