blob: 2f2e0d6bb9f423041d338fd4ae725f1735eadcd3 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 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
* James Blackburn (Broadcom Corp.) - ongoing development
* Lars Vogel <Lars.Vogel@vogella.com> - Bug 473427
*******************************************************************************/
package org.eclipse.core.internal.resources;
import java.net.URI;
import java.util.*;
import org.eclipse.core.filesystem.URIUtil;
import org.eclipse.core.internal.events.PathVariableChangeEvent;
import org.eclipse.core.internal.utils.FileUtil;
import org.eclipse.core.internal.utils.Messages;
import org.eclipse.core.resources.*;
import org.eclipse.core.runtime.*;
import org.eclipse.osgi.util.NLS;
/**
* Core's implementation of IPathVariableManager.
*/
public class PathVariableManager implements IPathVariableManager, IManager {
static final String VARIABLE_PREFIX = "pathvariable."; //$NON-NLS-1$
private Set<IPathVariableChangeListener> listeners;
private Map<IProject, Collection<IPathVariableChangeListener>> projectListeners;
private Preferences preferences;
/**
* Constructor for the class.
*/
public PathVariableManager() {
this.listeners = Collections.synchronizedSet(new HashSet<>());
this.projectListeners = Collections.synchronizedMap(new HashMap<>());
this.preferences = ResourcesPlugin.getPlugin().getPluginPreferences();
}
/**
* @see org.eclipse.core.resources.IPathVariableManager#addChangeListener(IPathVariableChangeListener)
*/
@Override
public void addChangeListener(IPathVariableChangeListener listener) {
listeners.add(listener);
}
synchronized public void addChangeListener(IPathVariableChangeListener listener, IProject project) {
Collection<IPathVariableChangeListener> list = projectListeners.get(project);
if (list == null) {
list = Collections.synchronizedSet(new HashSet<>());
projectListeners.put(project, list);
}
list.add(listener);
}
/**
* Throws a runtime exception if the given name is not valid as a path
* variable name.
*/
private void checkIsValidName(String name) throws CoreException {
IStatus status = validateName(name);
if (!status.isOK())
throw new CoreException(status);
}
/**
* Throws an exception if the given path is not valid as a path variable
* value.
*/
private void checkIsValidValue(IPath newValue) throws CoreException {
IStatus status = validateValue(newValue);
if (!status.isOK())
throw new CoreException(status);
}
/**
* Fires a property change event corresponding to a change to the
* current value of the variable with the given name.
*
* @param name the name of the variable, to be used as the variable
* in the event object
* @param value the current value of the path variable or <code>null</code> if
* the variable was deleted
* @param type one of <code>IPathVariableChangeEvent.VARIABLE_CREATED</code>,
* <code>IPathVariableChangeEvent.VARIABLE_CHANGED</code>, or
* <code>IPathVariableChangeEvent.VARIABLE_DELETED</code>
* @see IPathVariableChangeEvent
* @see IPathVariableChangeEvent#VARIABLE_CREATED
* @see IPathVariableChangeEvent#VARIABLE_CHANGED
* @see IPathVariableChangeEvent#VARIABLE_DELETED
*/
private void fireVariableChangeEvent(String name, IPath value, int type) {
fireVariableChangeEvent(this.listeners, name, value, type);
}
private void fireVariableChangeEvent(Collection<IPathVariableChangeListener> list, String name, IPath value, int type) {
if (list.isEmpty())
return;
// use a separate collection to avoid interference of simultaneous additions/removals
IPathVariableChangeListener[] listenerArray = list.toArray(new IPathVariableChangeListener[list.size()]);
final PathVariableChangeEvent pve = new PathVariableChangeEvent(this, name, value, type);
for (final IPathVariableChangeListener listener : listenerArray) {
ISafeRunnable job = new ISafeRunnable() {
@Override
public void handleException(Throwable exception) {
// already being logged in SafeRunner#run()
}
@Override
public void run() throws Exception {
listener.pathVariableChanged(pve);
}
};
SafeRunner.run(job);
}
}
public void fireVariableChangeEvent(IProject project, String name, IPath value, int type) {
Collection<IPathVariableChangeListener> list = projectListeners.get(project);
if (list != null)
fireVariableChangeEvent(list, name, value, type);
}
/**
* Return a key to use in the Preferences.
*/
private String getKeyForName(String varName) {
return VARIABLE_PREFIX + varName;
}
/**
* @see org.eclipse.core.resources.IPathVariableManager#getPathVariableNames()
*/
@Override
public String[] getPathVariableNames() {
List<String> result = new LinkedList<>();
String[] names = preferences.propertyNames();
for (String name : names) {
if (name.startsWith(VARIABLE_PREFIX)) {
String key = name.substring(VARIABLE_PREFIX.length());
// filter out names for preferences which might be valid in the
// preference store but does not have valid path variable names
// and/or values. We can get in this state if the user has
// edited the file on disk or set a preference using the prefix
// reserved to path variables (#VARIABLE_PREFIX).
// TODO: we may want to look at removing these keys from the
// preference store as a garbage collection means
if (validateName(key).isOK() && validateValue(getValue(key)).isOK())
result.add(key);
}
}
return result.toArray(new String[result.size()]);
}
/**
* Note that if a user changes the key in the preferences file to be invalid
* and then calls #getValue using that key, they will get the value back for
* that. But then if they try and call #setValue using the same key it will throw
* an exception. We may want to revisit this behaviour in the future.
*
* @see org.eclipse.core.resources.IPathVariableManager#getValue(String)
*/
@Deprecated
@Override
public IPath getValue(String varName) {
String key = getKeyForName(varName);
String value = preferences.getString(key);
return value.length() == 0 ? null : Path.fromPortableString(value);
}
/**
* @see org.eclipse.core.resources.IPathVariableManager#isDefined(String)
*/
@Override
public boolean isDefined(String varName) {
return getValue(varName) != null;
}
/**
* @see org.eclipse.core.resources.IPathVariableManager#removeChangeListener(IPathVariableChangeListener)
*/
@Override
public void removeChangeListener(IPathVariableChangeListener listener) {
listeners.remove(listener);
}
synchronized public void removeChangeListener(IPathVariableChangeListener listener, IProject project) {
Collection<IPathVariableChangeListener> list = projectListeners.get(project);
if (list != null) {
list.remove(listener);
if (list.isEmpty())
projectListeners.remove(project);
}
}
/**
* @see org.eclipse.core.resources.IPathVariableManager#resolvePath(IPath)
*/
@Deprecated
@Override
public IPath resolvePath(IPath path) {
if (path == null || path.segmentCount() == 0 || path.isAbsolute() || path.getDevice() != null)
return path;
IPath value = getValue(path.segment(0));
return value == null ? path : value.append(path.removeFirstSegments(1));
}
@Override
public URI resolveURI(URI uri) {
if (uri == null || uri.isAbsolute())
return uri;
String schemeSpecificPart = uri.getSchemeSpecificPart();
if (schemeSpecificPart == null || schemeSpecificPart.isEmpty()) {
return uri;
}
IPath raw = new Path(schemeSpecificPart);
IPath resolved = resolvePath(raw);
return raw == resolved ? uri : URIUtil.toURI(resolved);
}
/**
* @see org.eclipse.core.resources.IPathVariableManager#setValue(String, IPath)
*/
@Override
public void setValue(String varName, IPath newValue) throws CoreException {
checkIsValidName(varName);
//convert path value to canonical form
if (newValue != null && newValue.isAbsolute())
newValue = FileUtil.canonicalPath(newValue);
checkIsValidValue(newValue);
int eventType;
// read previous value and set new value atomically in order to generate the right event
synchronized (this) {
IPath currentValue = getValue(varName);
boolean variableExists = currentValue != null;
if (!variableExists && newValue == null)
return;
if (variableExists && currentValue.equals(newValue))
return;
if (newValue == null) {
preferences.setToDefault(getKeyForName(varName));
eventType = IPathVariableChangeEvent.VARIABLE_DELETED;
} else {
preferences.setValue(getKeyForName(varName), newValue.toPortableString());
eventType = variableExists ? IPathVariableChangeEvent.VARIABLE_CHANGED : IPathVariableChangeEvent.VARIABLE_CREATED;
}
}
// notify listeners from outside the synchronized block to avoid deadlocks
fireVariableChangeEvent(varName, newValue, eventType);
}
/**
* @see org.eclipse.core.internal.resources.IManager#shutdown(IProgressMonitor)
*/
@Override
public void shutdown(IProgressMonitor monitor) {
// The preferences for this plug-in are saved in the Plugin.shutdown
// method so we don't have to do it here.
}
/**
* @see org.eclipse.core.internal.resources.IManager#startup(IProgressMonitor)
*/
@Override
public void startup(IProgressMonitor monitor) {
// since we are accessing the preference store directly, we don't
// need to do any setup here.
}
/**
* @see org.eclipse.core.resources.IPathVariableManager#validateName(String)
*/
@Override
public IStatus validateName(String name) {
String message = null;
if (name.length() == 0) {
message = Messages.pathvar_length;
return new ResourceStatus(IResourceStatus.INVALID_VALUE, null, message);
}
char first = name.charAt(0);
if (!Character.isLetter(first) && first != '_') {
message = NLS.bind(Messages.pathvar_beginLetter, String.valueOf(first));
return new ResourceStatus(IResourceStatus.INVALID_VALUE, null, message);
}
for (int i = 1; i < name.length(); i++) {
char following = name.charAt(i);
if (Character.isWhitespace(following))
return new ResourceStatus(IResourceStatus.INVALID_VALUE, null, Messages.pathvar_whitespace);
if (!Character.isLetter(following) && !Character.isDigit(following) && following != '_') {
message = NLS.bind(Messages.pathvar_invalidChar, String.valueOf(following));
return new ResourceStatus(IResourceStatus.INVALID_VALUE, null, message);
}
}
return Status.OK_STATUS;
}
/**
* @see IPathVariableManager#validateValue(IPath)
*/
@Override
public IStatus validateValue(IPath value) {
if (value != null && (!value.isValidPath(value.toString()) || !value.isAbsolute())) {
String message = Messages.pathvar_invalidValue;
return new ResourceStatus(IResourceStatus.INVALID_VALUE, null, message);
}
return Status.OK_STATUS;
}
/**
* @see IPathVariableManager#convertToRelative(URI, boolean, String)
*/
@Override
public URI convertToRelative(URI path, boolean force, String variableHint) throws CoreException {
return PathVariableUtil.convertToRelative(this, path, null, false, variableHint);
}
/**
* see IPathVariableManager#getURIValue(String)
*/
@Override
public URI getURIValue(String name) {
IPath path = getValue(name);
if (path != null)
return URIUtil.toURI(path);
return null;
}
/**
* see IPathVariableManager#setURIValue(String, URI)
*/
@Override
public void setURIValue(String name, URI value) throws CoreException {
setValue(name, (value != null ? URIUtil.toPath(value) : null));
}
/**
* @see IPathVariableManager#validateValue(URI)
*/
@Override
public IStatus validateValue(URI path) {
return validateValue(path != null ? URIUtil.toPath(path) : (IPath) null);
}
public URI resolveURI(URI uri, IResource resource) {
return resolveURI(uri);
}
public String[] getPathVariableNames(IResource resource) {
return getPathVariableNames();
}
@Override
public URI getVariableRelativePathLocation(URI location) {
try {
URI result = convertToRelative(location, false, null);
if (!result.equals(location))
return result;
} catch (CoreException e) {
// handled by returning null
}
return null;
}
/**
* @see IPathVariableManager#convertToUserEditableFormat(String, boolean)
*/
@Override
public String convertToUserEditableFormat(String value, boolean locationFormat) {
return PathVariableUtil.convertToUserEditableFormatInternal(value, locationFormat);
}
@Override
public String convertFromUserEditableFormat(String userFormat, boolean locationFormat) {
return PathVariableUtil.convertFromUserEditableFormatInternal(this, userFormat, locationFormat);
}
@Override
public boolean isUserDefined(String name) {
return ProjectVariableProviderManager.getDefault().findDescriptor(name) == null;
}
}