| /******************************************************************************* |
| * Copyright (c) 2004, 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 |
| * Julian Chen - fix for bug #92572, jclRM |
| * Jan-Ove Weichel (janove.weichel@vogella.com) - bug 474359 |
| * InterSystems Corporation - bug 444188 |
| *******************************************************************************/ |
| package org.eclipse.core.internal.preferences; |
| |
| import java.io.*; |
| import java.util.*; |
| import org.eclipse.core.internal.runtime.RuntimeLog; |
| import org.eclipse.core.runtime.*; |
| import org.eclipse.core.runtime.preferences.*; |
| import org.eclipse.osgi.util.NLS; |
| import org.osgi.service.prefs.BackingStoreException; |
| import org.osgi.service.prefs.Preferences; |
| |
| /** |
| * Represents a node in the Eclipse preference node hierarchy. This class |
| * is used as a default implementation/super class for those nodes which |
| * belong to scopes which are contributed by the Platform. |
| * |
| * Implementation notes: |
| * |
| * - For thread safety, we always synchronize on <tt>writeLock</tt> when writing |
| * the children or properties fields. Must ensure we don't synchronize when calling |
| * client code such as listeners. |
| * |
| * @since 3.0 |
| */ |
| public class EclipsePreferences implements IEclipsePreferences, IScope { |
| |
| public static final String DEFAULT_PREFERENCES_DIRNAME = ".settings"; //$NON-NLS-1$ |
| public static final String PREFS_FILE_EXTENSION = "prefs"; //$NON-NLS-1$ |
| protected static final IEclipsePreferences[] EMPTY_NODE_ARRAY = new IEclipsePreferences[0]; |
| protected static final String[] EMPTY_STRING_ARRAY = new String[0]; |
| private static final String FALSE = "false"; //$NON-NLS-1$ |
| private static final String TRUE = "true"; //$NON-NLS-1$ |
| protected static final String VERSION_KEY = "eclipse.preferences.version"; //$NON-NLS-1$ |
| protected static final String VERSION_VALUE = "1"; //$NON-NLS-1$ |
| protected static final String PATH_SEPARATOR = String.valueOf(IPath.SEPARATOR); |
| protected static final String DOUBLE_SLASH = "//"; //$NON-NLS-1$ |
| protected static final String EMPTY_STRING = ""; //$NON-NLS-1$ |
| |
| private String cachedPath; |
| protected ImmutableMap properties = ImmutableMap.EMPTY; |
| protected Map<String, Object> children; |
| /** |
| * Protects write access to properties and children. |
| */ |
| private final Object childAndPropertyLock = new Object(); |
| protected boolean dirty = false; |
| protected boolean loading = false; |
| protected final String name; |
| // the parent of an EclipsePreference node is always an EclipsePreference node. (or null) |
| protected final EclipsePreferences parent; |
| protected boolean removed = false; |
| private final ListenerList<INodeChangeListener> nodeChangeListeners = new ListenerList<>(); |
| private final ListenerList<IPreferenceChangeListener> preferenceChangeListeners = new ListenerList<>(); |
| private ScopeDescriptor descriptor; |
| |
| public static boolean DEBUG_PREFERENCE_GENERAL = false; |
| public static boolean DEBUG_PREFERENCE_SET = false; |
| public static boolean DEBUG_PREFERENCE_GET = false; |
| |
| protected final static String debugPluginName = "org.eclipse.equinox.preferences"; //$NON-NLS-1$ |
| |
| static { |
| DEBUG_PREFERENCE_GENERAL = PreferencesOSGiUtils.getDefault().getBooleanDebugOption(debugPluginName + "/general", false); //$NON-NLS-1$ |
| DEBUG_PREFERENCE_SET = PreferencesOSGiUtils.getDefault().getBooleanDebugOption(debugPluginName + "/set", false); //$NON-NLS-1$ |
| DEBUG_PREFERENCE_GET = PreferencesOSGiUtils.getDefault().getBooleanDebugOption(debugPluginName + "/get", false); //$NON-NLS-1$ |
| } |
| |
| public EclipsePreferences() { |
| this(null, null); |
| } |
| |
| protected EclipsePreferences(EclipsePreferences parent, String name) { |
| super(); |
| this.parent = parent; |
| this.name = name; |
| this.cachedPath = null; // make sure the cached path is cleared after setting the parent |
| } |
| |
| |
| @Override |
| public String absolutePath() { |
| if (cachedPath == null) { |
| if (parent == null) |
| cachedPath = PATH_SEPARATOR; |
| else { |
| String parentPath = parent.absolutePath(); |
| // if the parent is the root then we don't have to add a separator |
| // between the parent path and our path |
| if (parentPath.length() == 1) |
| cachedPath = parentPath + name(); |
| else |
| cachedPath = parentPath + PATH_SEPARATOR + name(); |
| } |
| } |
| return cachedPath; |
| } |
| |
| @Override |
| public void accept(IPreferenceNodeVisitor visitor) throws BackingStoreException { |
| if (!visitor.visit(this)) |
| return; |
| IEclipsePreferences[] toVisit = getChildren(true); |
| for (IEclipsePreferences p : toVisit) { |
| p.accept(visitor); |
| } |
| } |
| |
| protected IEclipsePreferences addChild(String childName, IEclipsePreferences child) { |
| //Thread safety: synchronize method to protect modification of children field |
| synchronized (childAndPropertyLock) { |
| if (children == null) |
| children = Collections.synchronizedMap(new HashMap<String, Object>()); |
| children.put(childName, child == null ? (Object) childName : child); |
| return child; |
| } |
| } |
| |
| |
| @Override |
| public void addNodeChangeListener(INodeChangeListener listener) { |
| checkRemoved(); |
| nodeChangeListeners.add(listener); |
| if (DEBUG_PREFERENCE_GENERAL) |
| PrefsMessages.message("Added preference node change listener: " + listener + " to: " + absolutePath()); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| |
| |
| @Override |
| public void addPreferenceChangeListener(IPreferenceChangeListener listener) { |
| checkRemoved(); |
| preferenceChangeListeners.add(listener); |
| if (DEBUG_PREFERENCE_GENERAL) |
| PrefsMessages.message("Added preference property change listener: " + listener + " to: " + absolutePath()); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| |
| private IEclipsePreferences calculateRoot() { |
| IEclipsePreferences result = this; |
| while (result.parent() != null) |
| result = (IEclipsePreferences) result.parent(); |
| return result; |
| } |
| |
| /* |
| * Convenience method for throwing an exception when methods |
| * are called on a removed node. |
| */ |
| protected void checkRemoved() { |
| if (removed) |
| throw new IllegalStateException(NLS.bind(PrefsMessages.preferences_removedNode, name)); |
| } |
| |
| |
| @Override |
| public String[] childrenNames() throws BackingStoreException { |
| // illegal state if this node has been removed |
| checkRemoved(); |
| String[] internal = internalChildNames(); |
| // if we are != 0 then we have already been initialized |
| if (internal.length != 0) |
| return internal; |
| // we only want to query the descriptor for the child names if |
| // this node is the scope root |
| if (descriptor != null && getSegmentCount(absolutePath()) == 1) |
| return descriptor.childrenNames(absolutePath()); |
| return internal; |
| } |
| |
| protected String[] internalChildNames() { |
| synchronized (childAndPropertyLock) { |
| if (children == null || children.size() == 0) |
| return EMPTY_STRING_ARRAY; |
| return children.keySet().toArray(EMPTY_STRING_ARRAY); |
| } |
| } |
| |
| |
| @Override |
| public void clear() { |
| // illegal state if this node has been removed |
| checkRemoved(); |
| // call each one separately (instead of Properties.clear) so |
| // clients get change notification |
| String[] keys; |
| synchronized (childAndPropertyLock) { |
| keys = properties.keys(); |
| } |
| //don't synchronize remove call because it calls listeners |
| for (String key : keys) { |
| remove(key); |
| } |
| makeDirty(); |
| } |
| |
| protected String[] computeChildren(IPath root) { |
| if (root == null) |
| return EMPTY_STRING_ARRAY; |
| IPath dir = root.append(DEFAULT_PREFERENCES_DIRNAME); |
| final ArrayList<String> result = new ArrayList<>(); |
| final String extension = '.' + PREFS_FILE_EXTENSION; |
| File file = dir.toFile(); |
| File[] totalFiles = file.listFiles(); |
| if (totalFiles != null) { |
| for (File totalFile : totalFiles) { |
| if (totalFile.isFile()) { |
| String filename = totalFile.getName(); |
| if (filename.endsWith(extension)) { |
| String shortName = filename.substring(0, filename.length() - extension.length()); |
| result.add(shortName); |
| } |
| } |
| } |
| } |
| return result.toArray(EMPTY_STRING_ARRAY); |
| } |
| |
| protected IPath computeLocation(IPath root, String qualifier) { |
| return root == null ? null : root.append(DEFAULT_PREFERENCES_DIRNAME).append(qualifier).addFileExtension(PREFS_FILE_EXTENSION); |
| } |
| |
| /* |
| * Version 1 (current version) |
| * path/key=value |
| */ |
| protected static void convertFromProperties(EclipsePreferences node, Properties table, boolean notify) { |
| String version = table.getProperty(VERSION_KEY); |
| if (version == null || !VERSION_VALUE.equals(version)) { |
| // ignore for now |
| } |
| table.remove(VERSION_KEY); |
| for (Iterator<?> i = table.keySet().iterator(); i.hasNext();) { |
| String fullKey = (String) i.next(); |
| String value = table.getProperty(fullKey); |
| if (value != null) { |
| String[] splitPath = decodePath(fullKey); |
| String path = splitPath[0]; |
| path = makeRelative(path); |
| String key = splitPath[1]; |
| if (DEBUG_PREFERENCE_SET) |
| PrefsMessages.message("Setting preference: " + path + '/' + key + '=' + value); //$NON-NLS-1$ |
| //use internal methods to avoid notifying listeners |
| EclipsePreferences childNode = (EclipsePreferences) node.internalNode(path, false, null); |
| String oldValue = childNode.internalPut(key, value); |
| // notify listeners if applicable |
| if (notify && !value.equals(oldValue)) |
| childNode.firePreferenceEvent(key, oldValue, value); |
| } |
| } |
| PreferencesService.getDefault().shareStrings(); |
| } |
| |
| /* |
| * Helper method to persist a Properties object to the filesystem. We use this |
| * helper so we can remove the date/timestamp that Properties#store always |
| * puts in the file. |
| */ |
| protected static void write(Properties properties, IPath location) throws BackingStoreException { |
| // create the parent directories if they don't exist |
| File parentFile = location.toFile().getParentFile(); |
| if (parentFile == null) |
| return; |
| parentFile.mkdirs(); |
| |
| OutputStream output = null; |
| try { |
| output = new SafeFileOutputStream(new File(location.toOSString())); |
| output.write(removeTimestampFromTable(properties).getBytes("UTF-8")); //$NON-NLS-1$ |
| output.flush(); |
| } catch (IOException e) { |
| String message = NLS.bind(PrefsMessages.preferences_saveException, location); |
| log(new Status(IStatus.ERROR, PrefsMessages.OWNER_NAME, IStatus.ERROR, message, e)); |
| throw new BackingStoreException(message, e); |
| } finally { |
| if (output != null) |
| try { |
| output.close(); |
| } catch (IOException e) { |
| // ignore |
| } |
| } |
| } |
| |
| protected static String removeTimestampFromTable(Properties properties) throws IOException { |
| // store the properties in a string and then skip the first line (date/timestamp) |
| ByteArrayOutputStream output = new ByteArrayOutputStream(); |
| try { |
| properties.store(output, null); |
| } finally { |
| output.close(); |
| } |
| String string = output.toString("UTF-8"); //$NON-NLS-1$ |
| String separator = System.getProperty("line.separator"); //$NON-NLS-1$ |
| return string.substring(string.indexOf(separator) + separator.length()); |
| } |
| |
| /* |
| * Helper method to convert this node to a Properties file suitable |
| * for persistence. |
| */ |
| protected Properties convertToProperties(Properties result, String prefix) throws BackingStoreException { |
| // add the key/value pairs from this node |
| boolean addSeparator = prefix.length() != 0; |
| //thread safety: copy reference in case of concurrent change |
| ImmutableMap temp; |
| synchronized (childAndPropertyLock) { |
| temp = properties; |
| } |
| String[] keys = temp.keys(); |
| for (int i = 0, imax = keys.length; i < imax; i++) { |
| String value = temp.get(keys[i]); |
| if (value != null) |
| result.put(encodePath(prefix, keys[i]), value); |
| } |
| // recursively add the child information |
| IEclipsePreferences[] childNodes = getChildren(true); |
| for (IEclipsePreferences childNode : childNodes) { |
| EclipsePreferences child = (EclipsePreferences) childNode; |
| String fullPath = addSeparator ? prefix + PATH_SEPARATOR + child.name() : child.name(); |
| child.convertToProperties(result, fullPath); |
| } |
| return result; |
| } |
| |
| |
| @Override |
| public IEclipsePreferences create(IEclipsePreferences nodeParent, String nodeName) { |
| return create((EclipsePreferences) nodeParent, nodeName, null); |
| } |
| |
| protected boolean isLoading() { |
| return loading; |
| } |
| |
| protected void setLoading(boolean isLoading) { |
| loading = isLoading; |
| } |
| |
| public IEclipsePreferences create(EclipsePreferences nodeParent, String nodeName, Object context) { |
| EclipsePreferences result = internalCreate(nodeParent, nodeName, context); |
| nodeParent.addChild(nodeName, result); |
| IEclipsePreferences loadLevel = result.getLoadLevel(); |
| |
| // if this node or a parent node is not the load level then return |
| if (loadLevel == null) |
| return result; |
| |
| // if the result node is not a load level, then a child must be |
| if (result != loadLevel) |
| return result; |
| |
| // the result node is a load level |
| if (isAlreadyLoaded(result) || result.isLoading()) |
| return result; |
| try { |
| result.setLoading(true); |
| result.loadLegacy(); |
| result.load(); |
| result.loaded(); |
| result.flush(); |
| } catch (BackingStoreException e) { |
| IPath location = result.getLocation(); |
| String message = NLS.bind(PrefsMessages.preferences_loadException, location == null ? EMPTY_STRING : location.toString()); |
| IStatus status = new Status(IStatus.ERROR, PrefsMessages.OWNER_NAME, IStatus.ERROR, message, e); |
| RuntimeLog.log(status); |
| } finally { |
| result.setLoading(false); |
| } |
| return result; |
| } |
| |
| |
| @Override |
| public void flush() throws BackingStoreException { |
| IEclipsePreferences toFlush = null; |
| synchronized (childAndPropertyLock) { |
| toFlush = internalFlush(); |
| } |
| //if we aren't at the right level, then flush the appropriate node |
| if (toFlush != null) |
| toFlush.flush(); |
| PreferencesService.getDefault().shareStrings(); |
| } |
| |
| /* |
| * Do the real flushing in a non-synchronized internal method so sub-classes |
| * (mainly ProjectPreferences and ProfilePreferences) don't cause deadlocks. |
| * |
| * If this node is not responsible for persistence (a load level), then this method |
| * returns the node that should be flushed. Returns null if this method performed |
| * the flush. |
| */ |
| protected IEclipsePreferences internalFlush() throws BackingStoreException { |
| // illegal state if this node has been removed |
| checkRemoved(); |
| |
| IEclipsePreferences loadLevel = getLoadLevel(); |
| |
| // if this node or a parent is not the load level, then flush the children |
| if (loadLevel == null) { |
| String[] childrenNames = childrenNames(); |
| for (String childrenName : childrenNames) { |
| node(childrenName).flush(); |
| } |
| return null; |
| } |
| |
| // a parent is the load level for this node |
| if (this != loadLevel) |
| return loadLevel; |
| |
| // this node is a load level |
| // any work to do? |
| if (!dirty) |
| return null; |
| //remove dirty bit before saving, to ensure that concurrent |
| //changes during save mark the store as dirty |
| dirty = false; |
| try { |
| save(); |
| } catch (BackingStoreException e) { |
| //mark it dirty again because the save failed |
| dirty = true; |
| throw e; |
| } |
| return null; |
| } |
| |
| |
| @Override |
| public String get(String key, String defaultValue) { |
| String value = internalGet(key); |
| return value == null ? defaultValue : value; |
| } |
| |
| |
| @Override |
| public boolean getBoolean(String key, boolean defaultValue) { |
| String value = internalGet(key); |
| return value == null ? defaultValue : TRUE.equalsIgnoreCase(value); |
| } |
| |
| |
| @Override |
| public byte[] getByteArray(String key, byte[] defaultValue) { |
| String value = internalGet(key); |
| return value == null ? defaultValue : Base64.decode(value.getBytes()); |
| } |
| |
| /* |
| * Return a boolean value indicating whether or not a child with the given |
| * name is known to this node. |
| */ |
| protected boolean childExists(String childName) { |
| synchronized (childAndPropertyLock) { |
| if (children == null) |
| return false; |
| return children.get(childName) != null; |
| } |
| } |
| |
| /** |
| * Thread safe way to obtain a child for a given key. Returns the child |
| * that matches the given key, or null if there is no matching child. |
| */ |
| protected IEclipsePreferences getChild(String key, Object context, boolean create) { |
| synchronized (childAndPropertyLock) { |
| if (children == null) |
| return null; |
| Object value = children.get(key); |
| if (value == null) |
| return null; |
| if (value instanceof IEclipsePreferences) |
| return (IEclipsePreferences) value; |
| // if we aren't supposed to create this node, then |
| // just return null |
| if (!create) |
| return null; |
| } |
| return addChild(key, create(this, key, context)); |
| } |
| |
| /** |
| * Thread safe way to obtain all children of this node. Never returns null. |
| */ |
| protected IEclipsePreferences[] getChildren(boolean create) { |
| ArrayList<IEclipsePreferences> result = new ArrayList<>(); |
| String[] names = internalChildNames(); |
| for (String n : names) { |
| IEclipsePreferences child = getChild(n, null, create); |
| if (child != null) |
| result.add(child); |
| } |
| return result.toArray(EMPTY_NODE_ARRAY); |
| } |
| |
| |
| @Override |
| public double getDouble(String key, double defaultValue) { |
| String value = internalGet(key); |
| double result = defaultValue; |
| if (value != null) |
| try { |
| result = Double.parseDouble(value); |
| } catch (NumberFormatException e) { |
| // use default |
| } |
| return result; |
| } |
| |
| |
| @Override |
| public float getFloat(String key, float defaultValue) { |
| String value = internalGet(key); |
| float result = defaultValue; |
| if (value != null) |
| try { |
| result = Float.parseFloat(value); |
| } catch (NumberFormatException e) { |
| // use default |
| } |
| return result; |
| } |
| |
| |
| @Override |
| public int getInt(String key, int defaultValue) { |
| String value = internalGet(key); |
| int result = defaultValue; |
| if (value != null) |
| try { |
| result = Integer.parseInt(value); |
| } catch (NumberFormatException e) { |
| // use default |
| } |
| return result; |
| } |
| |
| protected IEclipsePreferences getLoadLevel() { |
| return descriptor == null ? null : descriptor.getLoadLevel(this); |
| } |
| |
| /* |
| * Subclasses to over-ride |
| */ |
| protected IPath getLocation() { |
| return null; |
| } |
| |
| |
| @Override |
| public long getLong(String key, long defaultValue) { |
| String value = internalGet(key); |
| long result = defaultValue; |
| if (value != null) |
| try { |
| result = Long.parseLong(value); |
| } catch (NumberFormatException e) { |
| // use default |
| } |
| return result; |
| } |
| |
| protected EclipsePreferences internalCreate(EclipsePreferences nodeParent, String nodeName, Object context) { |
| EclipsePreferences result = new EclipsePreferences(nodeParent, nodeName); |
| result.descriptor = this.descriptor; |
| return result; |
| } |
| |
| /** |
| * Returns the existing value at the given key, or null if |
| * no such value exists. |
| */ |
| protected String internalGet(String key) { |
| // throw NPE if key is null |
| if (key == null) |
| throw new NullPointerException(); |
| // illegal state if this node has been removed |
| checkRemoved(); |
| String result; |
| synchronized (childAndPropertyLock) { |
| result = properties.get(key); |
| } |
| if (DEBUG_PREFERENCE_GET) |
| PrefsMessages.message("Getting preference value: " + absolutePath() + '/' + key + "->" + result); //$NON-NLS-1$ //$NON-NLS-2$ |
| return result; |
| } |
| |
| /** |
| * Implements the node(String) method, and optionally notifies listeners. |
| */ |
| protected IEclipsePreferences internalNode(String path, boolean notify, Object context) { |
| |
| // illegal state if this node has been removed |
| checkRemoved(); |
| |
| // short circuit this node |
| if (path.length() == 0) |
| return this; |
| |
| // if we have an absolute path use the root relative to |
| // this node instead of the global root |
| // in case we have a different hierarchy. (e.g. export) |
| if (path.charAt(0) == IPath.SEPARATOR) |
| return (IEclipsePreferences) calculateRoot().node(path.substring(1)); |
| |
| int index = path.indexOf(IPath.SEPARATOR); |
| String key = index == -1 ? path : path.substring(0, index); |
| boolean added = false; |
| IEclipsePreferences child = getChild(key, context, true); |
| if (child == null) { |
| child = create(this, key, context); |
| added = true; |
| } |
| // notify listeners if a child was added |
| if (added && notify) |
| fireNodeEvent(new NodeChangeEvent(this, child), true); |
| return (IEclipsePreferences) child.node(index == -1 ? EMPTY_STRING : path.substring(index + 1)); |
| } |
| |
| /** |
| * Stores the given (key,value) pair, performing lazy initialization of the |
| * properties field if necessary. Returns the old value for the given key, |
| * or null if no value existed. |
| */ |
| protected String internalPut(String key, String newValue) { |
| synchronized (childAndPropertyLock) { |
| // illegal state if this node has been removed |
| checkRemoved(); |
| String oldValue = properties.get(key); |
| if (oldValue != null && oldValue.equals(newValue)) |
| return oldValue; |
| if (DEBUG_PREFERENCE_SET) |
| PrefsMessages.message("Setting preference: " + absolutePath() + '/' + key + '=' + newValue); //$NON-NLS-1$ |
| properties = properties.put(key, newValue); |
| return oldValue; |
| } |
| } |
| |
| /* |
| * Subclasses to over-ride. |
| */ |
| protected boolean isAlreadyLoaded(IEclipsePreferences node) { |
| return descriptor == null ? true : descriptor.isAlreadyLoaded(node.absolutePath()); |
| } |
| |
| |
| @Override |
| public String[] keys() { |
| // illegal state if this node has been removed |
| synchronized (childAndPropertyLock) { |
| checkRemoved(); |
| return properties.keys(); |
| } |
| } |
| |
| /** |
| * Loads the preference node. This method returns silently if the node does not exist |
| * in the backing store (for example non-existent project). |
| * |
| * @throws BackingStoreException if the node exists in the backing store but it |
| * could not be loaded |
| */ |
| protected void load() throws BackingStoreException { |
| if (descriptor == null) { |
| load(getLocation()); |
| } else { |
| // load the properties then set them without sending out change events |
| Properties props = descriptor.load(absolutePath()); |
| if (props == null || props.isEmpty()) |
| return; |
| convertFromProperties(this, props, false); |
| } |
| } |
| |
| protected static Properties loadProperties(IPath location) throws BackingStoreException { |
| if (DEBUG_PREFERENCE_GENERAL) |
| PrefsMessages.message("Loading preferences from file: " + location); //$NON-NLS-1$ |
| InputStream input = null; |
| Properties result = new Properties(); |
| try { |
| input = new SafeFileInputStream(location.toFile()); |
| result.load(input); |
| } catch (FileNotFoundException e) { |
| // file doesn't exist but that's ok. |
| if (DEBUG_PREFERENCE_GENERAL) |
| PrefsMessages.message("Preference file does not exist: " + location); //$NON-NLS-1$ |
| return result; |
| } catch (IOException e) { |
| String message = NLS.bind(PrefsMessages.preferences_loadException, location); |
| log(new Status(IStatus.INFO, PrefsMessages.OWNER_NAME, IStatus.INFO, message, e)); |
| throw new BackingStoreException(message, e); |
| } catch (IllegalArgumentException e) { |
| String message = NLS.bind(PrefsMessages.preferences_loadException, location); |
| log(new Status(IStatus.INFO, PrefsMessages.OWNER_NAME, IStatus.INFO, message, e)); |
| throw new BackingStoreException(message, e); |
| } finally { |
| if (input != null) |
| try { |
| input.close(); |
| } catch (IOException e) { |
| // ignore |
| } |
| } |
| return result; |
| } |
| |
| protected void load(IPath location) throws BackingStoreException { |
| if (location == null) { |
| if (DEBUG_PREFERENCE_GENERAL) |
| PrefsMessages.message("Unable to determine location of preference file for node: " + absolutePath()); //$NON-NLS-1$ |
| return; |
| } |
| Properties fromDisk = loadProperties(location); |
| convertFromProperties(this, fromDisk, false); |
| } |
| |
| protected void loaded() { |
| if (descriptor == null) { |
| // do nothing |
| } else { |
| descriptor.loaded(absolutePath()); |
| } |
| } |
| |
| protected void loadLegacy() { |
| // sub-classes to over-ride if necessary |
| } |
| |
| public static void log(IStatus status) { |
| RuntimeLog.log(status); |
| } |
| |
| protected void makeDirty() { |
| EclipsePreferences node = this; |
| while (node != null && !node.removed) { |
| node.dirty = true; |
| node = (EclipsePreferences) node.parent(); |
| } |
| } |
| |
| public boolean isDirty() { |
| return dirty; |
| } |
| |
| |
| @Override |
| public String name() { |
| return name; |
| } |
| |
| |
| @Override |
| public Preferences node(String pathName) { |
| return internalNode(pathName, true, null); |
| } |
| |
| protected void fireNodeEvent(final NodeChangeEvent event, final boolean added) { |
| if (nodeChangeListeners == null) |
| return; |
| for (final INodeChangeListener listener : nodeChangeListeners) { |
| ISafeRunnable job = new ISafeRunnable() { |
| @Override |
| public void handleException(Throwable exception) { |
| // already logged in Platform#run() |
| } |
| |
| @Override |
| public void run() throws Exception { |
| if (added) |
| listener.added(event); |
| else |
| listener.removed(event); |
| } |
| }; |
| SafeRunner.run(job); |
| } |
| } |
| |
| |
| @Override |
| public boolean nodeExists(String path) throws BackingStoreException { |
| // short circuit for checking this node |
| if (path.length() == 0) |
| return !removed; |
| |
| // illegal state if this node has been removed. |
| // do this AFTER checking for the empty string. |
| checkRemoved(); |
| |
| // use the root relative to this node instead of the global root |
| // in case we have a different hierarchy. (e.g. export) |
| if (path.charAt(0) == IPath.SEPARATOR) |
| return calculateRoot().nodeExists(path.substring(1)); |
| |
| int index = path.indexOf(IPath.SEPARATOR); |
| boolean noSlash = index == -1; |
| |
| // if we are looking for a simple child then just look in the table and return |
| if (noSlash) |
| return childExists(path); |
| |
| // otherwise load the parent of the child and then recursively ask |
| String childName = path.substring(0, index); |
| if (!childExists(childName)) |
| return false; |
| IEclipsePreferences child = getChild(childName, null, true); |
| if (child == null) |
| return false; |
| return child.nodeExists(path.substring(index + 1)); |
| } |
| |
| |
| @Override |
| public Preferences parent() { |
| // illegal state if this node has been removed |
| checkRemoved(); |
| return parent; |
| } |
| |
| /* |
| * Convenience method for notifying preference change listeners. |
| */ |
| protected void firePreferenceEvent(String key, Object oldValue, Object newValue) { |
| if (preferenceChangeListeners == null) |
| return; |
| final PreferenceChangeEvent event = new PreferenceChangeEvent(this, key, oldValue, newValue); |
| for (final IPreferenceChangeListener listener : preferenceChangeListeners) { |
| ISafeRunnable job = new ISafeRunnable() { |
| @Override |
| public void handleException(Throwable exception) { |
| // already logged in Platform#run() |
| } |
| |
| @Override |
| public void run() throws Exception { |
| listener.preferenceChange(event); |
| } |
| }; |
| SafeRunner.run(job); |
| } |
| } |
| |
| |
| @Override |
| public void put(String key, String newValue) { |
| if (key == null || newValue == null) |
| throw new NullPointerException(); |
| String oldValue = internalPut(key, newValue); |
| if (!newValue.equals(oldValue)) { |
| makeDirty(); |
| firePreferenceEvent(key, oldValue, newValue); |
| } |
| } |
| |
| |
| @Override |
| public void putBoolean(String key, boolean value) { |
| if (key == null) |
| throw new NullPointerException(); |
| String newValue = value ? TRUE : FALSE; |
| String oldValue = internalPut(key, newValue); |
| if (!newValue.equals(oldValue)) { |
| makeDirty(); |
| firePreferenceEvent(key, oldValue, newValue); |
| } |
| } |
| |
| |
| @Override |
| public void putByteArray(String key, byte[] value) { |
| if (key == null || value == null) |
| throw new NullPointerException(); |
| String newValue = new String(Base64.encode(value)); |
| String oldValue = internalPut(key, newValue); |
| if (!newValue.equals(oldValue)) { |
| makeDirty(); |
| firePreferenceEvent(key, oldValue, newValue); |
| } |
| } |
| |
| |
| @Override |
| public void putDouble(String key, double value) { |
| if (key == null) |
| throw new NullPointerException(); |
| String newValue = Double.toString(value); |
| String oldValue = internalPut(key, newValue); |
| if (!newValue.equals(oldValue)) { |
| makeDirty(); |
| firePreferenceEvent(key, oldValue, newValue); |
| } |
| } |
| |
| |
| @Override |
| public void putFloat(String key, float value) { |
| if (key == null) |
| throw new NullPointerException(); |
| String newValue = Float.toString(value); |
| String oldValue = internalPut(key, newValue); |
| if (!newValue.equals(oldValue)) { |
| makeDirty(); |
| firePreferenceEvent(key, oldValue, newValue); |
| } |
| } |
| |
| |
| @Override |
| public void putInt(String key, int value) { |
| if (key == null) |
| throw new NullPointerException(); |
| String newValue = Integer.toString(value); |
| String oldValue = internalPut(key, newValue); |
| if (!newValue.equals(oldValue)) { |
| makeDirty(); |
| firePreferenceEvent(key, oldValue, newValue); |
| } |
| } |
| |
| |
| @Override |
| public void putLong(String key, long value) { |
| if (key == null) |
| throw new NullPointerException(); |
| String newValue = Long.toString(value); |
| String oldValue = internalPut(key, newValue); |
| if (!newValue.equals(oldValue)) { |
| makeDirty(); |
| firePreferenceEvent(key, oldValue, newValue); |
| } |
| } |
| |
| |
| @Override |
| public void remove(String key) { |
| String oldValue; |
| synchronized (childAndPropertyLock) { |
| // illegal state if this node has been removed |
| checkRemoved(); |
| oldValue = properties.get(key); |
| if (oldValue == null) |
| return; |
| properties = properties.removeKey(key); |
| } |
| makeDirty(); |
| firePreferenceEvent(key, oldValue, null); |
| } |
| |
| |
| @Override |
| public void removeNode() throws BackingStoreException { |
| // illegal state if this node has been removed |
| checkRemoved(); |
| // clear all the property values. do it "the long way" so |
| // everyone gets notification |
| String[] keys = keys(); |
| for (String key : keys) { |
| remove(key); |
| } |
| // don't remove the global root or the scope root from the |
| // parent but remove all its children |
| if (parent != null && !(parent instanceof RootPreferences)) { |
| // remove the node from the parent's collection and notify listeners |
| removed = true; |
| parent.removeNode(this); |
| } |
| IEclipsePreferences[] childNodes = getChildren(false); |
| for (IEclipsePreferences childNode : childNodes) { |
| try { |
| childNode.removeNode(); |
| }catch (IllegalStateException e) { |
| // ignore since we only get this exception if we have already |
| // been removed. no work to do. |
| } |
| } |
| } |
| |
| /* |
| * Remove the child from the collection and notify the listeners if something |
| * was actually removed. |
| */ |
| protected void removeNode(IEclipsePreferences child) { |
| if (removeNode(child.name()) != null) { |
| fireNodeEvent(new NodeChangeEvent(this, child), false); |
| if (descriptor != null) |
| descriptor.removed(child.absolutePath()); |
| } |
| } |
| |
| /* |
| * Remove non-initialized node from the collection. |
| */ |
| protected Object removeNode(String key) { |
| synchronized (childAndPropertyLock) { |
| if (children != null) { |
| Object result = children.remove(key); |
| if (result != null) |
| makeDirty(); |
| if (children.isEmpty()) |
| children = null; |
| return result; |
| } |
| } |
| return null; |
| } |
| |
| |
| @Override |
| public void removeNodeChangeListener(INodeChangeListener listener) { |
| checkRemoved(); |
| if (nodeChangeListeners == null) |
| return; |
| nodeChangeListeners.remove(listener); |
| if (DEBUG_PREFERENCE_GENERAL) |
| PrefsMessages.message("Removed preference node change listener: " + listener + " from: " + absolutePath()); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| |
| |
| @Override |
| public void removePreferenceChangeListener(IPreferenceChangeListener listener) { |
| checkRemoved(); |
| if (preferenceChangeListeners == null) |
| return; |
| preferenceChangeListeners.remove(listener); |
| if (DEBUG_PREFERENCE_GENERAL) |
| PrefsMessages.message("Removed preference property change listener: " + listener + " from: " + absolutePath()); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| |
| /** |
| * Saves the preference node. This method returns silently if the node does not exist |
| * in the backing store (for example non-existent project) |
| * |
| * @throws BackingStoreException if the node exists in the backing store but it |
| * could not be saved |
| */ |
| protected void save() throws BackingStoreException { |
| if (descriptor == null) { |
| save(getLocation()); |
| } else { |
| descriptor.save(absolutePath(), convertToProperties(new Properties(), "")); //$NON-NLS-1$ |
| } |
| } |
| |
| protected void save(IPath location) throws BackingStoreException { |
| if (location == null) { |
| if (DEBUG_PREFERENCE_GENERAL) |
| PrefsMessages.message("Unable to determine location of preference file for node: " + absolutePath()); //$NON-NLS-1$ |
| return; |
| } |
| if (DEBUG_PREFERENCE_GENERAL) |
| PrefsMessages.message("Saving preferences to file: " + location); //$NON-NLS-1$ |
| Properties table = convertToProperties(new SortedProperties(), EMPTY_STRING); |
| if (table.isEmpty()) { |
| // nothing to save. delete existing file if one exists. |
| if (location.toFile().exists() && !location.toFile().delete()) { |
| String message = NLS.bind(PrefsMessages.preferences_failedDelete, location); |
| log(new Status(IStatus.WARNING, PrefsMessages.OWNER_NAME, IStatus.WARNING, message, null)); |
| } |
| return; |
| } |
| table.put(VERSION_KEY, VERSION_VALUE); |
| write(table, location); |
| } |
| |
| /** |
| * Traverses the preference hierarchy rooted at this node, and adds |
| * all preference key and value strings to the provided pool. If an added |
| * string was already in the pool, all references will be replaced with the |
| * canonical copy of the string. |
| * |
| * @param pool The pool to share strings in |
| */ |
| public void shareStrings(StringPool pool) { |
| //thread safety: copy reference in case of concurrent change |
| ImmutableMap temp; |
| synchronized (childAndPropertyLock) { |
| temp = properties; |
| } |
| temp.shareStrings(pool); |
| IEclipsePreferences[] myChildren = getChildren(false); |
| for (IEclipsePreferences child : myChildren) { |
| if (child instanceof EclipsePreferences) { |
| ((EclipsePreferences) child).shareStrings(pool); |
| } |
| } |
| } |
| |
| /* |
| * Encode the given path and key combo to a form which is suitable for |
| * persisting or using when searching. If the key contains a slash character |
| * then we must use a double-slash to indicate the end of the |
| * path/the beginning of the key. |
| */ |
| public static String encodePath(String path, String key) { |
| String result; |
| int pathLength = path == null ? 0 : path.length(); |
| if (key.indexOf(IPath.SEPARATOR) == -1) { |
| if (pathLength == 0) |
| result = key; |
| else |
| result = path + IPath.SEPARATOR + key; |
| } else { |
| if (pathLength == 0) |
| result = DOUBLE_SLASH + key; |
| else |
| result = path + DOUBLE_SLASH + key; |
| } |
| return result; |
| } |
| |
| /* |
| * Return the segment from the given path or null. |
| * "segment" parameter is 0-based. |
| */ |
| public static String getSegment(String path, int segment) { |
| int start = path.indexOf(IPath.SEPARATOR) == 0 ? 1 : 0; |
| int end = path.indexOf(IPath.SEPARATOR, start); |
| if (end == path.length() - 1) |
| end = -1; |
| for (int i = 0; i < segment; i++) { |
| if (end == -1) |
| return null; |
| start = end + 1; |
| end = path.indexOf(IPath.SEPARATOR, start); |
| } |
| if (end == -1) |
| end = path.length(); |
| return path.substring(start, end); |
| } |
| |
| public static int getSegmentCount(String path) { |
| StringTokenizer tokenizer = new StringTokenizer(path, String.valueOf(IPath.SEPARATOR)); |
| return tokenizer.countTokens(); |
| } |
| |
| /* |
| * Return a relative path |
| */ |
| public static String makeRelative(String path) { |
| String result = path; |
| if (path == null) |
| return EMPTY_STRING; |
| if (path.length() > 0 && path.charAt(0) == IPath.SEPARATOR) |
| result = path.length() == 0 ? EMPTY_STRING : path.substring(1); |
| return result; |
| } |
| |
| /* |
| * Return a 2 element String array. |
| * element 0 - the path |
| * element 1 - the key |
| * The path may be null. |
| * The key is never null. |
| */ |
| public static String[] decodePath(String fullPath) { |
| String key = null; |
| String path = null; |
| |
| // check to see if we have an indicator which tells us where the path ends |
| int index = fullPath.indexOf(DOUBLE_SLASH); |
| if (index == -1) { |
| // we don't have a double-slash telling us where the path ends |
| // so the path is up to the last slash character |
| int lastIndex = fullPath.lastIndexOf(IPath.SEPARATOR); |
| if (lastIndex == -1) { |
| key = fullPath; |
| } else { |
| path = fullPath.substring(0, lastIndex); |
| key = fullPath.substring(lastIndex + 1); |
| } |
| } else { |
| // the child path is up to the double-slash and the key |
| // is the string after it |
| path = fullPath.substring(0, index); |
| key = fullPath.substring(index + 2); |
| } |
| |
| // adjust if we have an absolute path |
| if (path != null) |
| if (path.length() == 0) |
| path = null; |
| else if (path.charAt(0) == IPath.SEPARATOR) |
| path = path.substring(1); |
| |
| return new String[] {path, key}; |
| } |
| |
| |
| |
| @Override |
| public void sync() throws BackingStoreException { |
| // illegal state if this node has been removed |
| checkRemoved(); |
| IEclipsePreferences node = getLoadLevel(); |
| if (node == null) { |
| if (DEBUG_PREFERENCE_GENERAL) |
| PrefsMessages.message("Preference node is not a load root: " + absolutePath()); //$NON-NLS-1$ |
| return; |
| } |
| if (node instanceof EclipsePreferences) { |
| ((EclipsePreferences) node).load(); |
| node.flush(); |
| } |
| } |
| |
| public String toDeepDebugString() { |
| final StringBuffer buffer = new StringBuffer(); |
| IPreferenceNodeVisitor visitor = (IEclipsePreferences node) -> { |
| buffer.append(node); |
| buffer.append('\n'); |
| String[] keys = node.keys(); |
| for (String key : keys) { |
| buffer.append(node.absolutePath()); |
| buffer.append(PATH_SEPARATOR); |
| buffer.append(key); |
| buffer.append('='); |
| buffer.append(node.get(key, "*default*")); //$NON-NLS-1$ |
| buffer.append('\n'); |
| } |
| return true; |
| }; |
| try { |
| accept(visitor); |
| } catch (BackingStoreException e) { |
| System.out.println("Exception while calling #toDeepDebugString()"); //$NON-NLS-1$ |
| e.printStackTrace(); |
| } |
| return buffer.toString(); |
| } |
| |
| @Override |
| public String toString() { |
| return absolutePath(); |
| } |
| |
| void setDescriptor(ScopeDescriptor descriptor) { |
| this.descriptor = descriptor; |
| } |
| } |