/*******************************************************************************
 * Copyright (c) 2004, 2016 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * 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 java.util.WeakHashMap;

import org.eclipse.core.commands.common.HandleObjectManager;
import org.eclipse.core.commands.common.NamedHandleObject;
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 {

		@Override
		public void notDefined(String commandId, NotDefinedException exception) {
			if (executionListeners != null) {
				for (final IExecutionListener object : executionListeners) {
					if (object instanceof IExecutionListenerWithChecks) {
						final IExecutionListenerWithChecks listener = (IExecutionListenerWithChecks) object;
						listener.notDefined(commandId, exception);
					}
				}
			}
		}

		@Override
		public void notEnabled(String commandId, NotEnabledException exception) {
			if (executionListeners != null) {
				for (final IExecutionListener object : executionListeners) {
					if (object instanceof IExecutionListenerWithChecks) {
						final IExecutionListenerWithChecks listener = (IExecutionListenerWithChecks) object;
						listener.notEnabled(commandId, exception);
					}
				}
			}
		}

		@Override
		public void notHandled(final String commandId,
				final NotHandledException exception) {
			if (executionListeners != null) {
				for (final IExecutionListener listener : executionListeners) {
					listener.notHandled(commandId, exception);
				}
			}
		}

		@Override
		public void postExecuteFailure(final String commandId,
				final ExecutionException exception) {
			if (executionListeners != null) {
				for (final IExecutionListener listener : executionListeners) {
					listener.postExecuteFailure(commandId, exception);
				}
			}
		}

		@Override
		public void postExecuteSuccess(final String commandId,
				final Object returnValue) {
			if (executionListeners != null) {
				for (final IExecutionListener listener : executionListeners) {
					listener.postExecuteSuccess(commandId, returnValue);
				}
			}
		}

		@Override
		public void preExecute(final String commandId,
				final ExecutionEvent event) {
			if (executionListeners != null) {
				for (final IExecutionListener listener : executionListeners) {
					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>.
	 *
	 * @since 3.2
	 */
	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(String)}. 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.
	 * <p>
	 * See also ParameterizedCommand.escape(String)
	 * </p>
	 *
	 * @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
	 * @since 3.2
	 */
	private static String unescape(final String escapedText)
			throws SerializationException {

		// defer initialization of a StringBuilder until we know we need one
		StringBuilder 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 StringBuilder(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<String, Category> 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<String> 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<String> 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 IExecutionListenerWithChecks executionListener;

	private boolean shouldCommandFireEvents = true;

	/**
	 * The collection of execution listeners. This collection is
	 * <code>null</code> if there are no listeners.
	 */
	private ListenerList<IExecutionListener> executionListeners;

	/**
	 * The help context identifiers ({@link String}) for a handler ({@link IHandler}).
	 * This map may be empty, but it is never <code>null</code>. Entries are
	 * removed if all strong references to the handler are removed.
	 *
	 * @since 3.2
	 */
	private final Map<IHandler, String> helpContextIdsByHandler = new WeakHashMap<>();

	/**
	 * 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<String, ParameterType> 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 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 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<NamedHandleObject> commandItr = handleObjectsById.values().iterator();
			while (commandItr.hasNext()) {
				final Command command = (Command) commandItr.next();
				command.addExecutionListener(executionListener);
			}

		}

		executionListeners.add(listener);
	}

	@Override
	public 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);
			}
			if (isListenerAttached()) {
				fireCommandManagerChanged(new CommandManagerEvent(this, null,
						false, false, categoryId, categoryIdAdded, true));
			}
		}
	}

	@Override
	public 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);
			}
			if (isListenerAttached()) {
				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 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 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 void fireCommandManagerChanged(final CommandManagerEvent event) {
		if (event == null) {
			throw new NullPointerException();
		}

		for (Object listener : getListeners()) {
			final ICommandManagerListener commandManagerListener = (ICommandManagerListener) listener;
			commandManagerListener.commandManagerChanged(event);
		}
	}

	/**
	 * Returns all of the commands known by this manager -- defined and
	 * undefined.
	 *
	 * @return All of the commands; may be empty, but never <code>null</code>.
	 * @since 3.2
	 */
	public Command[] getAllCommands() {
		return (Command[]) handleObjectsById.values().toArray(new Command[handleObjectsById.size()]);
	}

	/**
	 * 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 Category getCategory(final String categoryId) {
		if (categoryId == null) {
			return getCategory(AUTOGENERATED_CATEGORY_ID);
		}

		checkId(categoryId);

		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 Command getCommand(final String commandId) {
		checkId(commandId);

		Command command = (Command) handleObjectsById.get(commandId);
		if (command == null) {
			command = new Command(commandId);
			command.shouldFireEvents = shouldCommandFireEvents;
			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 Category[] getDefinedCategories() {
		final Category[] categories = new Category[definedCategoryIds.size()];
		final Iterator<String> categoryIdItr = definedCategoryIds.iterator();
		int i = 0;
		while (categoryIdItr.hasNext()) {
			String categoryId = 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>.
	 */
	@SuppressWarnings("rawtypes")
	public 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>.
	 */
	@SuppressWarnings("rawtypes")
	public 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 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
	 */
	@SuppressWarnings("rawtypes")
	public 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 ParameterType[] getDefinedParameterTypes() {
		final ParameterType[] parameterTypes = new ParameterType[definedParameterTypeIds
				.size()];
		final Iterator<String> iterator = definedParameterTypeIds.iterator();
		int i = 0;
		while (iterator.hasNext()) {
			final String parameterTypeId = iterator.next();
			parameterTypes[i++] = getParameterType(parameterTypeId);
		}
		return parameterTypes;
	}

	/**
	 * Gets the help context identifier for a particular command. The command's
	 * handler is first checked for a help context identifier. If the handler
	 * does not have a help context identifier, then the help context identifier
	 * for the command is returned. If neither has a help context identifier,
	 * then <code>null</code> is returned.
	 *
	 * @param command
	 *            The command for which the help context should be retrieved;
	 *            must not be <code>null</code>.
	 * @return The help context identifier to use for the given command; may be
	 *         <code>null</code>.
	 * @throws NotDefinedException
	 *             If the given command is not defined.
	 * @since 3.2
	 */
	public String getHelpContextId(final Command command) throws NotDefinedException {
		// Check if the command is defined.
		if (!command.isDefined()) {
			throw new NotDefinedException("The command is not defined. " //$NON-NLS-1$
					+ command.getId());
		}

		// Check the handler.
		final IHandler handler = command.getHandler();
		if (handler != null) {
			final String helpContextId = helpContextIdsByHandler.get(handler);
			if (helpContextId != null) {
				return helpContextId;
			}
		}

		// Simply return whatever the command has as a help context identifier.
		return command.getHelpContextId();
	}

	/**
	 * 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 SerializationException
	 *             if there is an error deserializing the parameters
	 * @since 3.2
	 */
	private 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<Parameterization> 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 (final IParameter parameter : parameters) {
				if (parameter.getId().equals(parameterId)) {
					paramList.add(new Parameterization(parameter, parameterValue));
					break;
				}
			}

		} while (commaPosition != -1);

		return 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 ParameterType getParameterType(final String parameterTypeId) {
		checkId(parameterTypeId);

		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
	 */
	@Override
	public 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 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 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<NamedHandleObject> 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 void setHandlersByCommandId(@SuppressWarnings("rawtypes") 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<NamedHandleObject> 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);
			}
		}
	}

	/**
	 * Sets the help context identifier to associate with a particular handler.
	 *
	 * @param handler
	 *            The handler with which to register a help context identifier;
	 *            must not be <code>null</code>.
	 * @param helpContextId
	 *            The help context identifier to register; may be
	 *            <code>null</code> if the help context identifier should be
	 *            removed.
	 * @since 3.2
	 */
	public void setHelpContextId(final IHandler handler, final String helpContextId) {
		if (handler == null) {
			throw new NullPointerException("The handler cannot be null"); //$NON-NLS-1$
		}
		if (helpContextId == null) {
			helpContextIdsByHandler.remove(handler);
		} else {
			helpContextIdsByHandler.put(handler, helpContextId);
		}
	}

	/**
	 * 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 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;

	}

	/**
	 * Fires the <code>notEnabled</code> event for
	 * <code>executionListeners</code>.
	 * <p>
	 * <b>Note:</b> This supports bridging actions to the command framework,
	 * and should not be used outside the framework.
	 * </p>
	 *
	 * @param commandId
	 *            The command id of the command about to execute, never
	 *            <code>null</code>.
	 * @param exception
	 *            The exception, never <code>null</code>.
	 * @since 3.4
	 */
	public void fireNotEnabled(String commandId, NotEnabledException exception) {
		if (executionListener != null) {
			executionListener.notEnabled(commandId, exception);
		}
	}

	/**
	 * Fires the <code>notDefined</code> event for
	 * <code>executionListeners</code>.
	 * <p>
	 * <b>Note:</b> This supports bridging actions to the command framework,
	 * and should not be used outside the framework.
	 * </p>
	 *
	 * @param commandId
	 *            The command id of the command about to execute, never
	 *            <code>null</code>.
	 * @param exception
	 *            The exception, never <code>null</code>.
	 * @since 3.4
	 */
	public void fireNotDefined(String commandId, NotDefinedException exception) {
		if (executionListener != null) {
			executionListener.notDefined(commandId, exception);
		}
	}

	/**
	 * Fires the <code>preExecute</code> event for
	 * <code>executionListeners</code>.
	 * <p>
	 * <b>Note:</b> This supports bridging actions to the command framework,
	 * and should not be used outside the framework.
	 * </p>
	 *
	 * @param commandId
	 *            The command id of the command about to execute, never
	 *            <code>null</code>.
	 * @param event
	 *            The event that triggered the command, may be <code>null</code>.
	 * @since 3.4
	 */
	public void firePreExecute(String commandId, ExecutionEvent event) {
		if (executionListener != null) {
			executionListener.preExecute(commandId, event);
		}
	}

	/**
	 * Fires the <code>postExecuteSuccess</code> event for
	 * <code>executionListeners</code>.
	 * <p>
	 * <b>Note:</b> This supports bridging actions to the command framework,
	 * and should not be used outside the framework.
	 * </p>
	 *
	 * @param commandId
	 *            The command id of the command executed, never
	 *            <code>null</code>.
	 * @param returnValue
	 *            The value returned from the command, may be <code>null</code>.
	 * @since 3.4
	 */
	public void firePostExecuteSuccess(String commandId, Object returnValue) {
		if (executionListener != null) {
			executionListener.postExecuteSuccess(commandId, returnValue);
		}
	}

	/**
	 * Fires the <code>postExecuteFailure</code> event for
	 * <code>executionListeners</code>.
	 * <p>
	 * <b>Note:</b> This supports bridging actions to the command framework,
	 * and should not be used outside the framework.
	 * </p>
	 *
	 * @param commandId
	 *            The command id of the command executed, never
	 *            <code>null</code>.
	 * @param exception
	 *            The exception, never <code>null</code>.
	 * @since 3.4
	 */
	public void firePostExecuteFailure(String commandId,
			ExecutionException exception) {
		if (executionListener != null) {
			executionListener.postExecuteFailure(commandId, exception);
		}
	}
}
