1 /******************************************************************************* 2 * Copyright (c) 2004, 2015 IBM Corporation and others. 3 * 4 * This program and the accompanying materials 5 * are made available under the terms of the Eclipse Public License 2.0 6 * which accompanies this distribution, and is available at 7 * https://www.eclipse.org/legal/epl-2.0/ 8 * 9 * SPDX-License-Identifier: EPL-2.0 10 * 11 * Contributors: 12 * IBM Corporation - initial API and implementation 13 * Gunnar Wagenknecht - Bug 179695 - [prefs] NPE when using Preferences API without a product 14 * Thirumala Reddy Mutchukota, Google Inc - Bug 380859 - [prefs] Inconsistency between DefaultPreferences and InstancePreferences 15 *******************************************************************************/ 16 package org.eclipse.core.internal.preferences; 17 18 import java.io.*; 19 import java.lang.ref.WeakReference; 20 import java.net.URL; 21 import java.util.*; 22 import org.eclipse.core.internal.preferences.exchange.IProductPreferencesService; 23 import org.eclipse.core.internal.runtime.RuntimeLog; 24 import org.eclipse.core.runtime.*; 25 import org.eclipse.core.runtime.preferences.BundleDefaultsScope; 26 import org.eclipse.core.runtime.preferences.IEclipsePreferences; 27 import org.eclipse.osgi.util.NLS; 28 import org.osgi.framework.Bundle; 29 import org.osgi.framework.BundleContext; 30 import org.osgi.service.prefs.BackingStoreException; 31 import org.osgi.service.prefs.Preferences; 32 import org.osgi.util.tracker.ServiceTracker; 33 34 /** 35 * @since 3.0 36 */ 37 public class DefaultPreferences extends EclipsePreferences { 38 // cache which nodes have been loaded from disk 39 private static Set<String> loadedNodes = Collections.synchronizedSet(new HashSet<String>()); 40 private static final String KEY_PREFIX = "%"; //$NON-NLS-1$ 41 private static final String KEY_DOUBLE_PREFIX = "%%"; //$NON-NLS-1$ 42 private static final IPath NL_DIR = new Path("$nl$"); //$NON-NLS-1$ 43 44 private static final String PROPERTIES_FILE_EXTENSION = "properties"; //$NON-NLS-1$ 45 private static Properties productCustomization; 46 private static Properties productTranslation; 47 private static Properties commandLineCustomization; 48 private EclipsePreferences loadLevel; 49 private Thread initializingThread; 50 51 // cached values 52 private String qualifier; 53 private int segmentCount; 54 private WeakReference<Object> pluginReference; 55 56 public static String pluginCustomizationFile = null; 57 58 /** 59 * Default constructor for this class. 60 */ DefaultPreferences()61 public DefaultPreferences() { 62 this(null, null); 63 } 64 DefaultPreferences(EclipsePreferences parent, String name, Object context)65 private DefaultPreferences(EclipsePreferences parent, String name, Object context) { 66 this(parent, name); 67 this.pluginReference = new WeakReference<>(context); 68 } 69 DefaultPreferences(EclipsePreferences parent, String name)70 private DefaultPreferences(EclipsePreferences parent, String name) { 71 super(parent, name); 72 73 if (parent instanceof DefaultPreferences) 74 this.pluginReference = ((DefaultPreferences) parent).pluginReference; 75 76 // cache the segment count 77 String path = absolutePath(); 78 segmentCount = getSegmentCount(path); 79 if (segmentCount < 2) 80 return; 81 82 // cache the qualifier 83 qualifier = getSegment(path, 1); 84 } 85 86 /* 87 * Apply the values set in the bundle's install directory. 88 * 89 * In Eclipse 2.1 this is equivalent to: 90 * /eclipse/plugins/<pluginID>/prefs.ini 91 */ applyBundleDefaults()92 private void applyBundleDefaults() { 93 Bundle bundle = PreferencesOSGiUtils.getDefault().getBundle(name()); 94 if (bundle == null) 95 return; 96 URL url = FileLocator.find(bundle, new Path(IPreferencesConstants.PREFERENCES_DEFAULT_OVERRIDE_FILE_NAME), null); 97 if (url == null) { 98 if (EclipsePreferences.DEBUG_PREFERENCE_GENERAL) 99 PrefsMessages.message("Preference default override file not found for bundle: " + bundle.getSymbolicName()); //$NON-NLS-1$ 100 return; 101 } 102 URL transURL = FileLocator.find(bundle, NL_DIR.append(IPreferencesConstants.PREFERENCES_DEFAULT_OVERRIDE_BASE_NAME).addFileExtension(PROPERTIES_FILE_EXTENSION), null); 103 if (transURL == null && EclipsePreferences.DEBUG_PREFERENCE_GENERAL) 104 PrefsMessages.message("Preference translation file not found for bundle: " + bundle.getSymbolicName()); //$NON-NLS-1$ 105 applyDefaults(name(), loadProperties(url), loadProperties(transURL)); 106 } 107 108 /* 109 * Apply the default values as specified in the file 110 * as an argument on the command-line. 111 */ applyCommandLineDefaults()112 private void applyCommandLineDefaults() { 113 if (commandLineCustomization != null) 114 applyDefaults(null, commandLineCustomization, null); 115 } 116 117 /* 118 * If the qualifier is null then the file is of the format: 119 * pluginID/key=value 120 * otherwise the file is of the format: 121 * key=value 122 */ applyDefaults(String id, Properties defaultValues, Properties translations)123 private void applyDefaults(String id, Properties defaultValues, Properties translations) { 124 for (Enumeration<?> e = defaultValues.keys(); e.hasMoreElements();) { 125 String fullKey = (String) e.nextElement(); 126 String value = defaultValues.getProperty(fullKey); 127 if (value == null) 128 continue; 129 String localQualifier = id; 130 String fullPath = fullKey; 131 int firstIndex = fullKey.indexOf(PATH_SEPARATOR); 132 if (id == null && firstIndex > 0) { 133 localQualifier = fullKey.substring(0, firstIndex); 134 fullPath = fullKey.substring(firstIndex, fullKey.length()); 135 } 136 String[] splitPath = decodePath(fullPath); 137 String childPath = splitPath[0]; 138 childPath = makeRelative(childPath); 139 String key = splitPath[1]; 140 if (name().equals(localQualifier)) { 141 value = translatePreference(value, translations); 142 if (EclipsePreferences.DEBUG_PREFERENCE_SET) 143 PrefsMessages.message("Setting default preference: " + (new Path(absolutePath()).append(childPath).append(key)) + '=' + value); //$NON-NLS-1$ 144 ((EclipsePreferences) internalNode(childPath.toString(), false, null)).internalPut(key, value); 145 } 146 } 147 } 148 node(String childName, Object context)149 public IEclipsePreferences node(String childName, Object context) { 150 return internalNode(childName, true, context); 151 } 152 containsNode(Properties props, IPath path)153 private boolean containsNode(Properties props, IPath path) { 154 if (props == null) 155 return false; 156 for (Enumeration<?> e = props.keys(); e.hasMoreElements();) { 157 String fullKey = (String) e.nextElement(); 158 if (props.getProperty(fullKey) == null) 159 continue; 160 // remove last segment which stands for key 161 IPath nodePath = new Path(fullKey).removeLastSegments(1); 162 if (path.isPrefixOf(nodePath)) 163 return true; 164 } 165 return false; 166 } 167 168 @Override nodeExists(String path)169 public boolean nodeExists(String path) throws BackingStoreException { 170 // use super implementation for empty and absolute paths 171 if (path.length() == 0 || path.charAt(0) == IPath.SEPARATOR) 172 return super.nodeExists(path); 173 // if the node already exists, nothing more to do 174 if (super.nodeExists(path)) 175 return true; 176 // if the node does not exist, maybe it has not been loaded yet 177 initializeCustomizations(); 178 // scope based path is a path relative to the "/default" node; this is the path that appears in customizations 179 IPath scopeBasedPath = new Path(absolutePath() + PATH_SEPARATOR + path).removeFirstSegments(1); 180 return containsNode(productCustomization, scopeBasedPath) || containsNode(commandLineCustomization, scopeBasedPath); 181 } 182 initializeCustomizations()183 private void initializeCustomizations() { 184 // prime the cache the first time 185 if (productCustomization == null) { 186 BundleContext context = Activator.getContext(); 187 if (context != null) { 188 ServiceTracker<?, IProductPreferencesService> productTracker = new ServiceTracker<>(context, IProductPreferencesService.class, null); 189 productTracker.open(); 190 IProductPreferencesService productSpecials = productTracker.getService(); 191 if (productSpecials != null) { 192 productCustomization = productSpecials.getProductCustomization(); 193 productTranslation = productSpecials.getProductTranslation(); 194 } 195 productTracker.close(); 196 } else { 197 PrefsMessages.message("Product-specified preferences called before plugin is started"); //$NON-NLS-1$ 198 } 199 if (productCustomization == null) 200 productCustomization = new Properties(); 201 } 202 if (commandLineCustomization == null) { 203 String filename = pluginCustomizationFile; 204 if (filename == null) { 205 if (EclipsePreferences.DEBUG_PREFERENCE_GENERAL) 206 PrefsMessages.message("Command-line preferences customization file not specified."); //$NON-NLS-1$ 207 } else { 208 if (EclipsePreferences.DEBUG_PREFERENCE_GENERAL) 209 PrefsMessages.message("Using command-line preference customization file: " + filename); //$NON-NLS-1$ 210 commandLineCustomization = loadProperties(filename); 211 } 212 } 213 } 214 215 /* 216 * Runtime defaults are the ones which are specified in code at runtime. 217 * 218 * In the Eclipse 2.1 world they were the ones which were specified in the 219 * over-ridden Plugin#initializeDefaultPluginPreferences() method. 220 * 221 * In Eclipse 3.0 they are set in the code which is indicated by the 222 * extension to the plug-in default customizer extension point. 223 */ applyRuntimeDefaults()224 private void applyRuntimeDefaults() { 225 WeakReference<Object> ref = PreferencesService.getDefault().applyRuntimeDefaults(name(), pluginReference); 226 if (ref != null) 227 pluginReference = ref; 228 } 229 230 /* 231 * Apply the default values as specified by the file 232 * in the product extension. 233 * 234 * In Eclipse 2.1 this is equivalent to the plugin_customization.ini 235 * file in the primary feature's plug-in directory. 236 */ applyProductDefaults()237 private void applyProductDefaults() { 238 if (!productCustomization.isEmpty()) 239 applyDefaults(null, productCustomization, productTranslation); 240 } 241 242 243 @Override flush()244 public void flush() { 245 // default values are not persisted 246 } 247 248 @Override getLoadLevel()249 protected IEclipsePreferences getLoadLevel() { 250 if (loadLevel == null) { 251 if (qualifier == null) 252 return null; 253 // Make it relative to this node rather than navigating to it from the root. 254 // Walk backwards up the tree starting at this node. 255 // This is important to avoid a chicken/egg thing on startup. 256 EclipsePreferences node = this; 257 for (int i = 2; i < segmentCount; i++) 258 node = (EclipsePreferences) node.parent(); 259 loadLevel = node; 260 } 261 return loadLevel; 262 } 263 264 @Override internalCreate(EclipsePreferences nodeParent, String nodeName, Object context)265 protected EclipsePreferences internalCreate(EclipsePreferences nodeParent, String nodeName, Object context) { 266 return new DefaultPreferences(nodeParent, nodeName, context); 267 } 268 269 @Override isAlreadyLoaded(IEclipsePreferences node)270 protected boolean isAlreadyLoaded(IEclipsePreferences node) { 271 return loadedNodes.contains(node.name()); 272 } 273 274 275 @Override load()276 protected void load() { 277 setInitializingBundleDefaults(); 278 try { 279 applyRuntimeDefaults(); 280 applyBundleDefaults(); 281 } finally { 282 clearInitializingBundleDefaults(); 283 } 284 initializeCustomizations(); 285 applyProductDefaults(); 286 applyCommandLineDefaults(); 287 } 288 289 290 @Override internalPut(String key, String newValue)291 protected String internalPut(String key, String newValue) { 292 // set the value in this node 293 String result = super.internalPut(key, newValue); 294 295 // if we are setting the bundle defaults, then set the corresponding value in 296 // the bundle_defaults scope 297 if (isInitializingBundleDefaults()) { 298 String relativePath = getScopeRelativePath(absolutePath()); 299 if (relativePath != null) { 300 Preferences node = PreferencesService.getDefault().getRootNode().node(BundleDefaultsScope.SCOPE).node(relativePath); 301 node.put(key, newValue); 302 } 303 } 304 return result; 305 } 306 307 /* 308 * Set that we are in the middle of initializing the bundle defaults. 309 * This is stored on the load level so we know where to look when 310 * we are setting values on sub-nodes. 311 */ setInitializingBundleDefaults()312 private void setInitializingBundleDefaults() { 313 IEclipsePreferences node = getLoadLevel(); 314 if (node instanceof DefaultPreferences) { 315 DefaultPreferences loader = (DefaultPreferences) node; 316 loader.initializingThread = Thread.currentThread(); 317 } 318 } 319 320 /* 321 * Clear the bit saying we are in the middle of initializing the bundle defaults. 322 * This is stored on the load level so we know where to look when 323 * we are setting values on sub-nodes. 324 */ clearInitializingBundleDefaults()325 private void clearInitializingBundleDefaults() { 326 IEclipsePreferences node = getLoadLevel(); 327 if (node instanceof DefaultPreferences) { 328 DefaultPreferences loader = (DefaultPreferences) node; 329 loader.initializingThread = null; 330 } 331 } 332 333 /* 334 * Are we in the middle of initializing defaults from the bundle 335 * initializer or found in the bundle itself? Look on the load level in 336 * case we are in a sub-node. 337 */ isInitializingBundleDefaults()338 private boolean isInitializingBundleDefaults() { 339 IEclipsePreferences node = getLoadLevel(); 340 if (node instanceof DefaultPreferences) { 341 DefaultPreferences loader = (DefaultPreferences) node; 342 return loader.initializingThread == Thread.currentThread(); 343 } 344 return false; 345 } 346 347 /* 348 * Return a path which is relative to the scope of this node. 349 * e.g. com.example.foo for /instance/com.example.foo 350 */ getScopeRelativePath(String absolutePath)351 protected static String getScopeRelativePath(String absolutePath) { 352 // shouldn't happen but handle empty or root 353 if (absolutePath.length() < 2) 354 return null; 355 int index = absolutePath.indexOf('/', 1); 356 if (index == -1 || index + 1 >= absolutePath.length()) 357 return null; 358 return absolutePath.substring(index + 1); 359 } 360 loadProperties(URL url)361 private Properties loadProperties(URL url) { 362 Properties result = new Properties(); 363 if (url == null) 364 return result; 365 InputStream input = null; 366 try { 367 input = url.openStream(); 368 result.load(input); 369 } catch (IOException | IllegalArgumentException e) { 370 if (EclipsePreferences.DEBUG_PREFERENCE_GENERAL) { 371 PrefsMessages.message("Problem opening stream to preference customization file: " + url); //$NON-NLS-1$ 372 e.printStackTrace(); 373 } 374 } 375 finally { 376 if (input != null) 377 try { 378 input.close(); 379 } catch (IOException e) { 380 // ignore 381 } 382 } 383 return result; 384 } 385 loadProperties(String filename)386 private Properties loadProperties(String filename) { 387 Properties result = new Properties(); 388 InputStream input = null; 389 try { 390 input = new BufferedInputStream(new FileInputStream(filename)); 391 result.load(input); 392 } catch (FileNotFoundException e) { 393 if (EclipsePreferences.DEBUG_PREFERENCE_GENERAL) 394 PrefsMessages.message("Preference customization file not found: " + filename); //$NON-NLS-1$ 395 } catch (IOException e) { 396 String message = NLS.bind(PrefsMessages.preferences_loadException, filename); 397 IStatus status = new Status(IStatus.ERROR, PrefsMessages.OWNER_NAME, IStatus.ERROR, message, e); 398 RuntimeLog.log(status); 399 } catch (IllegalArgumentException e) { 400 String message = NLS.bind(PrefsMessages.preferences_loadException, filename); 401 IStatus status = new Status(IStatus.ERROR, PrefsMessages.OWNER_NAME, IStatus.ERROR, message, e); 402 RuntimeLog.log(status); 403 } finally { 404 if (input != null) 405 try { 406 input.close(); 407 } catch (IOException e) { 408 // ignore 409 } 410 } 411 return result; 412 } 413 414 @Override loaded()415 protected void loaded() { 416 loadedNodes.add(name()); 417 } 418 419 420 @Override sync()421 public void sync() { 422 // default values are not persisted 423 } 424 425 /** 426 * Takes a preference value and a related resource bundle and 427 * returns the translated version of this value (if one exists). 428 */ translatePreference(String origValue, Properties props)429 private String translatePreference(String origValue, Properties props) { 430 if (props == null || origValue.startsWith(KEY_DOUBLE_PREFIX)) 431 return origValue; 432 if (origValue.startsWith(KEY_PREFIX)) { 433 String value = origValue.trim(); 434 int ix = value.indexOf(" "); //$NON-NLS-1$ 435 String key = ix == -1 ? value.substring(1) : value.substring(1, ix); 436 String dflt = ix == -1 ? value : value.substring(ix + 1); 437 return props.getProperty(key, dflt); 438 } 439 return origValue; 440 } 441 } 442