blob: b2b0daa4ec41982e51c72bf9615b7c64aeaab187 [file] [log] [blame]
/*******************************************************************************
* 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 | 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;
}
}