package org.eclipse.che.core.internal.preferences; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.internal.preferences.Base64; import org.eclipse.core.internal.preferences.ImmutableMap; import org.eclipse.core.internal.preferences.PrefsMessages; import org.eclipse.core.internal.preferences.SafeFileInputStream; import org.eclipse.core.internal.preferences.SafeFileOutputStream; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.preferences.IEclipsePreferences; import org.eclipse.core.runtime.preferences.IPreferenceNodeVisitor; import org.osgi.service.prefs.BackingStoreException; import org.osgi.service.prefs.Preferences; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Collections; import java.util.Comparator; import java.util.Enumeration; import java.util.Iterator; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.TreeSet; /** * @author Evgen Vidolob */ public class ChePreferences implements IEclipsePreferences { protected static final String VERSION_KEY = "eclipse.preferences.version"; //$NON-NLS-1$ protected static final String VERSION_VALUE = "1"; //$NON-NLS-1$ protected static final String DOUBLE_SLASH = "//"; //$NON-NLS-1$ protected static final String EMPTY_STRING = ""; //$NON-NLS-1$ private static final String FALSE = "false"; //$NON-NLS-1$ private static final String TRUE = "true"; //$NON-NLS-1$ /** * Protects write access to properties and children. */ protected ImmutableMap properties = ImmutableMap.EMPTY; private final Object childAndPropertyLock = new Object(); protected boolean dirty = false; private final String filePath; public ChePreferences(String filePath) { this.filePath = filePath; } /** * Loads the preference node. This method returns silently if the node does not exist * in the backing store (for example non-existent project). * * @throws BackingStoreException if the node exists in the backing store but it * could not be loaded */ protected void load() throws BackingStoreException { load(filePath); } protected void load(String location) throws BackingStoreException { if (location == null) { return; } Properties fromDisk = loadProperties(location); convertFromProperties(this, fromDisk, false); } protected static Properties loadProperties(String location) throws BackingStoreException { // if (DEBUG_PREFERENCE_GENERAL) // PrefsMessages.message("Loading preferences from file: " + location); //$NON-NLS-1$ InputStream input = null; Properties result = new Properties(); try { input = new SafeFileInputStream(new File(location)); result.load(input); } catch (FileNotFoundException e) { // file doesn't exist but that's ok. // if (DEBUG_PREFERENCE_GENERAL) // PrefsMessages.message("Preference file does not exist: " + location); //$NON-NLS-1$ return result; } catch (IOException e) { // String message = NLS.bind(PrefsMessages.preferences_loadException, location); // log(new Status(IStatus.INFO, PrefsMessages.OWNER_NAME, IStatus.INFO, message, e)); throw new BackingStoreException(e.getMessage(), e); } finally { if (input != null) try { input.close(); } catch (IOException e) { // ignore } } return result; } /* * Helper method to convert this node to a Properties file suitable * for persistence. */ protected Properties convertToProperties(Properties result, String prefix) throws BackingStoreException { // add the key/value pairs from this node boolean addSeparator = prefix.length() != 0; //thread safety: copy reference in case of concurrent change ImmutableMap temp; synchronized (childAndPropertyLock) { temp = properties; } String[] keys = temp.keys(); for (int i = 0, imax = keys.length; i < imax; i++) { String value = temp.get(keys[i]); if (value != null) result.put(encodePath(prefix, keys[i]), value); } // // recursively add the child information // IEclipsePreferences[] childNodes = getChildren(true); // for (int i = 0; i < childNodes.length; i++) { // ChePreferences child = (ChePreferences) childNodes[i]; // String fullPath = addSeparator ? prefix + PATH_SEPARATOR + child.name() : child.name(); // child.convertToProperties(result, fullPath); // } return result; } /* * Encode the given path and key combo to a form which is suitable for * persisting or using when searching. If the key contains a slash character * then we must use a double-slash to indicate the end of the * path/the beginning of the key. */ public static String encodePath(String path, String key) { String result; int pathLength = path == null ? 0 : path.length(); if (key.indexOf(IPath.SEPARATOR) == -1) { if (pathLength == 0) result = key; else result = path + IPath.SEPARATOR + key; } else { if (pathLength == 0) result = DOUBLE_SLASH + key; else result = path + DOUBLE_SLASH + key; } return result; } /* * Version 1 (current version) * path/key=value */ protected static void convertFromProperties(ChePreferences node, Properties table, boolean notify) { String version = table.getProperty(VERSION_KEY); if (version == null || !VERSION_VALUE.equals(version)) { // ignore for now } table.remove(VERSION_KEY); for (Iterator i = table.keySet().iterator(); i.hasNext();) { String fullKey = (String) i.next(); String value = table.getProperty(fullKey); if (value != null) { String[] splitPath = decodePath(fullKey); String path = splitPath[0]; path = makeRelative(path); String key = splitPath[1]; // if (DEBUG_PREFERENCE_SET) // PrefsMessages.message("Setting preference: " + path + '/' + key + '=' + value); //$NON-NLS-1$ //use internal methods to avoid notifying listeners ChePreferences childNode = (ChePreferences) node.internalNode(path, false, null); String oldValue = childNode.internalPut(key, value); // // notify listeners if applicable // if (notify && !value.equals(oldValue)) // childNode.firePreferenceEvent(key, oldValue, value); } } // PreferencesService.getDefault().shareStrings(); } /** * Stores the given (key,value) pair, performing lazy initialization of the * properties field if necessary. Returns the old value for the given key, * or null if no value existed. */ protected String internalPut(String key, String newValue) { synchronized (childAndPropertyLock) { // illegal state if this node has been removed // checkRemoved(); String oldValue = properties.get(key); if (oldValue != null && oldValue.equals(newValue)) return oldValue; // if (DEBUG_PREFERENCE_SET) // PrefsMessages.message("Setting preference: " + absolutePath() + '/' + key + '=' + newValue); //$NON-NLS-1$ properties = properties.put(key, newValue); return oldValue; } } /** * Implements the node(String) method, and optionally notifies listeners. */ protected IEclipsePreferences internalNode(String path, boolean notify, Object context) { // illegal state if this node has been removed // checkRemoved(); // short circuit this node // if (path.length() == 0) //TODO only return this; // // // if we have an absolute path use the root relative to // // this node instead of the global root // // in case we have a different hierarchy. (e.g. export) // if (path.charAt(0) == IPath.SEPARATOR) // return (IEclipsePreferences) calculateRoot().node(path.substring(1)); // // int index = path.indexOf(IPath.SEPARATOR); // String key = index == -1 ? path : path.substring(0, index); // boolean added = false; // IEclipsePreferences child = getChild(key, context, true); // if (child == null) { // child = create(this, key, context); // added = true; // } // // notify listeners if a child was added // if (added && notify) // fireNodeEvent(new NodeChangeEvent(this, child), true); // return (IEclipsePreferences) child.node(index == -1 ? EMPTY_STRING : path.substring(index + 1)); } // /** // * Thread safe way to obtain a child for a given key. Returns the child // * that matches the given key, or null if there is no matching child. // */ // protected IEclipsePreferences getChild(String key, Object context, boolean create) { // synchronized (childAndPropertyLock) { // if (children == null) // return null; // Object value = children.get(key); // if (value == null) // return null; // if (value instanceof IEclipsePreferences) // return (IEclipsePreferences) value; // // if we aren't supposed to create this node, then // // just return null // if (!create) // return null; // } // return addChild(key, create(this, key, context)); // } private IEclipsePreferences calculateRoot() { IEclipsePreferences result = this; while (result.parent() != null) result = (IEclipsePreferences) result.parent(); return result; } /* * Return a relative path */ public static String makeRelative(String path) { String result = path; if (path == null) return EMPTY_STRING; if (path.length() > 0 && path.charAt(0) == IPath.SEPARATOR) result = path.length() == 0 ? EMPTY_STRING : path.substring(1); return result; } /* * Return a 2 element String array. * element 0 - the path * element 1 - the key * The path may be null. * The key is never null. */ public static String[] decodePath(String fullPath) { String key = null; String path = null; // check to see if we have an indicator which tells us where the path ends int index = fullPath.indexOf(DOUBLE_SLASH); if (index == -1) { // we don't have a double-slash telling us where the path ends // so the path is up to the last slash character int lastIndex = fullPath.lastIndexOf(IPath.SEPARATOR); if (lastIndex == -1) { key = fullPath; } else { path = fullPath.substring(0, lastIndex); key = fullPath.substring(lastIndex + 1); } } else { // the child path is up to the double-slash and the key // is the string after it path = fullPath.substring(0, index); key = fullPath.substring(index + 2); } // adjust if we have an absolute path if (path != null) if (path.length() == 0) path = null; else if (path.charAt(0) == IPath.SEPARATOR) path = path.substring(1); return new String[] {path, key}; } @Override public void addNodeChangeListener(INodeChangeListener listener) { throw new UnsupportedOperationException(); } @Override public void removeNodeChangeListener(INodeChangeListener listener) { throw new UnsupportedOperationException(); } @Override public void addPreferenceChangeListener(IPreferenceChangeListener listener) { throw new UnsupportedOperationException(); } @Override public void removePreferenceChangeListener(IPreferenceChangeListener listener) { throw new UnsupportedOperationException(); } @Override public void removeNode() throws BackingStoreException { throw new UnsupportedOperationException(); } @Override public String name() { throw new UnsupportedOperationException(); } @Override public String absolutePath() { throw new UnsupportedOperationException(); } @Override public void flush() throws BackingStoreException { IEclipsePreferences toFlush = null; synchronized (childAndPropertyLock) { toFlush = internalFlush(); } //if we aren't at the right level, then flush the appropriate node if (toFlush != null) toFlush.flush(); } /* * Do the real flushing in a non-synchronized internal method so sub-classes * (mainly ProjectPreferences and ProfilePreferences) don't cause deadlocks. * * If this node is not responsible for persistence (a load level), then this method * returns the node that should be flushed. Returns null if this method performed * the flush. */ protected IEclipsePreferences internalFlush() throws BackingStoreException { // illegal state if this node has been removed // checkRemoved(); // IEclipsePreferences loadLevel = null;//getLoadLevel(); // // if this node or a parent is not the load level, then flush the children // if (loadLevel == null) { // String[] childrenNames = childrenNames(); // for (int i = 0; i < childrenNames.length; i++) // node(childrenNames[i]).flush(); // return null; // } // // // a parent is the load level for this node // if (this != loadLevel) // return loadLevel; // this node is a load level // any work to do? if (!dirty) return null; //remove dirty bit before saving, to ensure that concurrent //changes during save mark the store as dirty dirty = false; try { save(); } catch (BackingStoreException e) { //mark it dirty again because the save failed dirty = true; throw e; } return null; } /** * Saves the preference node. This method returns silently if the node does not exist * in the backing store (for example non-existent project) * * @throws BackingStoreException if the node exists in the backing store but it * could not be saved */ protected void save() throws BackingStoreException { // if (descriptor == null) { save(filePath); // } else { // descriptor.save(absolutePath(), convertToProperties(new Properties(), "")); //$NON-NLS-1$ // } } protected void save(String location) throws BackingStoreException { if (location == null) { // if (DEBUG_PREFERENCE_GENERAL) // PrefsMessages.message("Unable to determine location of preference file for node: " + absolutePath()); //$NON-NLS-1$ return; } // if (DEBUG_PREFERENCE_GENERAL) // PrefsMessages.message("Saving preferences to file: " + location); //$NON-NLS-1$ Properties table = convertToProperties(new SortedProperties(), EMPTY_STRING); if (table.isEmpty()) { File file = new File(location); // nothing to save. delete existing file if one exists. if (file.exists() && !file.delete()) { // String message = NLS.bind(PrefsMessages.preferences_failedDelete, location); ResourcesPlugin.log(new Status(IStatus.WARNING, PrefsMessages.OWNER_NAME, IStatus.WARNING, "preferences save failed, file was delete", null)); } return; } table.put(VERSION_KEY, VERSION_VALUE); write(table, location); } /* * Helper method to persist a Properties object to the filesystem. We use this * helper so we can remove the date/timestamp that Properties#store always * puts in the file. */ protected static void write(Properties properties, String location) throws BackingStoreException { // create the parent directories if they don't exist // File parentFile = new File(location); // if (parentFile == null) // return; // parentFile.mkdirs(); OutputStream output = null; try { File file = new File(location); if(!file.exists()){ File parentFile = file.getParentFile(); if(!parentFile.exists()){ parentFile.mkdirs(); } file.createNewFile(); } output = new SafeFileOutputStream(file); output.write(removeTimestampFromTable(properties).getBytes("UTF-8")); //$NON-NLS-1$ output.flush(); } catch (IOException e) { // String message = NLS.bind(PrefsMessages.preferences_saveException, location); ResourcesPlugin.log(new Status(IStatus.ERROR, PrefsMessages.OWNER_NAME, IStatus.ERROR, "preferences_saveException", e)); throw new BackingStoreException("preferences_saveException"); } finally { if (output != null) try { output.close(); } catch (IOException e) { // ignore } } } protected static String removeTimestampFromTable(Properties properties) throws IOException { // store the properties in a string and then skip the first line (date/timestamp) ByteArrayOutputStream output = new ByteArrayOutputStream(); try { properties.store(output, null); } finally { output.close(); } String string = output.toString("UTF-8"); //$NON-NLS-1$ String separator = System.getProperty("line.separator"); //$NON-NLS-1$ return string.substring(string.indexOf(separator) + separator.length()); } @Override public void sync() throws BackingStoreException { load(); flush(); } @Override public void put(String key, String newValue) { if (key == null || newValue == null) throw new NullPointerException(); String oldValue = internalPut(key, newValue); if (!newValue.equals(oldValue)) { makeDirty(); // firePreferenceEvent(key, oldValue, newValue); } } protected void makeDirty() { // EclipsePreferences node = this; // while (node != null && !node.removed) { dirty = true; // node = (EclipsePreferences) node.parent(); // } } @Override public String get(String key, String defaultValue) { String value = internalGet(key); return value == null ? defaultValue : value; } /** * Returns the existing value at the given key, or null if * no such value exists. */ protected String internalGet(String key) { // throw NPE if key is null if (key == null) throw new NullPointerException(); // illegal state if this node has been removed // checkRemoved(); String result; synchronized (childAndPropertyLock) { result = properties.get(key); } // if (DEBUG_PREFERENCE_GET) // PrefsMessages.message("Getting preference value: " + absolutePath() + '/' + key + "->" + result); //$NON-NLS-1$ //$NON-NLS-2$ return result; } @Override public void remove(String key) { String oldValue; synchronized (childAndPropertyLock) { // illegal state if this node has been removed // checkRemoved(); oldValue = properties.get(key); if (oldValue == null) return; properties = properties.removeKey(key); } makeDirty(); } @Override public void clear() throws BackingStoreException { // illegal state if this node has been removed // checkRemoved(); // call each one separately (instead of Properties.clear) so // clients get change notification String[] keys; synchronized (childAndPropertyLock) { keys = properties.keys(); } //don't synchronize remove call because it calls listeners for (int i = 0; i < keys.length; i++) remove(keys[i]); makeDirty(); } @Override public void putInt(String key, int value) { if (key == null) throw new NullPointerException(); String newValue = Integer.toString(value); String oldValue = internalPut(key, newValue); if (!newValue.equals(oldValue)) { makeDirty(); // firePreferenceEvent(key, oldValue, newValue); } } @Override public int getInt(String key, int defaultValue) { String value = internalGet(key); int result = defaultValue; if (value != null) try { result = Integer.parseInt(value); } catch (NumberFormatException e) { // use default } return result; } @Override public void putLong(String key, long value) { if (key == null) throw new NullPointerException(); String newValue = Long.toString(value); String oldValue = internalPut(key, newValue); if (!newValue.equals(oldValue)) { makeDirty(); // firePreferenceEvent(key, oldValue, newValue); } } @Override public long getLong(String key, long defaultValue) { String value = internalGet(key); long result = defaultValue; if (value != null) try { result = Long.parseLong(value); } catch (NumberFormatException e) { // use default } return result; } @Override public void putBoolean(String key, boolean value) { if (key == null) throw new NullPointerException(); String newValue = value ? TRUE : FALSE; String oldValue = internalPut(key, newValue); if (!newValue.equals(oldValue)) { makeDirty(); // firePreferenceEvent(key, oldValue, newValue); } } @Override public boolean getBoolean(String key, boolean defaultValue) { String value = internalGet(key); return value == null ? defaultValue : TRUE.equalsIgnoreCase(value); } @Override public void putFloat(String key, float value) { if (key == null) throw new NullPointerException(); String newValue = Float.toString(value); String oldValue = internalPut(key, newValue); if (!newValue.equals(oldValue)) { makeDirty(); // firePreferenceEvent(key, oldValue, newValue); } } @Override public float getFloat(String key, float defaultValue) { String value = internalGet(key); float result = defaultValue; if (value != null) try { result = Float.parseFloat(value); } catch (NumberFormatException e) { // use default } return result; } @Override public void putDouble(String key, double value) { if (key == null) throw new NullPointerException(); String newValue = Double.toString(value); String oldValue = internalPut(key, newValue); if (!newValue.equals(oldValue)) { makeDirty(); // firePreferenceEvent(key, oldValue, newValue); } } @Override public double getDouble(String key, double defaultValue) { String value = internalGet(key); double result = defaultValue; if (value != null) try { result = Double.parseDouble(value); } catch (NumberFormatException e) { // use default } return result; } @Override public void putByteArray(String key, byte[] value) { if (key == null || value == null) throw new NullPointerException(); String newValue = new String(Base64.encode(value)); String oldValue = internalPut(key, newValue); if (!newValue.equals(oldValue)) { makeDirty(); // firePreferenceEvent(key, oldValue, newValue); } } @Override public byte[] getByteArray(String key, byte[] defaultValue) { String value = internalGet(key); return value == null ? defaultValue : Base64.decode(value.getBytes()); } @Override public String[] keys() throws BackingStoreException { // illegal state if this node has been removed synchronized (childAndPropertyLock) { // checkRemoved(); return properties.keys(); } } @Override public String[] childrenNames() throws BackingStoreException { throw new UnsupportedOperationException(); } @Override public Preferences parent() { throw new UnsupportedOperationException(); } @Override public Preferences node(String path) { throw new UnsupportedOperationException(); } @Override public boolean nodeExists(String pathName) throws BackingStoreException { return false; } @Override public void accept(IPreferenceNodeVisitor visitor) throws BackingStoreException { throw new UnsupportedOperationException(); } protected class SortedProperties extends Properties { private static final long serialVersionUID = 1L; public SortedProperties() { super(); } /* (non-Javadoc) * @see java.util.Hashtable#keys() */ public synchronized Enumeration keys() { TreeSet set = new TreeSet(); for (Enumeration e = super.keys(); e.hasMoreElements();) set.add(e.nextElement()); return Collections.enumeration(set); } /* (non-Javadoc) * @see java.util.Hashtable#entrySet() */ public Set entrySet() { TreeSet set = new TreeSet(new Comparator() { public int compare(Object e1, Object e2) { String s1 = (String) ((Map.Entry) e1).getKey(); String s2 = (String) ((Map.Entry) e2).getKey(); return s1.compareTo(s2); } }); for (Iterator i = super.entrySet().iterator(); i.hasNext();) set.add(i.next()); return set; } } }