blob: 5caf8e5f38498caa6d237ea20358e8e2245fca83 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2005, 2015 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.ui.internal.services;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import org.eclipse.core.commands.Command;
import org.eclipse.core.commands.IParameter;
import org.eclipse.core.commands.Parameterization;
import org.eclipse.core.commands.ParameterizedCommand;
import org.eclipse.core.commands.common.NotDefinedException;
import org.eclipse.core.expressions.ElementHandler;
import org.eclipse.core.expressions.EvaluationResult;
import org.eclipse.core.expressions.Expression;
import org.eclipse.core.expressions.ExpressionConverter;
import org.eclipse.core.expressions.IEvaluationContext;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtensionRegistry;
import org.eclipse.core.runtime.IRegistryChangeEvent;
import org.eclipse.core.runtime.IRegistryChangeListener;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.commands.ICommandService;
import org.eclipse.ui.internal.WorkbenchPlugin;
import org.eclipse.ui.internal.registry.IWorkbenchRegistryConstants;
import org.eclipse.ui.services.IDisposable;
/**
* <p>
* A manager for items parsed from the registry. This attaches a listener to the
* registry after the first read, and monitors the registry for changes from
* that point on. When {@link #dispose()} is called, the listener is detached.
* </p>
* <p>
* This class is only intended for internal use within the
* <code>org.eclipse.ui.workbench</code> plug-in.
* </p>
*
* @since 3.2
*/
public abstract class RegistryPersistence implements IDisposable, IWorkbenchRegistryConstants {
/**
* The expression to return when there is an error. Never <code>null</code>.
*/
protected static final Expression ERROR_EXPRESSION = new Expression() {
@Override
public final EvaluationResult evaluate(final IEvaluationContext context) {
return null;
}
};
/**
* Inserts the given element into the indexed two-dimensional array in the array
* at the index. The array is grown as necessary.
*
* @param elementToAdd The element to add to the indexed array; may be
* <code>null</code>
* @param indexedArray The two-dimensional array that is indexed by element
* type; must not be <code>null</code>.
* @param index The index at which the element should be added; must be a
* valid index.
* @param currentCount The current number of items in the array at the index.
*/
protected static final void addElementToIndexedArray(final IConfigurationElement elementToAdd,
final IConfigurationElement[][] indexedArray, final int index, final int currentCount) {
final IConfigurationElement[] elements;
if (currentCount == 0) {
elements = new IConfigurationElement[1];
indexedArray[index] = elements;
} else {
if (currentCount >= indexedArray[index].length) {
final IConfigurationElement[] copy = new IConfigurationElement[indexedArray[index].length * 2];
System.arraycopy(indexedArray[index], 0, copy, 0, currentCount);
elements = copy;
indexedArray[index] = elements;
} else {
elements = indexedArray[index];
}
}
elements[currentCount] = elementToAdd;
}
/**
* Adds a warning to be logged at some later point in time.
*
* @param warningsToLog The collection of warnings to be logged; must not be
* <code>null</code>.
* @param message The mesaage to log; must not be <code>null</code>.
* @param element The element from which the warning originates; may be
* <code>null</code>.
*/
protected static final void addWarning(final List<IStatus> warningsToLog, final String message,
final IConfigurationElement element) {
addWarning(warningsToLog, message, element, null, null, null);
}
/**
* Adds a warning to be logged at some later point in time. This logs the
* identifier of the item.
*
* @param warningsToLog The collection of warnings to be logged; must not be
* <code>null</code>.
* @param message The mesaage to log; must not be <code>null</code>.
* @param element The element from which the warning originates; may be
* <code>null</code>.
* @param id The identifier of the item for which a warning is being
* logged; may be <code>null</code>.
*/
protected static final void addWarning(final List<IStatus> warningsToLog, final String message,
final IConfigurationElement element, final String id) {
addWarning(warningsToLog, message, element, id, null, null);
}
/**
* Adds a warning to be logged at some later point in time. This logs the
* identifier of the item, as well as an extra attribute.
*
* @param warningsToLog The collection of warnings to be logged; must not
* be <code>null</code>.
* @param message The message to log; must not be <code>null</code>.
* @param element The element from which the warning originates; may
* be <code>null</code>.
* @param id The identifier of the item for which a warning is
* being logged; may be <code>null</code>.
* @param extraAttributeName The name of extra attribute to be logged; may be
* <code>null</code>.
* @param extraAttributeValue The value of the extra attribute to be logged; may
* be <code>null</code>.
*/
protected static final void addWarning(final List<IStatus> warningsToLog, final String message,
final IConfigurationElement element, final String id, final String extraAttributeName,
final String extraAttributeValue) {
String statusMessage = message;
if (element != null) {
statusMessage = statusMessage + ": plug-in='" + element.getContributor().getName() + '\''; //$NON-NLS-1$
}
if (id != null) {
if (element != null) {
statusMessage = statusMessage + ',';
} else {
statusMessage = statusMessage + ':';
}
statusMessage = statusMessage + " id='" + id + '\''; //$NON-NLS-1$
}
if (extraAttributeName != null) {
if ((element != null) || (id != null)) {
statusMessage = statusMessage + ',';
} else {
statusMessage = statusMessage + ':';
}
statusMessage = statusMessage + ' ' + extraAttributeName + "='" //$NON-NLS-1$
+ extraAttributeValue + '\'';
}
final IStatus status = new Status(IStatus.WARNING, WorkbenchPlugin.PI_WORKBENCH, 0, statusMessage, null);
warningsToLog.add(status);
}
/**
* Checks that the class attribute or element exists for this element. This is
* used for executable extensions that are being read in.
*
* @param configurationElement The configuration element which should contain a
* class attribute or a class child element; must
* not be <code>null</code>.
* @param warningsToLog The list of warnings to be logged; never
* <code>null</code>.
* @param message The message to log if something goes wrong; may
* be <code>null</code>.
* @param id The identifier of the handle object; may be
* <code>null</code>.
* @return <code>true</code> if the class attribute or element exists;
* <code>false</code> otherwise.
*/
protected static final boolean checkClass(final IConfigurationElement configurationElement,
final List<IStatus> warningsToLog, final String message, final String id) {
// Check to see if we have a handler class.
if ((configurationElement.getAttribute(ATT_CLASS) == null)
&& (configurationElement.getChildren(TAG_CLASS).length == 0)) {
addWarning(warningsToLog, message, configurationElement, id);
return false;
}
return true;
}
/**
* Checks to see whether the configuration element represents a pulldown action.
* This involves reading the <code>style</code> and <code>pulldown</code>
* attributes.
*
* @param element The element to check; must not be <code>null</code>.
* @return <code>true</code> if the element is a pulldown action;
* <code>false</code> otherwise.
*/
protected static final boolean isPulldown(final IConfigurationElement element) {
final String style = readOptional(element, ATT_STYLE);
final boolean pulldown = readBoolean(element, ATT_PULLDOWN, false);
return (pulldown || STYLE_PULLDOWN.equals(style));
}
/**
* Logs any warnings in <code>warningsToLog</code>.
*
* @param warningsToLog The warnings to log; may be <code>null</code>.
* @param message The message to include in the log entry; must not be
* <code>null</code>.
*/
protected static final void logWarnings(final List<IStatus> warningsToLog, final String message) {
// If there were any warnings, then log them now.
if ((warningsToLog != null) && (!warningsToLog.isEmpty())) {
final IStatus status = new MultiStatus(WorkbenchPlugin.PI_WORKBENCH, 0,
warningsToLog.toArray(new IStatus[warningsToLog.size()]), message, null);
WorkbenchPlugin.log(status);
}
}
/**
* Reads a boolean attribute from an element.
*
* @param configurationElement The configuration element from which to read the
* attribute; must not be <code>null</code>.
* @param attribute The attribute to read; must not be
* <code>null</code>.
* @param defaultValue The default boolean value.
* @return The attribute's value; may be <code>null</code> if none.
*/
protected static final boolean readBoolean(final IConfigurationElement configurationElement, final String attribute,
final boolean defaultValue) {
final String value = configurationElement.getAttribute(attribute);
if (value == null) {
return defaultValue;
}
if (defaultValue) {
return !value.equalsIgnoreCase("false"); //$NON-NLS-1$
}
return value.equalsIgnoreCase("true"); //$NON-NLS-1$
}
/**
* Reads an optional attribute from an element. This converts zero-length
* strings into <code>null</code>.
*
* @param configurationElement The configuration element from which to read the
* attribute; must not be <code>null</code>.
* @param attribute The attribute to read; must not be
* <code>null</code>.
* @return The attribute's value; may be <code>null</code> if none.
*/
protected static final String readOptional(final IConfigurationElement configurationElement,
final String attribute) {
String value = configurationElement.getAttribute(attribute);
if ((value != null) && (value.isEmpty())) {
value = null;
}
return value;
}
/**
* Reads the parameterized command from a parent configuration element. This is
* used to read the parameter sub-elements from a key element, as well as the
* command id. Each parameter is guaranteed to be valid. If invalid parameters
* are found, then a warning status will be appended to the
* <code>warningsToLog</code> list. The command id is required, or a warning
* will be logged.
*
* @param configurationElement The configuration element from which the
* parameters should be read; must not be
* <code>null</code>.
* @param commandService The service providing commands for the workbench;
* must not be <code>null</code>.
* @param warningsToLog The list of warnings found during parsing.
* Warnings found will parsing the parameters will
* be appended to this list. This value must not be
* <code>null</code>.
* @param message The message to print if the command identifier is
* not present; must not be <code>null</code>.
* @return The array of parameters found for this configuration element;
* <code>null</code> if none can be found.
*/
protected static final ParameterizedCommand readParameterizedCommand(
final IConfigurationElement configurationElement, final ICommandService commandService,
final List<IStatus> warningsToLog, final String message, final String id) {
final String commandId = readRequired(configurationElement, ATT_COMMAND_ID, warningsToLog, message, id);
if (commandId == null) {
return null;
}
final Command command = commandService.getCommand(commandId);
return readParameters(configurationElement, warningsToLog, command);
}
/**
* Reads the parameters from a parent configuration element. This is used to
* read the parameter sub-elements from a key element. Each parameter is
* guaranteed to be valid. If invalid parameters are found, then a warning
* status will be appended to the <code>warningsToLog</code> list.
*
* @param configurationElement The configuration element from which the
* parameters should be read; must not be
* <code>null</code>.
* @param warningsToLog The list of warnings found during parsing.
* Warnings found will parsing the parameters will
* be appended to this list. This value must not be
* <code>null</code>.
* @param command The command around which the parameterization
* should be created; must not be <code>null</code>.
* @return The array of parameters found for this configuration element;
* <code>null</code> if none can be found.
*/
protected static final ParameterizedCommand readParameters(final IConfigurationElement configurationElement,
final List<IStatus> warningsToLog, final Command command) {
final IConfigurationElement[] parameterElements = configurationElement.getChildren(TAG_PARAMETER);
if ((parameterElements == null) || (parameterElements.length == 0)) {
return new ParameterizedCommand(command, null);
}
final Collection<Parameterization> parameters = new ArrayList<>();
for (final IConfigurationElement parameterElement : parameterElements) {
// Read out the id.
final String id = parameterElement.getAttribute(ATT_ID);
if ((id == null) || (id.isEmpty())) {
// The name should never be null. This is invalid.
addWarning(warningsToLog, "Parameters need a name", //$NON-NLS-1$
configurationElement);
continue;
}
// Find the parameter on the command.
IParameter parameter = null;
try {
final IParameter[] commandParameters = command.getParameters();
if (parameters != null) {
for (final IParameter currentParameter : commandParameters) {
if (Objects.equals(currentParameter.getId(), id)) {
parameter = currentParameter;
break;
}
}
}
} catch (final NotDefinedException e) {
// This should not happen.
}
if (parameter == null) {
// The name should never be null. This is invalid.
addWarning(warningsToLog, "Could not find a matching parameter", //$NON-NLS-1$
configurationElement, id);
continue;
}
// Read out the value.
final String value = parameterElement.getAttribute(ATT_VALUE);
if ((value == null) || (value.isEmpty())) {
// The name should never be null. This is invalid.
addWarning(warningsToLog, "Parameters need a value", //$NON-NLS-1$
configurationElement, id);
continue;
}
parameters.add(new Parameterization(parameter, value));
}
if (parameters.isEmpty()) {
return new ParameterizedCommand(command, null);
}
return new ParameterizedCommand(command,
parameters.toArray(new Parameterization[parameters.size()]));
}
/**
* Reads a required attribute from the configuration element.
*
* @param configurationElement The configuration element from which to read;
* must not be <code>null</code>.
* @param attribute The attribute to read; must not be
* <code>null</code>.
* @param warningsToLog The list of warnings; must not be
* <code>null</code>.
* @param message The warning message to use if the attribute is
* missing; must not be <code>null</code>.
* @return The required attribute; may be <code>null</code> if missing.
*/
protected static final String readRequired(final IConfigurationElement configurationElement, final String attribute,
final List<IStatus> warningsToLog, final String message) {
return readRequired(configurationElement, attribute, warningsToLog, message, null);
}
/**
* Reads a required attribute from the configuration element. This logs the
* identifier of the item if this required element cannot be found.
*
* @param configurationElement The configuration element from which to read;
* must not be <code>null</code>.
* @param attribute The attribute to read; must not be
* <code>null</code>.
* @param warningsToLog The list of warnings; must not be
* <code>null</code>.
* @param message The warning message to use if the attribute is
* missing; must not be <code>null</code>.
* @param id The identifier of the element for which this is a
* required attribute; may be <code>null</code>.
* @return The required attribute; may be <code>null</code> if missing.
*/
protected static final String readRequired(final IConfigurationElement configurationElement, final String attribute,
final List<IStatus> warningsToLog, final String message, final String id) {
final String value = configurationElement.getAttribute(attribute);
if ((value == null) || (value.isEmpty())) {
addWarning(warningsToLog, message, configurationElement, id);
return null;
}
return value;
}
/**
* Reads a <code>when</code> child element from the given configuration element.
* Warnings will be appended to <code>warningsToLog</code>.
*
* @param parentElement The configuration element which might have a
* <code>when</code> element as a child; never
* <code>null</code>.
* @param whenElementName The name of the when element to find; never
* <code>null</code>.
* @param id The identifier of the menu element whose
* <code>when</code> expression is being read; never
* <code>null</code>.
* @param warningsToLog The list of warnings while parsing the extension
* point; never <code>null</code>.
* @return The <code>when</code> expression for the
* <code>configurationElement</code>, if any; otherwise,
* <code>null</code>.
*/
protected static final Expression readWhenElement(final IConfigurationElement parentElement,
final String whenElementName, final String id, final List<IStatus> warningsToLog) {
// Check to see if we have an when expression.
final IConfigurationElement[] whenElements = parentElement.getChildren(whenElementName);
Expression whenExpression = null;
if (whenElements.length > 0) {
// Check if we have too many when elements.
if (whenElements.length > 1) {
// There should only be one when element
addWarning(warningsToLog, "There should only be one when element", parentElement, //$NON-NLS-1$
id, "whenElementName", //$NON-NLS-1$
whenElementName);
return ERROR_EXPRESSION;
}
final IConfigurationElement whenElement = whenElements[0];
final IConfigurationElement[] expressionElements = whenElement.getChildren();
if (expressionElements.length > 0) {
// Check if we have too many expression elements
if (expressionElements.length > 1) {
// There should only be one expression element
addWarning(warningsToLog, "There should only be one expression element", parentElement, //$NON-NLS-1$
id, "whenElementName", //$NON-NLS-1$
whenElementName);
return ERROR_EXPRESSION;
}
// Convert the activeWhen element into an expression.
final ElementHandler elementHandler = ElementHandler.getDefault();
final ExpressionConverter converter = ExpressionConverter.getDefault();
final IConfigurationElement expressionElement = expressionElements[0];
try {
whenExpression = elementHandler.create(converter, expressionElement);
} catch (final CoreException e) {
// There when expression could not be created.
addWarning(warningsToLog, "Problem creating when element", //$NON-NLS-1$
parentElement, id, "whenElementName", whenElementName); //$NON-NLS-1$
return ERROR_EXPRESSION;
}
}
}
return whenExpression;
}
/**
* The registry change listener for this class.
*/
private final IRegistryChangeListener registryChangeListener;
/**
* Whether the preference and registry change listeners have been attached yet.
*/
protected boolean registryListenerAttached = false;
/**
* Constructs a new instance of {@link RegistryPersistence}. A registry change
* listener is created.
*/
protected RegistryPersistence() {
registryChangeListener = event -> {
if (isChangeImportant(event)) {
Display.getDefault().asyncExec(this::read);
}
};
}
/**
* Detaches the registry change listener from the registry.
*/
@Override
public void dispose() {
final IExtensionRegistry registry = Platform.getExtensionRegistry();
registry.removeRegistryChangeListener(registryChangeListener);
registryListenerAttached = false;
}
/**
* Checks whether the registry change could affect this persistence class.
*
* @param event The event indicating the registry change; must not be
* <code>null</code>.
* @return <code>true</code> if the persistence instance is affected by this
* change; <code>false</code> otherwise.
*/
protected abstract boolean isChangeImportant(final IRegistryChangeEvent event);
/**
* Reads the various elements from the registry. Subclasses should extend, but
* must not override.
*/
protected void read() {
if (!registryListenerAttached) {
final IExtensionRegistry registry = Platform.getExtensionRegistry();
registry.addRegistryChangeListener(registryChangeListener);
registryListenerAttached = true;
}
}
}