move preferences implementation to trunk (this time with correct url)
git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@560958 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/prefs/pom.xml b/prefs/pom.xml
new file mode 100644
index 0000000..78427f7
--- /dev/null
+++ b/prefs/pom.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+<project>
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>felix</artifactId>
+ <version>1.0.0</version>
+ </parent>
+
+ <artifactId>org.apache.felix.preferences</artifactId>
+ <packaging>bundle</packaging>
+ <version>0.1.0-SNAPSHOT</version>
+
+ <name>Apache Felix Prefrences Service</name>
+ <description>
+ Implementation of the OSGi Preferences Service Specification 1.1
+ </description>
+
+ <dependencies>
+ <dependency>
+ <groupId>${pom.groupId}</groupId>
+ <artifactId>org.osgi.core</artifactId>
+ <version>1.0.0</version>
+ </dependency>
+ <dependency>
+ <groupId>${pom.groupId}</groupId>
+ <artifactId>org.osgi.compendium</artifactId>
+ <version>0.9.0-SNAPSHOT</version>
+ </dependency>
+ <dependency>
+ <groupId>commons-codec</groupId>
+ <artifactId>commons-codec</artifactId>
+ <version>1.3</version>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <version>1.0.0</version>
+ <extensions>true</extensions>
+ <configuration>
+ <instructions>
+ <Bundle-Category>osgi</Bundle-Category>
+ <Export-Package>
+ org.apache.felix.sandbox.preferences,
+ org.osgi.service.prefs;version=1.1
+ </Export-Package>
+ <Private-Package>
+ org.apache.felix.sandbox.preferences.*,
+ org.apache.commons.codec.binary.*
+ </Private-Package>
+ <Bundle-Activator>
+ org.apache.felix.sandbox.preferences.impl.PreferencesManager
+ </Bundle-Activator>
+ </instructions>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/prefs/src/main/java/org/apache/felix/sandbox/preferences/BackingStore.java b/prefs/src/main/java/org/apache/felix/sandbox/preferences/BackingStore.java
new file mode 100644
index 0000000..35e356b
--- /dev/null
+++ b/prefs/src/main/java/org/apache/felix/sandbox/preferences/BackingStore.java
@@ -0,0 +1,76 @@
+/*
+ * 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.sandbox.preferences;
+
+import org.osgi.service.prefs.BackingStoreException;
+
+/**
+ * The BackingStore for the preferences.
+ *
+ * This interface allows for different implementation strategies.
+ */
+public interface BackingStore {
+
+ /**
+ * Store the current preferences and its children in the backing
+ * store.
+ * The store should check, if the preferences have changed,
+ * it should also check all children.
+ * @param prefs The preferences.
+ * @throws BackingStoreException
+ */
+ void store(PreferencesImpl prefs) throws BackingStoreException;
+
+ /**
+ * Update the current preferences and its children from the
+ * backing store.
+ */
+ void update(PreferencesImpl prefs) throws BackingStoreException;
+
+ /**
+ * Return all bundle ids for which preferences are stored..
+ * @return Return an array of bundle ids or an empty array.
+ */
+ Long[] availableBundles();
+
+ /**
+ * Remove all preferences stored for this bundle.
+ * @param bundleId The bundle id.
+ * @throws BackingStoreException
+ */
+ void remove(Long bundleId) throws BackingStoreException;
+
+ /**
+ * Load the preferences for the given description.
+ * @param manager The backing store manager which should be passed to new preferences implementations.
+ * @param desc
+ * @return A new preferences object or null if it's not available in the backing store.
+ * @throws BackingStoreException
+ */
+ PreferencesImpl load(BackingStoreManager manager, PreferencesDescription desc) throws BackingStoreException;
+
+ /**
+ * Load all preferences for this bundle.
+ * @param manager The backing store manager which should be passed to new preferences implementations.
+ * @param bundleId The bundle id.
+ * @return An array with the preferences or an empty array.
+ * @throws BackingStoreException
+ */
+ PreferencesImpl[] loadAll(BackingStoreManager manager, Long bundleId) throws BackingStoreException;
+}
diff --git a/prefs/src/main/java/org/apache/felix/sandbox/preferences/BackingStoreManager.java b/prefs/src/main/java/org/apache/felix/sandbox/preferences/BackingStoreManager.java
new file mode 100644
index 0000000..aea89e4
--- /dev/null
+++ b/prefs/src/main/java/org/apache/felix/sandbox/preferences/BackingStoreManager.java
@@ -0,0 +1,31 @@
+/*
+ * 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.sandbox.preferences;
+
+
+/**
+ * Get the backing store.
+ */
+public interface BackingStoreManager {
+
+ /**
+ * Return the current backing store.
+ */
+ BackingStore getStore();
+}
diff --git a/prefs/src/main/java/org/apache/felix/sandbox/preferences/ChangeSet.java b/prefs/src/main/java/org/apache/felix/sandbox/preferences/ChangeSet.java
new file mode 100644
index 0000000..647626d
--- /dev/null
+++ b/prefs/src/main/java/org/apache/felix/sandbox/preferences/ChangeSet.java
@@ -0,0 +1,150 @@
+/*
+ * 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.sandbox.preferences;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * This class keeps track of the changes to a preferences node.
+ */
+public class ChangeSet {
+
+ /** Do we have changes at all? */
+ protected boolean hasChanges = false;
+
+ /** A set of changed/added properties. */
+ protected final Set changedProperties = new HashSet();
+
+ /** A set of removed properties. */
+ protected final Set removedProperties = new HashSet();
+
+ /** A set of added children. */
+ protected final Set addedChildren = new HashSet();
+
+ /** A set of removed children. */
+ protected final Set removedChildren = new HashSet();
+
+ /**
+ * Do we have changes?
+ * @return
+ */
+ public boolean hasChanges() {
+ return this.hasChanges;
+ }
+
+ /**
+ * Inform that a property has been added/changed.
+ * @param name The name of the property.
+ */
+ public void propertyChanged(String name) {
+ this.hasChanges = true;
+ this.removedProperties.remove(name);
+ this.changedProperties.add(name);
+ }
+
+ /**
+ * Inform that a property has removed.
+ * @param name The name of the property.
+ */
+ public void propertyRemoved(String name) {
+ this.hasChanges = true;
+ this.changedProperties.remove(name);
+ this.removedProperties.add(name);
+ }
+
+ /**
+ * Inform that a child has been added.
+ * @param name The name of the child.
+ */
+ public void childAdded(String name) {
+ this.hasChanges = true;
+ this.removedChildren.remove(name);
+ this.addedChildren.add(name);
+ }
+
+ /**
+ * Inform that a child has been removed.
+ * @param name The name of the child.
+ */
+ public void childRemoved(String name) {
+ this.hasChanges = true;
+ this.addedChildren.remove(name);
+ this.removedChildren.add(name);
+ }
+
+ /**
+ * Reset state to unchanged.
+ */
+ public void clear() {
+ this.hasChanges = false;
+ this.removedChildren.clear();
+ this.removedProperties.clear();
+ this.addedChildren.clear();
+ this.changedProperties.clear();
+ }
+
+ /**
+ * Import the changes from the other change set.
+ * @param other
+ */
+ public void importChanges(ChangeSet other) {
+ if (other.hasChanges) {
+ this.hasChanges = true;
+ this.addedChildren.addAll(other.addedChildren);
+ this.removedChildren.addAll(other.removedChildren);
+ this.changedProperties.addAll(other.changedProperties);
+ this.removedProperties.addAll(other.removedProperties);
+ }
+ }
+
+ /**
+ * Return a collection with the changed property names.
+ * @return A collection.
+ */
+ public Collection getChangedProperties() {
+ return Collections.unmodifiableCollection(this.changedProperties);
+ }
+
+ /**
+ * Return a collection with the removed property names.
+ * @return A collection.
+ */
+ public Collection getRemovedProperties() {
+ return Collections.unmodifiableCollection(this.removedProperties);
+ }
+
+ /**
+ * Return a collection with the added children names.
+ * @return A collection.
+ */
+ public Collection getAddedChildren() {
+ return Collections.unmodifiableCollection(this.addedChildren);
+ }
+
+ /**
+ * Return a collection with the removed children names.
+ * @return A collection.
+ */
+ public Collection getRemovedChildren() {
+ return Collections.unmodifiableCollection(this.removedChildren);
+ }
+}
diff --git a/prefs/src/main/java/org/apache/felix/sandbox/preferences/PreferencesDescription.java b/prefs/src/main/java/org/apache/felix/sandbox/preferences/PreferencesDescription.java
new file mode 100644
index 0000000..7cb1531
--- /dev/null
+++ b/prefs/src/main/java/org/apache/felix/sandbox/preferences/PreferencesDescription.java
@@ -0,0 +1,46 @@
+/*
+ * 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.sandbox.preferences;
+
+/**
+ * A preferences description identifies a preferences try.
+ * It is a unique identifier consisting of:
+ * - the bundle id
+ * - the preferences name which is not the node name but either
+ * null for the system preferences or the user name.
+ */
+public class PreferencesDescription {
+
+ protected final Long bundleId;
+
+ protected final String identifier;
+
+ public PreferencesDescription(Long id, String identifier) {
+ this.bundleId = id;
+ this.identifier = identifier;
+ }
+
+ public Long getBundleId() {
+ return this.bundleId;
+ }
+
+ public String getIdentifier() {
+ return this.identifier;
+ }
+}
diff --git a/prefs/src/main/java/org/apache/felix/sandbox/preferences/PreferencesImpl.java b/prefs/src/main/java/org/apache/felix/sandbox/preferences/PreferencesImpl.java
new file mode 100644
index 0000000..48a5192
--- /dev/null
+++ b/prefs/src/main/java/org/apache/felix/sandbox/preferences/PreferencesImpl.java
@@ -0,0 +1,654 @@
+/*
+ * 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.sandbox.preferences;
+
+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 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
+ */
+ 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
+ */
+ 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.");
+ }
+ }
+
+ /**
+ * @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.decodeBase64(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
+ */
+ 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 "/";
+ } else {
+ 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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/prefs/src/main/java/org/apache/felix/sandbox/preferences/impl/DataFileBackingStoreImpl.java b/prefs/src/main/java/org/apache/felix/sandbox/preferences/impl/DataFileBackingStoreImpl.java
new file mode 100644
index 0000000..55ed28d
--- /dev/null
+++ b/prefs/src/main/java/org/apache/felix/sandbox/preferences/impl/DataFileBackingStoreImpl.java
@@ -0,0 +1,195 @@
+/*
+ * 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.sandbox.preferences.impl;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.felix.sandbox.preferences.BackingStoreManager;
+import org.apache.felix.sandbox.preferences.PreferencesDescription;
+import org.apache.felix.sandbox.preferences.PreferencesImpl;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.prefs.BackingStoreException;
+
+/**
+ * This implementating of the backing store uses the bundle mechanism to store
+ * binary data.
+ */
+public class DataFileBackingStoreImpl extends StreamBackingStoreImpl {
+
+ /** The root directory (or null if not available) */
+ protected final File rootDirectory;
+
+ public DataFileBackingStoreImpl(BundleContext context) {
+ super(context);
+ this.rootDirectory = context.getDataFile("");
+ }
+
+ /**
+ * @see org.apache.felix.sandbox.preferences.impl.StreamBackingStoreImpl#checkAccess()
+ */
+ protected void checkAccess() throws BackingStoreException {
+ if ( this.rootDirectory == null ) {
+ throw new BackingStoreException("Saving of data files to the bundle context is currently not supported.");
+ }
+ }
+
+ /**
+ * @see org.apache.felix.sandbox.preferences.impl.StreamBackingStoreImpl#getOutputStream(org.apache.felix.sandbox.preferences.PreferencesDescription)
+ */
+ protected OutputStream getOutputStream(PreferencesDescription desc) throws IOException {
+ final File file = this.getFile(desc);
+ return new FileOutputStream(file);
+ }
+
+ /**
+ * @see org.apache.felix.sandbox.preferences.BackingStore#availableBundles()
+ */
+ public Long[] availableBundles() {
+ // If the root directory is not available, then we do nothing!
+ try {
+ this.checkAccess();
+ } catch (BackingStoreException ignore) {
+ return new Long[0];
+ }
+ final Set bundleIds = new HashSet();
+ final File[] children = this.rootDirectory.listFiles();
+ for( int i=0; i<children.length; i++ ) {
+ final File current = children[i];
+
+ final PreferencesDescription desc = this.getDescription(current);
+ if ( desc != null ) {
+ bundleIds.add(desc.getBundleId());
+ }
+ }
+ return (Long[])bundleIds.toArray(new Long[bundleIds.size()]);
+ }
+
+ protected PreferencesDescription getDescription(File file) {
+ final String fileName = file.getName();
+ // parse the file name to get: bundle id, user|system identifer
+ if ( fileName.startsWith("P") && fileName.endsWith(".ser") ) {
+ final String name = fileName.substring(1, fileName.length() - 4);
+ final String key;
+ final String identifier;
+ int pos = name.indexOf("_");
+ if ( pos != -1 ) {
+ identifier = name.substring(pos+1);
+ key = name.substring(0, pos);
+ } else {
+ key = name;
+ identifier = null;
+ }
+ final Long bundleId = Long.valueOf(key);
+ return new PreferencesDescription(bundleId, identifier);
+ }
+ return null;
+ }
+
+ /**
+ * @see org.apache.felix.sandbox.preferences.BackingStore#remove(java.lang.Long)
+ */
+ public void remove(Long bundleId) throws BackingStoreException {
+ this.checkAccess();
+ final File[] children = this.rootDirectory.listFiles();
+ for( int i=0; i<children.length; i++ ) {
+ final File current = children[i];
+
+ final PreferencesDescription desc = this.getDescription(current);
+ if ( desc != null ) {
+ if ( desc.getBundleId().equals(bundleId) ) {
+ current.delete();
+ }
+ }
+ }
+ }
+
+ /**
+ * @see org.apache.felix.sandbox.preferences.BackingStore#loadAll(org.apache.felix.sandbox.preferences.BackingStoreManager, java.lang.Long)
+ */
+ public PreferencesImpl[] loadAll(BackingStoreManager manager, Long bundleId) throws BackingStoreException {
+ this.checkAccess();
+ final List list = new ArrayList();
+ final File[] children = this.rootDirectory.listFiles();
+ for( int i=0; i<children.length; i++ ) {
+ final File current = children[i];
+
+ final PreferencesDescription desc = this.getDescription(current);
+ if ( desc != null ) {
+ if ( desc.getBundleId().equals(bundleId) ) {
+ final PreferencesImpl root = new PreferencesImpl(desc, manager);
+ try {
+ final FileInputStream fis = new FileInputStream(current);
+ this.read(root, fis);
+ fis.close();
+ } catch (IOException ioe) {
+ throw new BackingStoreException("Unable to load preferences.", ioe);
+ }
+ list.add(root);
+ }
+ }
+ }
+ return (PreferencesImpl[])list.toArray(new PreferencesImpl[list.size()]);
+ }
+
+ /**
+ * @see org.apache.felix.sandbox.preferences.BackingStore#load(org.apache.felix.sandbox.preferences.BackingStoreManager, org.apache.felix.sandbox.preferences.PreferencesDescription)
+ */
+ public PreferencesImpl load(BackingStoreManager manager, PreferencesDescription desc) throws BackingStoreException {
+ this.checkAccess();
+ final File file = this.getFile(desc);
+ if ( file.exists() ) {
+ try {
+ final PreferencesImpl root = new PreferencesImpl(desc, manager);
+ final FileInputStream fis = new FileInputStream(file);
+ this.read(root, fis);
+ fis.close();
+
+ return root;
+ } catch (IOException ioe) {
+ throw new BackingStoreException("Unable to load preferences.", ioe);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get the file fo the preferences tree.
+ * @param desc
+ * @return
+ */
+ protected File getFile(PreferencesDescription desc) {
+ final StringBuffer buffer = new StringBuffer("P");
+ buffer.append(desc.getBundleId());
+ if ( desc.getIdentifier() != null ) {
+ buffer.append('_');
+ buffer.append(desc.getIdentifier());
+ }
+ buffer.append(".ser");
+ final File file = new File(this.rootDirectory, buffer.toString());
+ return file;
+ }
+}
diff --git a/prefs/src/main/java/org/apache/felix/sandbox/preferences/impl/PreferencesManager.java b/prefs/src/main/java/org/apache/felix/sandbox/preferences/impl/PreferencesManager.java
new file mode 100644
index 0000000..a2849af
--- /dev/null
+++ b/prefs/src/main/java/org/apache/felix/sandbox/preferences/impl/PreferencesManager.java
@@ -0,0 +1,248 @@
+/*
+ * 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.sandbox.preferences.impl;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import org.apache.felix.sandbox.preferences.BackingStore;
+import org.apache.felix.sandbox.preferences.BackingStoreManager;
+import org.apache.felix.sandbox.preferences.PreferencesImpl;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleEvent;
+import org.osgi.framework.BundleListener;
+import org.osgi.framework.ServiceFactory;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.log.LogService;
+import org.osgi.service.prefs.BackingStoreException;
+import org.osgi.service.prefs.PreferencesService;
+import org.osgi.util.tracker.ServiceTracker;
+
+/**
+ * This activator registers itself as a service factory for the
+ * preferences service.
+ *
+ */
+public class PreferencesManager
+ implements BundleActivator,
+ BundleListener,
+ ServiceFactory,
+ BackingStoreManager {
+
+ /** The map of already created services. For each client bundle
+ * a new service is created.
+ */
+ protected final Map services = new HashMap();
+
+ /** The bundle context. */
+ protected BundleContext context;
+
+ /** The backing store service tracker. */
+ protected ServiceTracker storeTracker;
+
+ /** The service tracker for the log service. */
+ protected ServiceTracker logTracker;
+
+ /** The default store which is used if no service can be found. */
+ protected BackingStore defaultStore;
+
+ /** Tracking count for the store tracker to detect changes. */
+ protected int storeTrackingCount = -1;
+
+ /**
+ * @see org.osgi.framework.BundleListener#bundleChanged(org.osgi.framework.BundleEvent)
+ */
+ public void bundleChanged(BundleEvent event) {
+ if (event.getType() == BundleEvent.UNINSTALLED) {
+ final Long bundleId = new Long(event.getBundle().getBundleId());
+ synchronized ( this.services ) {
+ try {
+ this.getStore().remove(bundleId);
+ } catch (BackingStoreException ignore) {
+ // we ignore this for now
+ }
+ this.services.remove(bundleId);
+ }
+ }
+ }
+
+ /**
+ * @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext)
+ */
+ public void start(BundleContext context) throws Exception {
+ this.context = context;
+
+ // track the log service using a ServiceTracker
+ this.logTracker = new ServiceTracker( context, LogService.class.getName(), null );
+ this.logTracker.open();
+
+ // create the tracker for our backing store
+ this.storeTracker = new ServiceTracker( context, BackingStore.class.getName(), null);
+ this.storeTracker.open();
+
+ // register this activator as a bundle lister
+ context.addBundleListener(this);
+
+ // finally register the service factory for the preferences service
+ context.registerService(PreferencesService.class.getName(), this, null);
+ }
+
+ /**
+ * @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext)
+ */
+ public void stop(BundleContext context) throws Exception {
+ // if we get stopped, we should save all in memory representations
+ synchronized (this.services) {
+ final Iterator i = this.services.values().iterator();
+ while ( i.hasNext() ) {
+ final PreferencesServiceImpl service = (PreferencesServiceImpl)i.next();
+ this.save(service);
+ }
+ this.services.clear();
+ }
+ // stop tracking store service
+ if ( this.storeTracker != null ) {
+ this.storeTracker.close();
+ this.storeTracker = null;
+ }
+ this.defaultStore = null;
+
+ // stop tracking log service
+ if ( this.logTracker != null ) {
+ this.logTracker.close();
+ this.logTracker = null;
+ }
+
+ this.context = null;
+ }
+
+ /**
+ * @see org.osgi.framework.ServiceFactory#getService(org.osgi.framework.Bundle, org.osgi.framework.ServiceRegistration)
+ */
+ public Object getService(Bundle bundle, ServiceRegistration reg) {
+ final Long bundleId = new Long(bundle.getBundleId());
+
+ synchronized (this.services) {
+ PreferencesServiceImpl service;
+ // do we already have created a service for this bundle?
+ service = (PreferencesServiceImpl) this.services.get(bundleId);
+
+ if (service == null) {
+ // create a new service instance
+ service = new PreferencesServiceImpl(bundleId, this);
+ this.services.put(bundleId, service);
+ }
+ return service;
+ }
+ }
+
+ /**
+ * @see org.osgi.framework.ServiceFactory#ungetService(org.osgi.framework.Bundle, org.osgi.framework.ServiceRegistration, java.lang.Object)
+ */
+ public void ungetService(Bundle bundle, ServiceRegistration reg, Object s) {
+ final Long bundleId = new Long(bundle.getBundleId());
+ // we save all the preferences
+ synchronized ( this.services ) {
+ final PreferencesServiceImpl service = (PreferencesServiceImpl) this.services.get(bundleId);
+ if ( service != null ) {
+ this.save(service);
+ }
+ }
+ }
+
+ /**
+ * Save all preferences for this service.
+ * @param service
+ */
+ protected void save(PreferencesServiceImpl service) {
+ final Iterator i = service.getAllPreferences().iterator();
+ while ( i.hasNext() ) {
+ final PreferencesImpl prefs = (PreferencesImpl)i.next();
+ try {
+ this.getStore().store(prefs);
+ } catch (BackingStoreException ignore) {
+ // we ignore this
+ }
+ }
+ }
+
+ protected void log( int level, String message, Throwable t ) {
+ final LogService log = ( LogService ) this.logTracker.getService();
+ if ( log != null ) {
+ log.log( level, message, t );
+ return;
+ }
+ }
+
+ /**
+ * @see org.apache.felix.sandbox.preferences.BackingStoreManager#getStore()
+ */
+ public BackingStore getStore() {
+ // has the service changed?
+ int currentCount = this.storeTracker.getTrackingCount();
+ BackingStore service = (BackingStore) this.storeTracker.getService();
+ if ( service != null && this.storeTrackingCount < currentCount ) {
+ this.storeTrackingCount = currentCount;
+ this.cleanupStore(service);
+ }
+ if ( service == null ) {
+ // no service available use default store
+ if ( this.defaultStore == null ) {
+ synchronized ( this ) {
+ if ( this.defaultStore == null ) {
+ this.defaultStore = new DataFileBackingStoreImpl(this.context);
+ this.cleanupStore(this.defaultStore);
+ }
+ }
+ }
+ service = this.defaultStore;
+ }
+
+ return service;
+ }
+
+ /**
+ * Clean up the store and remove preferences for deleted bundles.
+ * @param store
+ */
+ protected void cleanupStore(BackingStore store) {
+ // check which bundles are available
+ final Long[] availableBundleIds = store.availableBundles();
+
+ // now check the bundles, for which we have preferences, if they are still
+ // in service and delete the preferences where the bundles are out of service.
+ // we synchronize on services in order to get not disturbed by a bundle event
+ synchronized ( this.services ) {
+ for(int i=0; i<availableBundleIds.length; i++) {
+ final Long bundleId = availableBundleIds[i];
+ final Bundle bundle = this.context.getBundle(bundleId.longValue());
+ if (bundle == null || bundle.getState() == Bundle.UNINSTALLED) {
+ try {
+ store.remove(bundleId);
+ } catch (BackingStoreException ignore) {
+ // we ignore this for now
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/prefs/src/main/java/org/apache/felix/sandbox/preferences/impl/PreferencesServiceImpl.java b/prefs/src/main/java/org/apache/felix/sandbox/preferences/impl/PreferencesServiceImpl.java
new file mode 100644
index 0000000..c17ad3a
--- /dev/null
+++ b/prefs/src/main/java/org/apache/felix/sandbox/preferences/impl/PreferencesServiceImpl.java
@@ -0,0 +1,123 @@
+/*
+ * 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.sandbox.preferences.impl;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.felix.sandbox.preferences.BackingStoreManager;
+import org.apache.felix.sandbox.preferences.PreferencesDescription;
+import org.apache.felix.sandbox.preferences.PreferencesImpl;
+import org.osgi.service.prefs.BackingStoreException;
+import org.osgi.service.prefs.Preferences;
+import org.osgi.service.prefs.PreferencesService;
+
+/**
+ * This is an implementation of the OSGI Preferences Service, Version 1.1.
+ */
+public class PreferencesServiceImpl implements PreferencesService {
+
+ /** This is the system preferences tree. */
+ protected PreferencesImpl systemTree;
+
+ /** This is the map containing the user preferences trees. */
+ protected final Map trees = new HashMap();
+
+ /** The service id for the bundle this service belongs to. */
+ protected final Long bundleId;
+
+ /** The backing store manager. */
+ protected final BackingStoreManager storeManager;
+
+ public PreferencesServiceImpl(Long id,
+ BackingStoreManager storeManager) {
+ this.bundleId = id;
+ this.storeManager = storeManager;
+ try {
+ // load prefs first
+ PreferencesImpl[] prefs = null;
+ prefs = this.storeManager.getStore().loadAll(storeManager, this.bundleId);
+ for(int i=0;i<prefs.length;i++) {
+ if ( prefs[i].getDescription().getIdentifier() == null ) {
+ this.systemTree = prefs[i];
+ } else {
+ this.trees.put(prefs[i].getDescription().getIdentifier(), prefs);
+ }
+ }
+ } catch (BackingStoreException e) {
+ // we ignore this here
+ }
+ }
+
+ /**
+ * @see org.osgi.service.prefs.PreferencesService#getSystemPreferences()
+ */
+ public synchronized Preferences getSystemPreferences() {
+ if ( this.systemTree == null ) {
+ this.systemTree = new PreferencesImpl(new PreferencesDescription(this.bundleId, null), this.storeManager);
+ }
+ // sync with latest version from store
+ try {
+ this.systemTree.sync();
+ } catch (BackingStoreException ignore) {
+ // we ignore this
+ }
+ return this.systemTree;
+ }
+
+ /**
+ * @see org.osgi.service.prefs.PreferencesService#getUserPreferences(java.lang.String)
+ */
+ public synchronized Preferences getUserPreferences(String name) {
+ PreferencesImpl result = (PreferencesImpl) this.trees.get(name);
+ // if the tree does not exist yet, create it
+ if (result == null) {
+ result = new PreferencesImpl(new PreferencesDescription(this.bundleId, name), this.storeManager);
+ this.trees.put(name, result);
+ }
+ // sync with latest version from store
+ try {
+ result.sync();
+ } catch (BackingStoreException ignore) {
+ // we ignore this
+ }
+ return result;
+ }
+
+ /**
+ * @see org.osgi.service.prefs.PreferencesService#getUsers()
+ */
+ public synchronized String[] getUsers() {
+ // TODO - we have to sync with the store
+ final Set userKeys = this.trees.keySet();
+ return (String[])userKeys.toArray(new String[userKeys.size()]);
+ }
+
+ protected List getAllPreferences() {
+ final List list = new ArrayList();
+ if ( this.systemTree != null ) {
+ list.add(this.systemTree);
+ }
+ list.addAll(this.trees.values());
+ return list;
+ }
+}
\ No newline at end of file
diff --git a/prefs/src/main/java/org/apache/felix/sandbox/preferences/impl/StreamBackingStoreImpl.java b/prefs/src/main/java/org/apache/felix/sandbox/preferences/impl/StreamBackingStoreImpl.java
new file mode 100644
index 0000000..8769978
--- /dev/null
+++ b/prefs/src/main/java/org/apache/felix/sandbox/preferences/impl/StreamBackingStoreImpl.java
@@ -0,0 +1,208 @@
+/*
+ * 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.sandbox.preferences.impl;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.OutputStream;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+
+import org.apache.felix.sandbox.preferences.BackingStore;
+import org.apache.felix.sandbox.preferences.PreferencesDescription;
+import org.apache.felix.sandbox.preferences.PreferencesImpl;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.prefs.BackingStoreException;
+
+/**
+ * This is an abstract implementation of a backing store
+ * which uses streams to read/write the preferences and
+ * stores a complete preferences tree in a single stream.
+ */
+public abstract class StreamBackingStoreImpl implements BackingStore {
+
+ /** The bundle context. */
+ protected final BundleContext bundleContext;
+
+ public StreamBackingStoreImpl(BundleContext context) {
+ this.bundleContext = context;
+ }
+
+ /**
+ * This method is invoked to check if the backing store is accessible right now.
+ * @throws BackingStoreException
+ */
+ protected abstract void checkAccess() throws BackingStoreException;
+
+ /**
+ * Get the output stream to write the preferences.
+ */
+ protected abstract OutputStream getOutputStream(PreferencesDescription desc)
+ throws IOException;
+
+ /**
+ * @see org.apache.felix.sandbox.preferences.BackingStore#store(org.apache.felix.sandbox.preferences.PreferencesImpl)
+ */
+ public void store(PreferencesImpl prefs) throws BackingStoreException {
+ // do we need to store at all?
+ if ( !this.hasChanges(prefs) ) {
+ return;
+ }
+ this.checkAccess();
+ // load existing data
+ final PreferencesImpl savedData = this.load(prefs.getBackingStoreManager(), prefs.getDescription());
+ if ( savedData != null ) {
+ // merge with saved version
+ final PreferencesImpl n = savedData.getOrCreateNode(prefs.absolutePath());
+ n.applyChanges(prefs);
+ prefs = n;
+ }
+ final PreferencesImpl root = prefs.getRoot();
+ try {
+ final OutputStream os = this.getOutputStream(root.getDescription());
+ this.write(root, os);
+ os.close();
+ } catch (IOException ioe) {
+ throw new BackingStoreException("Unable to store preferences.", ioe);
+ }
+ }
+
+ /**
+ * Has the tree changes?
+ */
+ protected boolean hasChanges(PreferencesImpl prefs) {
+ if ( prefs.getChangeSet().hasChanges() ) {
+ return true;
+ }
+ final Iterator i = prefs.getChildren().iterator();
+ while ( i.hasNext() ) {
+ final PreferencesImpl current = (PreferencesImpl) i.next();
+ if ( this.hasChanges(current) ) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @see org.apache.felix.sandbox.preferences.BackingStore#update(org.apache.felix.sandbox.preferences.PreferencesImpl)
+ */
+ public void update(PreferencesImpl prefs) throws BackingStoreException {
+ final PreferencesImpl root = this.load(prefs.getBackingStoreManager(), prefs.getDescription());
+ if ( root != null ) {
+ // and now update
+ if ( root.nodeExists(prefs.absolutePath()) ) {
+ final PreferencesImpl updated = (PreferencesImpl)root.node(prefs.absolutePath());
+ prefs.update(updated);
+ }
+ }
+ }
+
+ /**
+ * Write the preferences recursively to the output stream.
+ * @param prefs
+ * @param os
+ * @throws IOException
+ */
+ protected void write(PreferencesImpl prefs, OutputStream os)
+ throws IOException {
+ this.writePreferences(prefs, os);
+ final ObjectOutputStream oos = new ObjectOutputStream(os);
+ final Collection children = prefs.getChildren();
+ oos.writeInt(children.size());
+ oos.flush();
+ final Iterator i = children.iterator();
+ while ( i.hasNext() ) {
+ final PreferencesImpl child = (PreferencesImpl) i.next();
+ final byte[] name = child.name().getBytes("utf-8");
+ oos.writeInt(name.length);
+ oos.write(name);
+ oos.flush();
+ this.write(child, os);
+ }
+ }
+
+ /**
+ * Read the preferences recursively from the input stream.
+ * @param prefs
+ * @param is
+ * @throws IOException
+ */
+ protected void read(PreferencesImpl prefs, InputStream is)
+ throws IOException {
+ this.readPreferences(prefs, is);
+ final ObjectInputStream ois = new ObjectInputStream(is);
+ final int numberOfChilren = ois.readInt();
+ for(int i=0; i<numberOfChilren; i++) {
+ int length = ois.readInt();
+ final byte[] name = new byte[length];
+ ois.readFully(name);
+ final PreferencesImpl impl = (PreferencesImpl)prefs.node(new String(name, "utf-8"));
+ this.read(impl, is);
+ }
+ }
+
+ /**
+ * Load this preferences from an input stream.
+ * Currently the prefs are read from an object input stream and
+ * the serialization is done by hand.
+ * The changeSet is neither updated nor cleared in order to provide
+ * an update/sync functionality. This has to be done at a higher level.
+ */
+ protected void readPreferences(PreferencesImpl prefs, InputStream in) throws IOException {
+ final ObjectInputStream ois = new ObjectInputStream(in);
+ final int size = ois.readInt();
+ for(int i=0; i<size; i++) {
+ int keyLength = ois.readInt();
+ int valueLength = ois.readInt();
+ final byte[] key = new byte[keyLength];
+ final byte[] value = new byte[valueLength];
+ ois.readFully(key);
+ ois.readFully(value);
+ prefs.getProperties().put(new String(key, "utf-8"), new String(value, "utf-8"));
+ }
+ }
+
+ /**
+ * Save this preferences to an output stream.
+ * Currently the prefs are written through an object output
+ * stream with handmade serialization of strings.
+ * The changeSet is neither updated nor cleared in order to provide
+ * an update/sync functionality. This has to be done at a higher level.
+ */
+ protected void writePreferences(PreferencesImpl prefs, OutputStream out) throws IOException {
+ final ObjectOutputStream oos = new ObjectOutputStream(out);
+ final int size = prefs.getProperties().size();
+ oos.writeInt(size);
+ final Iterator i = prefs.getProperties().entrySet().iterator();
+ while ( i.hasNext() ) {
+ final Map.Entry entry = (Map.Entry)i.next();
+ final byte[] key = entry.getKey().toString().getBytes("utf-8");
+ final byte[] value = entry.getValue().toString().getBytes("utf-8");
+ oos.writeInt(key.length);
+ oos.writeInt(value.length);
+ oos.write(key);
+ oos.write(value);
+ }
+ oos.flush();
+ }
+}