| /******************************************************************************* |
| * 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 |
| * Gunnar Wagenknecht - Bug 179695 - [prefs] NPE when using Preferences API without a product |
| * Thirumala Reddy Mutchukota, Google Inc - Bug 380859 - [prefs] Inconsistency between DefaultPreferences and InstancePreferences |
| *******************************************************************************/ |
| package org.eclipse.core.internal.preferences; |
| |
| import java.io.*; |
| import java.lang.ref.WeakReference; |
| import java.net.URL; |
| import java.util.*; |
| import org.eclipse.core.internal.preferences.exchange.IProductPreferencesService; |
| import org.eclipse.core.internal.runtime.RuntimeLog; |
| import org.eclipse.core.runtime.*; |
| import org.eclipse.core.runtime.preferences.BundleDefaultsScope; |
| import org.eclipse.core.runtime.preferences.IEclipsePreferences; |
| import org.eclipse.osgi.util.NLS; |
| import org.osgi.framework.Bundle; |
| import org.osgi.framework.BundleContext; |
| import org.osgi.service.prefs.BackingStoreException; |
| import org.osgi.service.prefs.Preferences; |
| import org.osgi.util.tracker.ServiceTracker; |
| |
| /** |
| * @since 3.0 |
| */ |
| public class DefaultPreferences extends EclipsePreferences { |
| // cache which nodes have been loaded from disk |
| private static Set<String> loadedNodes = Collections.synchronizedSet(new HashSet<String>()); |
| private static final String KEY_PREFIX = "%"; //$NON-NLS-1$ |
| private static final String KEY_DOUBLE_PREFIX = "%%"; //$NON-NLS-1$ |
| private static final IPath NL_DIR = new Path("$nl$"); //$NON-NLS-1$ |
| |
| private static final String PROPERTIES_FILE_EXTENSION = "properties"; //$NON-NLS-1$ |
| private static Properties productCustomization; |
| private static Properties productTranslation; |
| private static Properties commandLineCustomization; |
| private EclipsePreferences loadLevel; |
| private Thread initializingThread; |
| |
| // cached values |
| private String qualifier; |
| private int segmentCount; |
| private WeakReference<Object> pluginReference; |
| |
| public static String pluginCustomizationFile = null; |
| |
| /** |
| * Default constructor for this class. |
| */ |
| public DefaultPreferences() { |
| this(null, null); |
| } |
| |
| private DefaultPreferences(EclipsePreferences parent, String name, Object context) { |
| this(parent, name); |
| this.pluginReference = new WeakReference<>(context); |
| } |
| |
| private DefaultPreferences(EclipsePreferences parent, String name) { |
| super(parent, name); |
| |
| if (parent instanceof DefaultPreferences) |
| this.pluginReference = ((DefaultPreferences) parent).pluginReference; |
| |
| // cache the segment count |
| String path = absolutePath(); |
| segmentCount = getSegmentCount(path); |
| if (segmentCount < 2) |
| return; |
| |
| // cache the qualifier |
| qualifier = getSegment(path, 1); |
| } |
| |
| /* |
| * Apply the values set in the bundle's install directory. |
| * |
| * In Eclipse 2.1 this is equivalent to: |
| * /eclipse/plugins/<pluginID>/prefs.ini |
| */ |
| private void applyBundleDefaults() { |
| Bundle bundle = PreferencesOSGiUtils.getDefault().getBundle(name()); |
| if (bundle == null) |
| return; |
| URL url = FileLocator.find(bundle, new Path(IPreferencesConstants.PREFERENCES_DEFAULT_OVERRIDE_FILE_NAME), null); |
| if (url == null) { |
| if (EclipsePreferences.DEBUG_PREFERENCE_GENERAL) |
| PrefsMessages.message("Preference default override file not found for bundle: " + bundle.getSymbolicName()); //$NON-NLS-1$ |
| return; |
| } |
| URL transURL = FileLocator.find(bundle, NL_DIR.append(IPreferencesConstants.PREFERENCES_DEFAULT_OVERRIDE_BASE_NAME).addFileExtension(PROPERTIES_FILE_EXTENSION), null); |
| if (transURL == null && EclipsePreferences.DEBUG_PREFERENCE_GENERAL) |
| PrefsMessages.message("Preference translation file not found for bundle: " + bundle.getSymbolicName()); //$NON-NLS-1$ |
| applyDefaults(name(), loadProperties(url), loadProperties(transURL)); |
| } |
| |
| /* |
| * Apply the default values as specified in the file |
| * as an argument on the command-line. |
| */ |
| private void applyCommandLineDefaults() { |
| if (commandLineCustomization != null) |
| applyDefaults(null, commandLineCustomization, null); |
| } |
| |
| /* |
| * If the qualifier is null then the file is of the format: |
| * pluginID/key=value |
| * otherwise the file is of the format: |
| * key=value |
| */ |
| private void applyDefaults(String id, Properties defaultValues, Properties translations) { |
| for (Enumeration<?> e = defaultValues.keys(); e.hasMoreElements();) { |
| String fullKey = (String) e.nextElement(); |
| String value = defaultValues.getProperty(fullKey); |
| if (value == null) |
| continue; |
| String localQualifier = id; |
| String fullPath = fullKey; |
| int firstIndex = fullKey.indexOf(PATH_SEPARATOR); |
| if (id == null && firstIndex > 0) { |
| localQualifier = fullKey.substring(0, firstIndex); |
| fullPath = fullKey.substring(firstIndex, fullKey.length()); |
| } |
| String[] splitPath = decodePath(fullPath); |
| String childPath = splitPath[0]; |
| childPath = makeRelative(childPath); |
| String key = splitPath[1]; |
| if (name().equals(localQualifier)) { |
| value = translatePreference(value, translations); |
| if (EclipsePreferences.DEBUG_PREFERENCE_SET) |
| PrefsMessages.message("Setting default preference: " + (new Path(absolutePath()).append(childPath).append(key)) + '=' + value); //$NON-NLS-1$ |
| ((EclipsePreferences) internalNode(childPath.toString(), false, null)).internalPut(key, value); |
| } |
| } |
| } |
| |
| public IEclipsePreferences node(String childName, Object context) { |
| return internalNode(childName, true, context); |
| } |
| |
| private boolean containsNode(Properties props, IPath path) { |
| if (props == null) |
| return false; |
| for (Enumeration<?> e = props.keys(); e.hasMoreElements();) { |
| String fullKey = (String) e.nextElement(); |
| if (props.getProperty(fullKey) == null) |
| continue; |
| // remove last segment which stands for key |
| IPath nodePath = new Path(fullKey).removeLastSegments(1); |
| if (path.isPrefixOf(nodePath)) |
| return true; |
| } |
| return false; |
| } |
| |
| @Override |
| public boolean nodeExists(String path) throws BackingStoreException { |
| // use super implementation for empty and absolute paths |
| if (path.length() == 0 || path.charAt(0) == IPath.SEPARATOR) |
| return super.nodeExists(path); |
| // if the node already exists, nothing more to do |
| if (super.nodeExists(path)) |
| return true; |
| // if the node does not exist, maybe it has not been loaded yet |
| initializeCustomizations(); |
| // scope based path is a path relative to the "/default" node; this is the path that appears in customizations |
| IPath scopeBasedPath = new Path(absolutePath() + PATH_SEPARATOR + path).removeFirstSegments(1); |
| return containsNode(productCustomization, scopeBasedPath) || containsNode(commandLineCustomization, scopeBasedPath); |
| } |
| |
| private void initializeCustomizations() { |
| // prime the cache the first time |
| if (productCustomization == null) { |
| BundleContext context = Activator.getContext(); |
| if (context != null) { |
| ServiceTracker<?, IProductPreferencesService> productTracker = new ServiceTracker<>(context, IProductPreferencesService.class, null); |
| productTracker.open(); |
| IProductPreferencesService productSpecials = productTracker.getService(); |
| if (productSpecials != null) { |
| productCustomization = productSpecials.getProductCustomization(); |
| productTranslation = productSpecials.getProductTranslation(); |
| } |
| productTracker.close(); |
| } else { |
| PrefsMessages.message("Product-specified preferences called before plugin is started"); //$NON-NLS-1$ |
| } |
| if (productCustomization == null) |
| productCustomization = new Properties(); |
| } |
| if (commandLineCustomization == null) { |
| String filename = pluginCustomizationFile; |
| if (filename == null) { |
| if (EclipsePreferences.DEBUG_PREFERENCE_GENERAL) |
| PrefsMessages.message("Command-line preferences customization file not specified."); //$NON-NLS-1$ |
| } else { |
| if (EclipsePreferences.DEBUG_PREFERENCE_GENERAL) |
| PrefsMessages.message("Using command-line preference customization file: " + filename); //$NON-NLS-1$ |
| commandLineCustomization = loadProperties(filename); |
| } |
| } |
| } |
| |
| /* |
| * Runtime defaults are the ones which are specified in code at runtime. |
| * |
| * In the Eclipse 2.1 world they were the ones which were specified in the |
| * over-ridden Plugin#initializeDefaultPluginPreferences() method. |
| * |
| * In Eclipse 3.0 they are set in the code which is indicated by the |
| * extension to the plug-in default customizer extension point. |
| */ |
| private void applyRuntimeDefaults() { |
| WeakReference<Object> ref = PreferencesService.getDefault().applyRuntimeDefaults(name(), pluginReference); |
| if (ref != null) |
| pluginReference = ref; |
| } |
| |
| /* |
| * Apply the default values as specified by the file |
| * in the product extension. |
| * |
| * In Eclipse 2.1 this is equivalent to the plugin_customization.ini |
| * file in the primary feature's plug-in directory. |
| */ |
| private void applyProductDefaults() { |
| if (!productCustomization.isEmpty()) |
| applyDefaults(null, productCustomization, productTranslation); |
| } |
| |
| |
| @Override |
| public void flush() { |
| // default values are not persisted |
| } |
| |
| @Override |
| protected IEclipsePreferences getLoadLevel() { |
| if (loadLevel == null) { |
| if (qualifier == null) |
| return null; |
| // Make it relative to this node rather than navigating to it from the root. |
| // Walk backwards up the tree starting at this node. |
| // This is important to avoid a chicken/egg thing on startup. |
| EclipsePreferences node = this; |
| for (int i = 2; i < segmentCount; i++) |
| node = (EclipsePreferences) node.parent(); |
| loadLevel = node; |
| } |
| return loadLevel; |
| } |
| |
| @Override |
| protected EclipsePreferences internalCreate(EclipsePreferences nodeParent, String nodeName, Object context) { |
| return new DefaultPreferences(nodeParent, nodeName, context); |
| } |
| |
| @Override |
| protected boolean isAlreadyLoaded(IEclipsePreferences node) { |
| return loadedNodes.contains(node.name()); |
| } |
| |
| |
| @Override |
| protected void load() { |
| setInitializingBundleDefaults(); |
| try { |
| applyRuntimeDefaults(); |
| applyBundleDefaults(); |
| } finally { |
| clearInitializingBundleDefaults(); |
| } |
| initializeCustomizations(); |
| applyProductDefaults(); |
| applyCommandLineDefaults(); |
| } |
| |
| |
| @Override |
| protected String internalPut(String key, String newValue) { |
| // set the value in this node |
| String result = super.internalPut(key, newValue); |
| |
| // if we are setting the bundle defaults, then set the corresponding value in |
| // the bundle_defaults scope |
| if (isInitializingBundleDefaults()) { |
| String relativePath = getScopeRelativePath(absolutePath()); |
| if (relativePath != null) { |
| Preferences node = PreferencesService.getDefault().getRootNode().node(BundleDefaultsScope.SCOPE).node(relativePath); |
| node.put(key, newValue); |
| } |
| } |
| return result; |
| } |
| |
| /* |
| * Set that we are in the middle of initializing the bundle defaults. |
| * This is stored on the load level so we know where to look when |
| * we are setting values on sub-nodes. |
| */ |
| private void setInitializingBundleDefaults() { |
| IEclipsePreferences node = getLoadLevel(); |
| if (node instanceof DefaultPreferences) { |
| DefaultPreferences loader = (DefaultPreferences) node; |
| loader.initializingThread = Thread.currentThread(); |
| } |
| } |
| |
| /* |
| * Clear the bit saying we are in the middle of initializing the bundle defaults. |
| * This is stored on the load level so we know where to look when |
| * we are setting values on sub-nodes. |
| */ |
| private void clearInitializingBundleDefaults() { |
| IEclipsePreferences node = getLoadLevel(); |
| if (node instanceof DefaultPreferences) { |
| DefaultPreferences loader = (DefaultPreferences) node; |
| loader.initializingThread = null; |
| } |
| } |
| |
| /* |
| * Are we in the middle of initializing defaults from the bundle |
| * initializer or found in the bundle itself? Look on the load level in |
| * case we are in a sub-node. |
| */ |
| private boolean isInitializingBundleDefaults() { |
| IEclipsePreferences node = getLoadLevel(); |
| if (node instanceof DefaultPreferences) { |
| DefaultPreferences loader = (DefaultPreferences) node; |
| return loader.initializingThread == Thread.currentThread(); |
| } |
| return false; |
| } |
| |
| /* |
| * Return a path which is relative to the scope of this node. |
| * e.g. com.example.foo for /instance/com.example.foo |
| */ |
| protected static String getScopeRelativePath(String absolutePath) { |
| // shouldn't happen but handle empty or root |
| if (absolutePath.length() < 2) |
| return null; |
| int index = absolutePath.indexOf('/', 1); |
| if (index == -1 || index + 1 >= absolutePath.length()) |
| return null; |
| return absolutePath.substring(index + 1); |
| } |
| |
| private Properties loadProperties(URL url) { |
| Properties result = new Properties(); |
| if (url == null) |
| return result; |
| InputStream input = null; |
| try { |
| input = url.openStream(); |
| result.load(input); |
| } catch (IOException e) { |
| if (EclipsePreferences.DEBUG_PREFERENCE_GENERAL) { |
| PrefsMessages.message("Problem opening stream to preference customization file: " + url); //$NON-NLS-1$ |
| e.printStackTrace(); |
| } |
| } catch (IllegalArgumentException e) { |
| if (EclipsePreferences.DEBUG_PREFERENCE_GENERAL) { |
| PrefsMessages.message("Problem opening stream to preference customization file: " + url); //$NON-NLS-1$ |
| e.printStackTrace(); |
| } |
| } finally { |
| if (input != null) |
| try { |
| input.close(); |
| } catch (IOException e) { |
| // ignore |
| } |
| } |
| return result; |
| } |
| |
| private Properties loadProperties(String filename) { |
| Properties result = new Properties(); |
| InputStream input = null; |
| try { |
| input = new BufferedInputStream(new FileInputStream(filename)); |
| result.load(input); |
| } catch (FileNotFoundException e) { |
| if (EclipsePreferences.DEBUG_PREFERENCE_GENERAL) |
| PrefsMessages.message("Preference customization file not found: " + filename); //$NON-NLS-1$ |
| } catch (IOException e) { |
| String message = NLS.bind(PrefsMessages.preferences_loadException, filename); |
| IStatus status = new Status(IStatus.ERROR, PrefsMessages.OWNER_NAME, IStatus.ERROR, message, e); |
| RuntimeLog.log(status); |
| } catch (IllegalArgumentException e) { |
| String message = NLS.bind(PrefsMessages.preferences_loadException, filename); |
| IStatus status = new Status(IStatus.ERROR, PrefsMessages.OWNER_NAME, IStatus.ERROR, message, e); |
| RuntimeLog.log(status); |
| } finally { |
| if (input != null) |
| try { |
| input.close(); |
| } catch (IOException e) { |
| // ignore |
| } |
| } |
| return result; |
| } |
| |
| @Override |
| protected void loaded() { |
| loadedNodes.add(name()); |
| } |
| |
| |
| @Override |
| public void sync() { |
| // default values are not persisted |
| } |
| |
| /** |
| * Takes a preference value and a related resource bundle and |
| * returns the translated version of this value (if one exists). |
| */ |
| private String translatePreference(String origValue, Properties props) { |
| if (props == null || origValue.startsWith(KEY_DOUBLE_PREFIX)) |
| return origValue; |
| if (origValue.startsWith(KEY_PREFIX)) { |
| String value = origValue.trim(); |
| int ix = value.indexOf(" "); //$NON-NLS-1$ |
| String key = ix == -1 ? value.substring(1) : value.substring(1, ix); |
| String dflt = ix == -1 ? value : value.substring(ix + 1); |
| return props.getProperty(key, dflt); |
| } |
| return origValue; |
| } |
| } |