/******************************************************************************* * Copyright (c) 2004, 2015 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * 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; } }