blob: 88d8418776f698c377eb45e45b7e5aaed75ede89 [file] [log] [blame]
/*
* 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.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
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<String, String> properties = new HashMap<String, String>();
/** Has this node been removed? */
protected boolean valid = true;
/** The parent. */
protected final PreferencesImpl parent;
/** The child nodes. */
protected final Map<String, PreferencesImpl> children = new HashMap<String, PreferencesImpl>();
/** 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<PreferencesImpl> getChildren() {
return this.children.values();
}
/**
* Return the properties set.
*/
public Map<String, String> 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) {
if ( key == null ) {
throw new NullPointerException();
}
this.checkValidity();
String value = 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<String> i = this.properties.keySet().iterator();
while ( i.hasNext() ) {
final String key = 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 {
final byte[] bytes = value.getBytes("utf-8");
// check for invalid characters
boolean valid = bytes.length * 6 % 8 == 0;
if ( valid ) {
for(int i=0; i<bytes.length-1; i++) {
final byte b = bytes[i];
if ( b >= 'a' && b <= 'z') {
continue;
}
if ( b >= 'A' && b <= 'Z') {
continue;
}
if ( b >= '0' && b <= '9') {
continue;
}
if ( b == '+' || b == '/') {
continue;
}
valid = false;
break;
}
}
if ( valid ) {
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 {
if ( !this.changeSet.hasChanges ) {
this.storeManager.getStore().update(this);
}
final Set<String> keys = this.properties.keySet();
return keys.toArray(new String[keys.size()]);
}
/**
* @see org.osgi.service.prefs.Preferences#childrenNames()
*/
public synchronized String[] childrenNames() throws BackingStoreException {
if ( !this.changeSet.hasChanges ) {
this.storeManager.getStore().update(this);
}
final Set<String> names = this.children.keySet();
return 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.endsWith("/") ) {
throw new IllegalArgumentException("Path must not contained trailing 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 = 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<PreferencesImpl> c = null;
synchronized ( this ) {
this.valid = false;
this.properties.clear();
c = new ArrayList<PreferencesImpl>(this.children.values());
this.children.clear();
}
final Iterator<PreferencesImpl> i = c.iterator();
while ( i.hasNext() ) {
final PreferencesImpl child = 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<Map.Entry<String, String>> i = impl.properties.entrySet().iterator();
while ( i.hasNext() ) {
final Map.Entry<String, String> entry = i.next();
if ( !this.properties.containsKey(entry.getKey()) ) {
this.properties.put(entry.getKey(), entry.getValue());
}
}
final Iterator<Map.Entry<String, PreferencesImpl>> cI = impl.children.entrySet().iterator();
while ( cI.hasNext() ) {
final Map.Entry<String, PreferencesImpl> entry = cI.next();
final String name = entry.getKey().toString();
final PreferencesImpl child = entry.getValue();
if ( !this.children.containsKey(name) ) {
// create node
this.node(name);
}
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<String> 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 = i.next();
this.properties.put(key, prefs.properties.get(key));
}
// remove children
i = changeSet.removedChildren.iterator();
while ( i.hasNext() ) {
final String name = i.next();
this.children.remove(name);
}
// added childs are processed in the next loop
}
final Iterator<PreferencesImpl> cI = prefs.getChildren().iterator();
while ( cI.hasNext() ) {
final PreferencesImpl current = cI.next();
final PreferencesImpl child = this.getOrCreateNode(current.name());
child.applyChanges(current);
}
}
}