| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.apache.felix.prefs; |
| |
| import java.io.UnsupportedEncodingException; |
| import java.util.*; |
| |
| import org.apache.commons.codec.binary.Base64; |
| import org.osgi.service.prefs.BackingStoreException; |
| import org.osgi.service.prefs.Preferences; |
| |
| /** |
| * This is an implementation of the preferences. |
| * |
| * The access to the preferences is synchronized on the instance |
| * by making (nearly) all public methods synchronized. This avoids the |
| * heavy management of a separate read/write lock. Such a lock |
| * is too heavy for the simple operations preferences support. |
| * The various getXX and putXX methods are not synchronized as they |
| * all use the get/put methods which are synchronized. |
| */ |
| public class PreferencesImpl implements Preferences { |
| |
| /** The properties. */ |
| protected final Map properties = new HashMap(); |
| |
| /** Has this node been removed? */ |
| protected boolean valid = true; |
| |
| /** The parent. */ |
| protected final PreferencesImpl parent; |
| |
| /** The child nodes. */ |
| protected final Map children = new HashMap(); |
| |
| /** The name of the properties. */ |
| protected final String name; |
| |
| /** The description for this preferences. */ |
| protected final PreferencesDescription description; |
| |
| /** The backing store manager. */ |
| protected final BackingStoreManager storeManager; |
| |
| /** The change set keeps track of all changes. */ |
| protected final ChangeSet changeSet = new ChangeSet(); |
| |
| /** |
| * Construct the root node of the tree. |
| * @param d The unique description. |
| * @param storeManager The backing store. |
| */ |
| public PreferencesImpl(PreferencesDescription d, BackingStoreManager storeManager) { |
| this.parent = null; |
| this.name = ""; |
| this.description = d; |
| this.storeManager = storeManager; |
| } |
| |
| /** |
| * Construct a child node. |
| * @param p The parent node. |
| * @param name The node name |
| */ |
| public PreferencesImpl(PreferencesImpl p, String name) { |
| this.parent = p; |
| this.name = name; |
| this.description = p.description; |
| this.storeManager = p.storeManager; |
| } |
| |
| /** |
| * Return the change set. |
| */ |
| public ChangeSet getChangeSet() { |
| return this.changeSet; |
| } |
| |
| /** |
| * Return the preferences description. |
| * @return The preferences description. |
| */ |
| public PreferencesDescription getDescription() { |
| return this.description; |
| } |
| |
| /** |
| * Get the root preferences. |
| */ |
| public PreferencesImpl getRoot() { |
| PreferencesImpl root = this; |
| while ( root.parent != null ) { |
| root = root.parent; |
| } |
| return root; |
| } |
| |
| /** |
| * Return all children or an empty collection. |
| * @return A collection containing the children. |
| */ |
| public Collection getChildren() { |
| return this.children.values(); |
| } |
| |
| /** |
| * Return the properties set. |
| */ |
| public Map getProperties() { |
| return this.properties; |
| } |
| |
| /** |
| * Return the backing store manager. |
| */ |
| public BackingStoreManager getBackingStoreManager() { |
| return this.storeManager; |
| } |
| |
| /** |
| * Check if this node is still valid. |
| * It gets invalid if it has been removed. |
| */ |
| protected void checkValidity() throws IllegalStateException { |
| if ( !this.valid ) { |
| throw new IllegalStateException("The preferences node has been removed."); |
| } |
| } |
| |
| /** |
| * The key is not allowed to be null. |
| */ |
| protected void checkKey(String key) throws NullPointerException { |
| if ( key == null ) { |
| throw new NullPointerException("Key must not be null."); |
| } |
| } |
| |
| /** |
| * The value is not allowed to be null. |
| */ |
| protected void checkValue(Object value) throws NullPointerException { |
| if ( value == null ) { |
| throw new NullPointerException("Value must not be null."); |
| } |
| } |
| |
| public synchronized boolean isValid() { |
| return this.valid; |
| } |
| |
| /** |
| * @see org.osgi.service.prefs.Preferences#put(java.lang.String, java.lang.String) |
| */ |
| public synchronized void put(String key, String value) { |
| this.checkKey(key); |
| this.checkValue(value); |
| |
| this.checkValidity(); |
| |
| this.properties.put(key, value); |
| this.changeSet.propertyChanged(key); |
| } |
| |
| /** |
| * @see org.osgi.service.prefs.Preferences#get(java.lang.String, java.lang.String) |
| */ |
| public synchronized String get(String key, String def) { |
| this.checkValidity(); |
| String value = (String) this.properties.get(key); |
| if ( value == null ) { |
| value = def; |
| } |
| return value; |
| } |
| |
| /** |
| * @see org.osgi.service.prefs.Preferences#remove(java.lang.String) |
| */ |
| public synchronized void remove(String key) { |
| this.checkKey(key); |
| this.checkValidity(); |
| |
| this.properties.remove(key); |
| this.changeSet.propertyRemoved(key); |
| } |
| |
| /** |
| * @see org.osgi.service.prefs.Preferences#clear() |
| */ |
| public synchronized void clear() throws BackingStoreException { |
| this.checkValidity(); |
| |
| final Iterator i = this.properties.keySet().iterator(); |
| while ( i.hasNext() ) { |
| final String key = (String)i.next(); |
| this.changeSet.propertyRemoved(key); |
| } |
| this.properties.clear(); |
| } |
| |
| /** |
| * @see org.osgi.service.prefs.Preferences#putInt(java.lang.String, int) |
| */ |
| public void putInt(String key, int value) { |
| this.put(key, String.valueOf(value)); |
| } |
| |
| /** |
| * @see org.osgi.service.prefs.Preferences#getInt(java.lang.String, int) |
| */ |
| public int getInt(String key, int def) { |
| int result = def; |
| final String value = this.get(key, null); |
| if ( value != null ) { |
| try { |
| result = Integer.parseInt(value); |
| } catch (NumberFormatException ignore) { |
| // return the default value |
| } |
| } |
| return result; |
| } |
| |
| /** |
| * @see org.osgi.service.prefs.Preferences#putLong(java.lang.String, long) |
| */ |
| public void putLong(String key, long value) { |
| this.put(key, String.valueOf(value)); |
| } |
| |
| /** |
| * @see org.osgi.service.prefs.Preferences#getLong(java.lang.String, long) |
| */ |
| public long getLong(String key, long def) { |
| long result = def; |
| final String value = this.get(key, null); |
| if ( value != null ) { |
| try { |
| result = Long.parseLong(value); |
| } catch (NumberFormatException ignore) { |
| // return the default value |
| } |
| } |
| return result; |
| } |
| |
| /** |
| * @see org.osgi.service.prefs.Preferences#putBoolean(java.lang.String, boolean) |
| */ |
| public void putBoolean(String key, boolean value) { |
| this.put(key, String.valueOf(value)); |
| } |
| |
| /** |
| * @see org.osgi.service.prefs.Preferences#getBoolean(java.lang.String, boolean) |
| */ |
| public boolean getBoolean(String key, boolean def) { |
| boolean result = def; |
| final String value = this.get(key, null); |
| if ( value != null ) { |
| if ( value.equalsIgnoreCase("true") ) { |
| result = true; |
| } else if ( value.equalsIgnoreCase("false") ) { |
| result = false; |
| } |
| } |
| return result; |
| } |
| |
| /** |
| * @see org.osgi.service.prefs.Preferences#putFloat(java.lang.String, float) |
| */ |
| public void putFloat(String key, float value) { |
| this.put(key, String.valueOf(value)); |
| } |
| |
| /** |
| * @see org.osgi.service.prefs.Preferences#getFloat(java.lang.String, float) |
| */ |
| public float getFloat(String key, float def) { |
| float result = def; |
| final String value = this.get(key, null); |
| if ( value != null ) { |
| try { |
| result = Float.parseFloat(value); |
| } catch (NumberFormatException ignore) { |
| // return the default value |
| } |
| } |
| return result; |
| } |
| |
| /** |
| * @see org.osgi.service.prefs.Preferences#putDouble(java.lang.String, double) |
| */ |
| public void putDouble(String key, double value) { |
| this.put(key, String.valueOf(value)); |
| } |
| |
| /** |
| * @see org.osgi.service.prefs.Preferences#getDouble(java.lang.String, double) |
| */ |
| public double getDouble(String key, double def) { |
| double result = def; |
| final String value = this.get(key, null); |
| if ( value != null ) { |
| try { |
| result = Double.parseDouble(value); |
| } catch (NumberFormatException ignore) { |
| // return the default value |
| } |
| } |
| return result; |
| } |
| |
| /** |
| * @see org.osgi.service.prefs.Preferences#putByteArray(java.lang.String, byte[]) |
| */ |
| public void putByteArray(String key, byte[] value) { |
| this.checkKey(key); |
| this.checkValue(value); |
| try { |
| this.put(key, new String(Base64.encodeBase64(value), "utf-8")); |
| } catch (UnsupportedEncodingException ignore) { |
| // utf-8 is always available |
| } |
| } |
| |
| /** |
| * @see org.osgi.service.prefs.Preferences#getByteArray(java.lang.String, byte[]) |
| */ |
| public byte[] getByteArray(String key, byte[] def) { |
| byte[] result = def; |
| String value = this.get(key, null); |
| if ( value != null ) { |
| try { |
| result = Base64.decodeBase64(value.getBytes("utf-8")); |
| } catch (UnsupportedEncodingException ignore) { |
| // utf-8 is always available |
| } |
| } |
| return result; |
| } |
| |
| /** |
| * @see org.osgi.service.prefs.Preferences#keys() |
| */ |
| public synchronized String[] keys() throws BackingStoreException { |
| this.sync(); |
| final Set keys = this.properties.keySet(); |
| return (String[])keys.toArray(new String[keys.size()]); |
| } |
| |
| /** |
| * @see org.osgi.service.prefs.Preferences#childrenNames() |
| */ |
| public synchronized String[] childrenNames() throws BackingStoreException { |
| this.sync(); |
| final Set names = this.children.keySet(); |
| return (String[])names.toArray(new String[names.size()]); |
| } |
| |
| /** |
| * @see org.osgi.service.prefs.Preferences#parent() |
| */ |
| public Preferences parent() { |
| this.checkValidity(); |
| return this.parent; |
| } |
| |
| /** |
| * We do not synchronize this method to avoid dead locks as this |
| * method might call another preferences object in the hierarchy. |
| * @see org.osgi.service.prefs.Preferences#node(java.lang.String) |
| */ |
| public Preferences node(String pathName) { |
| if ( pathName == null ) { |
| throw new NullPointerException("Path must not be null."); |
| } |
| PreferencesImpl executingNode= this; |
| synchronized ( this ) { |
| this.checkValidity(); |
| if ( pathName.length() == 0 ) { |
| return this; |
| } |
| if ( pathName.startsWith("/") && this.parent != null ) { |
| executingNode = this.getRoot(); |
| } |
| if ( pathName.startsWith("/") ) { |
| pathName = pathName.substring(1); |
| } |
| } |
| return executingNode.getNode(pathName, true, true); |
| } |
| |
| /** |
| * Get or create the node. |
| * If the node already exists, it's just returned. If not |
| * it is created. |
| * @param pathName |
| * @return The preferences impl for the path. |
| */ |
| public PreferencesImpl getOrCreateNode(String pathName) { |
| if ( pathName == null ) { |
| throw new NullPointerException("Path must not be null."); |
| } |
| PreferencesImpl executingNode= this; |
| if ( pathName.length() == 0 ) { |
| return this; |
| } |
| if ( pathName.startsWith("/") && this.parent != null ) { |
| executingNode = this.getRoot(); |
| } |
| if ( pathName.startsWith("/") ) { |
| pathName = pathName.substring(1); |
| } |
| return executingNode.getNode(pathName, false, true); |
| } |
| |
| /** |
| * Get a relative node. |
| * @param path |
| * @return |
| */ |
| protected PreferencesImpl getNode(String path, boolean saveNewlyCreatedNode, boolean create) { |
| if ( path.startsWith("/") ) { |
| throw new IllegalArgumentException("Path must not contained consecutive slashes"); |
| } |
| if ( path.length() == 0 ) { |
| return this; |
| } |
| synchronized ( this ) { |
| this.checkValidity(); |
| |
| String subPath = null; |
| int pos = path.indexOf('/'); |
| if ( pos != -1 ) { |
| subPath = path.substring(pos+1); |
| path = path.substring(0, pos); |
| } |
| boolean save = false; |
| PreferencesImpl child = (PreferencesImpl) this.children.get(path); |
| if ( child == null ) { |
| if ( !create ) { |
| return null; |
| } |
| child = new PreferencesImpl(this, path); |
| this.children.put(path, child); |
| this.changeSet.childAdded(path); |
| if ( saveNewlyCreatedNode ) { |
| save = true; |
| } |
| saveNewlyCreatedNode = false; |
| } |
| final PreferencesImpl result; |
| if ( subPath == null ) { |
| result = child; |
| } else { |
| result = child.getNode(subPath, saveNewlyCreatedNode, create); |
| } |
| if ( save ) { |
| try { |
| result.flush(); |
| } catch (BackingStoreException ignore) { |
| // we ignore this for now |
| } |
| } |
| return result; |
| } |
| } |
| |
| /** |
| * We do not synchronize this method to avoid dead locks as this |
| * method might call another preferences object in the hierarchy. |
| * @see org.osgi.service.prefs.Preferences#nodeExists(java.lang.String) |
| */ |
| public boolean nodeExists(String pathName) throws BackingStoreException { |
| if ( pathName == null ) { |
| throw new NullPointerException("Path must not be null."); |
| } |
| if ( pathName.length() == 0 ) { |
| return this.valid; |
| } |
| PreferencesImpl node = this; |
| synchronized ( this ) { |
| this.checkValidity(); |
| if ( pathName.startsWith("/") && this.parent != null ) { |
| node = this.getRoot(); |
| } |
| if ( pathName.startsWith("/") ) { |
| pathName = pathName.substring(1); |
| } |
| } |
| final Preferences searchNode = node.getNode(pathName, false, false); |
| return searchNode != null; |
| } |
| |
| /** |
| * @see org.osgi.service.prefs.Preferences#removeNode() |
| */ |
| public void removeNode() throws BackingStoreException { |
| this.checkValidity(); |
| this.safelyRemoveNode(); |
| |
| if ( this.parent != null ) { |
| this.parent.removeChild(this); |
| } |
| } |
| |
| /** |
| * Safely remove a node by resetting all properties and calling |
| * this method on all children recursively. |
| */ |
| protected void safelyRemoveNode() { |
| if ( this.valid ) { |
| Collection c = null; |
| synchronized ( this ) { |
| this.valid = false; |
| this.properties.clear(); |
| c = new ArrayList(this.children.values()); |
| this.children.clear(); |
| } |
| final Iterator i = c.iterator(); |
| while ( i.hasNext() ) { |
| final PreferencesImpl child = (PreferencesImpl) i.next(); |
| child.safelyRemoveNode(); |
| } |
| } |
| } |
| |
| protected synchronized void removeChild(PreferencesImpl child) { |
| this.children.remove(child.name()); |
| this.changeSet.childRemoved(child.name()); |
| } |
| |
| /** |
| * @see org.osgi.service.prefs.Preferences#name() |
| */ |
| public String name() { |
| return this.name; |
| } |
| |
| /** |
| * @see org.osgi.service.prefs.Preferences#absolutePath() |
| */ |
| public String absolutePath() { |
| if (this.parent == null) { |
| return "/"; |
| } |
| final String parentPath = this.parent.absolutePath(); |
| if ( parentPath.length() == 1 ) { |
| return parentPath + this.name; |
| } |
| return parentPath + '/' + this.name; |
| } |
| |
| /** |
| * @see org.osgi.service.prefs.Preferences#flush() |
| */ |
| public synchronized void flush() throws BackingStoreException { |
| this.checkValidity(); |
| this.storeManager.getStore().store(this); |
| this.changeSet.clear(); |
| } |
| |
| /** |
| * @see org.osgi.service.prefs.Preferences#sync() |
| */ |
| public synchronized void sync() throws BackingStoreException { |
| this.checkValidity(); |
| this.storeManager.getStore().update(this); |
| this.storeManager.getStore().store(this); |
| } |
| |
| /** |
| * Update from the preferences impl. |
| * @param impl |
| */ |
| public void update(PreferencesImpl impl) { |
| final Iterator i = impl.properties.entrySet().iterator(); |
| while ( i.hasNext() ) { |
| final Map.Entry entry = (Map.Entry)i.next(); |
| if ( !this.properties.containsKey(entry.getKey()) ) { |
| this.properties.put(entry.getKey(), entry.getValue()); |
| } |
| } |
| final Iterator cI = impl.children.entrySet().iterator(); |
| while ( cI.hasNext() ) { |
| final Map.Entry entry = (Map.Entry)cI.next(); |
| final String name = entry.getKey().toString(); |
| final PreferencesImpl child = (PreferencesImpl)entry.getValue(); |
| if ( !this.children.containsKey(name) ) { |
| // create node |
| this.node(name); |
| } |
| ((PreferencesImpl)this.children.get(name)).update(child); |
| } |
| } |
| |
| /*** |
| * Apply the changes done to the passed preferences object. |
| * @param prefs |
| */ |
| public void applyChanges(PreferencesImpl prefs) { |
| final ChangeSet changeSet = prefs.getChangeSet(); |
| if ( changeSet.hasChanges ) { |
| this.changeSet.importChanges(prefs.changeSet); |
| Iterator i; |
| // remove properties |
| i = changeSet.removedProperties.iterator(); |
| while ( i.hasNext() ) { |
| this.properties.remove(i.next()); |
| } |
| // set/update properties |
| i = changeSet.changedProperties.iterator(); |
| while ( i.hasNext() ) { |
| final String key = (String)i.next(); |
| this.properties.put(key, prefs.properties.get(key)); |
| } |
| // remove children |
| i = changeSet.removedChildren.iterator(); |
| while ( i.hasNext() ) { |
| final String name = (String)i.next(); |
| this.children.remove(name); |
| } |
| // added childs are processed in the next loop |
| } |
| final Iterator cI = prefs.getChildren().iterator(); |
| while ( cI.hasNext() ) { |
| final PreferencesImpl current = (PreferencesImpl)cI.next(); |
| final PreferencesImpl child = this.getOrCreateNode(current.name()); |
| child.applyChanges(current); |
| } |
| } |
| } |