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