I thought I'd post this for comment before checking it in.
Also, I haven't written tests yet.
I happened to look at java.util.prefs yesterday and I noticed some
problems. This patch attempts to fix all of them (except for the
biggest problem, which is that we still only use the memory-based
provider).
* Exporting a node should use UTF-8 and should use a specific DOCTYPE
* We didn't implement listeners or events on AbstractPreferences.
Apparently events are delivered in a separate thread, which is what
this patch implements.
* Some methods were undocumented
* Some of the locking looked incorrect
* The event types are not actually serializable
* A couple things were commented out, pending 1.4 support -- we've got
that now
One thing I wasn't sure about is whether/how to get the new event
dispatch thread into an appropriate ThreadGroup. Perhaps we ought to
have some private, classpath-internals thread group for daemon threads
like this?
Tom
2006-02-14 Tom Tromey <[EMAIL PROTECTED]>
* gnu/java/util/prefs/EventDispatcher.java: New file.
* gnu/java/util/prefs/NodeWriter.java (NodeWriter): Removed.
(NodeWriter): Specify UTF-8.
(writeHeader): Emit DOCTYPE.
* java/util/prefs/Preferences.java (getFactory): Add cause to
exception.
(exportNode): Documented.
(exportSubtree): Likewise.
(importPreferences): Likewise.
* java/util/prefs/NodeChangeEvent.java (readObject): New method.
(writeObject): Likewise.
* java/util/prefs/PreferenceChangeEvent.java (readObject): New method.
(writeObject): Likewise.
* java/util/prefs/AbstractPreferences.java (putBoolean): Use 1.4 code.
(nodeListeners): New field.
(preferenceListeners): Likewise.
(addNodeChangeListener): Implemented.
(addPreferenceChangeListener): Likewise.
(removeNodeChangeListener): Likewise.
(removePreferenceChangeListener): Likewise.
(fire): New methods.
(put): Fire event.
(remove): Likewise.
(purge): Likewise. Fixed synchronization.
(removeNode): Fixed synchronization.
(getNode): Fire event.
(flushNode): Fixed synchronization.
Index: gnu/java/util/prefs/NodeWriter.java
===================================================================
RCS file: /cvsroot/classpath/classpath/gnu/java/util/prefs/NodeWriter.java,v
retrieving revision 1.3
diff -u -r1.3 NodeWriter.java
--- gnu/java/util/prefs/NodeWriter.java 2 Jul 2005 20:32:15 -0000 1.3
+++ gnu/java/util/prefs/NodeWriter.java 14 Feb 2006 18:44:35 -0000
@@ -41,6 +41,7 @@
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
+import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.util.StringTokenizer;
@@ -67,23 +68,24 @@
private boolean subtree;
/**
- * Creates a new NodeWriter for the given preferences node and writer.
- */
- public NodeWriter(Preferences prefs, Writer w) {
- this.prefs = prefs;
- if (w instanceof BufferedWriter) {
- this.bw = (BufferedWriter) w;
- } else {
- this.bw = new BufferedWriter(w);
- }
- }
-
- /**
* Creates a new NodeWriter for the given preferences node and
* outputstream. Creates a new OutputStreamWriter.
*/
public NodeWriter(Preferences prefs, OutputStream os) {
- this(prefs, new OutputStreamWriter(os));
+ this.prefs = prefs;
+ Writer w;
+ try
+ {
+ w = new OutputStreamWriter(os, "UTF-8");
+ }
+ catch (UnsupportedEncodingException uee)
+ {
+ // Shouldn't happen, since we always have UTF-8 available.
+ InternalError ie = new InternalError("UTF-8 encoding missing");
+ ie.initCause(uee);
+ throw ie;
+ }
+ this.bw = new BufferedWriter(w);
}
/**
@@ -112,6 +114,9 @@
private void writeHeader() throws BackingStoreException, IOException {
bw.write("<?xml version=\"1.0\"?>");
bw.newLine();
+ bw.write("<!DOCTYPE preferences SYSTEM "
+ + "\"http://java.sun.com/dtd/preferences.dtd\">");
+ bw.newLine();
bw.newLine();
bw.write("<!-- GNU Classpath java.util.prefs Preferences ");
Index: java/util/prefs/AbstractPreferences.java
===================================================================
RCS file:
/cvsroot/classpath/classpath/java/util/prefs/AbstractPreferences.java,v
retrieving revision 1.12
diff -u -r1.12 AbstractPreferences.java
--- java/util/prefs/AbstractPreferences.java 2 Jul 2005 20:32:44 -0000
1.12
+++ java/util/prefs/AbstractPreferences.java 14 Feb 2006 18:44:36 -0000
@@ -1,5 +1,5 @@
/* AbstractPreferences -- Partial implementation of a Preference node
- Copyright (C) 2001, 2003, 2004 Free Software Foundation, Inc.
+ Copyright (C) 2001, 2003, 2004, 2006 Free Software Foundation, Inc.
This file is part of GNU Classpath.
@@ -38,11 +38,13 @@
package java.util.prefs;
+import gnu.java.util.prefs.EventDispatcher;
import gnu.java.util.prefs.NodeWriter;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.TreeSet;
@@ -97,6 +99,16 @@
*/
private HashMap childCache = new HashMap();
+ /**
+ * A list of all the registered NodeChangeListener objects.
+ */
+ private ArrayList nodeListeners;
+
+ /**
+ * A list of all the registered PreferenceChangeListener objects.
+ */
+ private ArrayList preferenceListeners;
+
// constructor
/**
@@ -325,8 +337,9 @@
// Not in childCache yet so create a new sub node
child = childSpi(childName);
- // XXX - check if node is new
childCache.put(childName, child);
+ if (child.newNode && nodeListeners != null)
+ fire(new NodeChangeEvent(this, child), true);
}
// Lock the child and go down
@@ -477,9 +490,7 @@
// export methods
- /**
- * XXX
- */
+ // Inherit javadoc.
public void exportNode(OutputStream os)
throws BackingStoreException,
IOException
@@ -488,9 +499,7 @@
nodeWriter.writePrefs();
}
- /**
- * XXX
- */
+ // Inherit javadoc.
public void exportSubtree(OutputStream os)
throws BackingStoreException,
IOException
@@ -765,8 +774,8 @@
* Key and value cannot be null, the key cannot exceed 80 characters
* and the value cannot exceed 8192 characters.
* <p>
- * The result will be immediatly visible in this VM, but may not be
- * immediatly written to the backing store.
+ * The result will be immediately visible in this VM, but may not be
+ * immediately written to the backing store.
* <p>
* Checks that key and value are valid, locks this node, and checks that
* the node has not been removed. Then it calls <code>putSpi()</code>.
@@ -789,7 +798,8 @@
putSpi(key, value);
- // XXX - fire events
+ if (preferenceListeners != null)
+ fire(new PreferenceChangeEvent(this, key, value));
}
}
@@ -804,9 +814,7 @@
* @exception IllegalStateException when this node has been removed
*/
public void putBoolean(String key, boolean value) {
- put(key, String.valueOf(value));
- // XXX - Use when using 1.4 compatible Boolean
- // put(key, Boolean.toString(value));
+ put(key, Boolean.toString(value));
}
/**
@@ -935,8 +943,8 @@
/**
* Removes the preferences entry from this preferences node.
* <p>
- * The result will be immediatly visible in this VM, but may not be
- * immediatly written to the backing store.
+ * The result will be immediately visible in this VM, but may not be
+ * immediately written to the backing store.
* <p>
* This implementation checks that the key is not larger then 80
* characters, gets the lock of this node, checks that the node has
@@ -955,6 +963,9 @@
throw new IllegalStateException("Node removed");
removeSpi(key);
+
+ if (preferenceListeners != null)
+ fire(new PreferenceChangeEvent(this, key, null));
}
}
@@ -1049,7 +1060,7 @@
for (int i = 0; i < keys.length; i++) {
// Have to lock this node again to access the childCache
AbstractPreferences subNode;
- synchronized(this) {
+ synchronized(lock) {
subNode = (AbstractPreferences) childCache.get(keys[i]);
}
@@ -1087,8 +1098,8 @@
if (parent == null)
throw new UnsupportedOperationException("Cannot remove root node");
- synchronized(parent) {
- synchronized(this) {
+ synchronized (parent.lock) {
+ synchronized(this.lock) {
if (isRemoved())
throw new IllegalStateException("Node Removed");
@@ -1122,7 +1133,7 @@
Iterator i = childCache.values().iterator();
while (i.hasNext()) {
AbstractPreferences node = (AbstractPreferences) i.next();
- synchronized(node) {
+ synchronized(node.lock) {
node.purge();
}
}
@@ -1134,30 +1145,131 @@
removeNodeSpi();
removed = true;
- // XXX - check for listeners
+ if (nodeListeners != null)
+ fire(new NodeChangeEvent(parent, this), false);
}
// listener methods
/**
- * XXX
+ * Add a listener which is notified when a sub-node of this node
+ * is added or removed.
+ * @param listener the listener to add
+ */
+ public void addNodeChangeListener(NodeChangeListener listener)
+ {
+ synchronized (lock)
+ {
+ if (isRemoved())
+ throw new IllegalStateException("node has been removed");
+ if (listener == null)
+ throw new NullPointerException("listener is null");
+ if (nodeListeners == null)
+ nodeListeners = new ArrayList();
+ nodeListeners.add(listener);
+ }
+ }
+
+ /**
+ * Add a listener which is notified when a value in this node
+ * is added, changed, or removed.
+ * @param listener the listener to add
+ */
+ public void addPreferenceChangeListener(PreferenceChangeListener listener)
+ {
+ synchronized (lock)
+ {
+ if (isRemoved())
+ throw new IllegalStateException("node has been removed");
+ if (listener == null)
+ throw new NullPointerException("listener is null");
+ if (preferenceListeners == null)
+ preferenceListeners = new ArrayList();
+ preferenceListeners.add(listener);
+ }
+ }
+
+ /**
+ * Remove the indicated node change listener from the list of
+ * listeners to notify.
+ * @param listener the listener to remove
*/
- public void addNodeChangeListener(NodeChangeListener listener) {
- // XXX
+ public void removeNodeChangeListener(NodeChangeListener listener)
+ {
+ synchronized (lock)
+ {
+ if (isRemoved())
+ throw new IllegalStateException("node has been removed");
+ if (listener == null)
+ throw new NullPointerException("listener is null");
+ if (nodeListeners != null)
+ nodeListeners.remove(listener);
+ }
}
- public void addPreferenceChangeListener(PreferenceChangeListener listener)
{
- // XXX
+ /**
+ * Remove the indicated preference change listener from the list of
+ * listeners to notify.
+ * @param listener the listener to remove
+ */
+ public void removePreferenceChangeListener (PreferenceChangeListener
listener)
+ {
+ synchronized (lock)
+ {
+ if (isRemoved())
+ throw new IllegalStateException("node has been removed");
+ if (listener == null)
+ throw new NullPointerException("listener is null");
+ if (preferenceListeners != null)
+ preferenceListeners.remove(listener);
+ }
}
- public void removeNodeChangeListener(NodeChangeListener listener) {
- // XXX
+ /**
+ * Send a preference change event to all listeners. Note that
+ * the caller is responsible for holding the node's lock, and
+ * for checking that the list of listeners is not null.
+ * @param event the event to send
+ */
+ private void fire(final PreferenceChangeEvent event)
+ {
+ Iterator it = preferenceListeners.iterator();
+ while (it.hasNext())
+ {
+ final PreferenceChangeListener l = (PreferenceChangeListener)
it.next();
+ EventDispatcher.dispatch(new Runnable()
+ {
+ public void run()
+ {
+ l.preferenceChange(event);
+ }
+ });
+ }
}
- public void removePreferenceChangeListener
- (PreferenceChangeListener listener)
+ /**
+ * Send a node change event to all listeners. Note that
+ * the caller is responsible for holding the node's lock, and
+ * for checking that the list of listeners is not null.
+ * @param event the event to send
+ */
+ private void fire(final NodeChangeEvent event, final boolean added)
{
- // XXX
+ Iterator it = nodeListeners.iterator();
+ while (it.hasNext())
+ {
+ final NodeChangeListener l = (NodeChangeListener) it.next();
+ EventDispatcher.dispatch(new Runnable()
+ {
+ public void run()
+ {
+ if (added)
+ l.childAdded(event);
+ else
+ l.childRemoved(event);
+ }
+ });
+ }
}
// abstract spi methods
Index: java/util/prefs/NodeChangeEvent.java
===================================================================
RCS file: /cvsroot/classpath/classpath/java/util/prefs/NodeChangeEvent.java,v
retrieving revision 1.4
diff -u -r1.4 NodeChangeEvent.java
--- java/util/prefs/NodeChangeEvent.java 2 Jul 2005 20:32:44 -0000
1.4
+++ java/util/prefs/NodeChangeEvent.java 14 Feb 2006 18:44:36 -0000
@@ -1,5 +1,5 @@
/* NodeChangeEvent - ObjectEvent fired when a Preference node is added/removed
- Copyright (C) 2001 Free Software Foundation, Inc.
+ Copyright (C) 2001, 2006 Free Software Foundation, Inc.
This file is part of GNU Classpath.
@@ -37,6 +37,10 @@
package java.util.prefs;
+import java.io.IOException;
+import java.io.NotSerializableException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
import java.util.EventObject;
/**
@@ -44,12 +48,16 @@
* This event is only generated when a new subnode is added or a subnode is
* removed from a preference node. Changes in the entries of a preference node
* are indicated with a <code>PreferenceChangeEvent</code>.
+ * <p>
+ * Note that although this class is marked as serializable, attempts to
+ * serialize it will fail with NotSerializableException.
*
* @since 1.4
* @author Mark Wielaard ([EMAIL PROTECTED])
*/
public class NodeChangeEvent extends EventObject {
+ // We have this to placate the compiler.
private static final long serialVersionUID =8068949086596572957L;
/**
@@ -88,4 +96,16 @@
public Preferences getChild() {
return child;
}
+
+ private void readObject(ObjectInputStream ois)
+ throws IOException
+ {
+ throw new NotSerializableException("LineEvent is not serializable");
+ }
+
+ private void writeObject(ObjectOutputStream oos)
+ throws IOException
+ {
+ throw new NotSerializableException("LineEvent is not serializable");
+ }
}
Index: java/util/prefs/PreferenceChangeEvent.java
===================================================================
RCS file:
/cvsroot/classpath/classpath/java/util/prefs/PreferenceChangeEvent.java,v
retrieving revision 1.4
diff -u -r1.4 PreferenceChangeEvent.java
--- java/util/prefs/PreferenceChangeEvent.java 2 Jul 2005 20:32:44 -0000
1.4
+++ java/util/prefs/PreferenceChangeEvent.java 14 Feb 2006 18:44:36 -0000
@@ -1,5 +1,5 @@
/* PreferenceChangeEvent - ObjectEvent fired when a Preferences entry changes
- Copyright (C) 2001 Free Software Foundation, Inc.
+ Copyright (C) 2001, 2006 Free Software Foundation, Inc.
This file is part of GNU Classpath.
@@ -37,6 +37,10 @@
package java.util.prefs;
+import java.io.IOException;
+import java.io.NotSerializableException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
import java.util.EventObject;
/**
@@ -47,12 +51,16 @@
* Preference change events are only generated for entries in one particular
* preference node. Notification of subnode addition/removal is given by a
* <code>NodeChangeEvent</code>.
+ * <p>
+ * Note that although this class is marked as serializable, attempts to
+ * serialize it will fail with NotSerializableException.
*
* @since 1.4
* @author Mark Wielaard ([EMAIL PROTECTED])
*/
public class PreferenceChangeEvent extends EventObject {
+ // We have this to placate the compiler.
private static final long serialVersionUID = 793724513368024975L;
/**
@@ -102,4 +110,16 @@
public String getNewValue() {
return newValue;
}
+
+ private void readObject(ObjectInputStream ois)
+ throws IOException
+ {
+ throw new NotSerializableException("LineEvent is not serializable");
+ }
+
+ private void writeObject(ObjectOutputStream oos)
+ throws IOException
+ {
+ throw new NotSerializableException("LineEvent is not serializable");
+ }
}
Index: java/util/prefs/Preferences.java
===================================================================
RCS file: /cvsroot/classpath/classpath/java/util/prefs/Preferences.java,v
retrieving revision 1.12
diff -u -r1.12 Preferences.java
--- java/util/prefs/Preferences.java 17 Sep 2005 15:46:23 -0000 1.12
+++ java/util/prefs/Preferences.java 14 Feb 2006 18:44:36 -0000
@@ -1,5 +1,5 @@
/* Preferences -- Preference node containing key value entries and subnodes
- Copyright (C) 2001, 2004, 2005 Free Software Foundation, Inc.
+ Copyright (C) 2001, 2004, 2005, 2006 Free Software Foundation, Inc.
This file is part of GNU Classpath.
@@ -219,8 +219,7 @@
catch (Exception e)
{
throw new RuntimeException ("Couldn't load default factory"
- + " '"+ defaultFactoryClass +"'");
- // XXX - when using 1.4 compatible throwables add cause
+ + " '"+ defaultFactoryClass +"'", e);
}
}
@@ -288,7 +287,13 @@
}
/**
- * XXX
+ * Import preferences from the given input stream. This expects
+ * preferences to be represented in XML as emitted by
+ * [EMAIL PROTECTED] #exportNode(OutputStream)} and
+ * [EMAIL PROTECTED] #exportSubtree(OutputStream)}.
+ * @throws IOException if there is an error while reading
+ * @throws InvalidPreferencesFormatException if the XML is not properly
+ * formatted
*/
public static void importPreferences(InputStream is)
throws InvalidPreferencesFormatException,
@@ -385,14 +390,28 @@
// abstract methods (export)
/**
- * XXX
+ * Export this node, but not its descendants, as XML to the
+ * indicated output stream. The XML will be encoded using UTF-8
+ * and will use a specified document type:<br>
+ * <code><!DOCTYPE preferences SYSTEM
"http://java.sun.com/dtd/preferences.dtd"></code><br>
+ * @param os the output stream to which the XML is sent
+ * @throws BackingStoreException if preference data cannot be read
+ * @throws IOException if an error occurs while writing the XML
+ * @throws IllegalStateException if this node or an ancestor has been
removed
*/
public abstract void exportNode(OutputStream os)
throws BackingStoreException,
IOException;
/**
- * XXX
+ * Export this node and all its descendants as XML to the
+ * indicated output stream. The XML will be encoded using UTF-8
+ * and will use a specified document type:<br>
+ * <code><!DOCTYPE preferences SYSTEM
"http://java.sun.com/dtd/preferences.dtd"></code><br>
+ * @param os the output stream to which the XML is sent
+ * @throws BackingStoreException if preference data cannot be read
+ * @throws IOException if an error occurs while writing the XML
+ * @throws IllegalStateException if this node or an ancestor has been
removed
*/
public abstract void exportSubtree(OutputStream os)
throws BackingStoreException,
Index: gnu/java/util/prefs/EventDispatcher.java
===================================================================
RCS file: gnu/java/util/prefs/EventDispatcher.java
diff -N gnu/java/util/prefs/EventDispatcher.java
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ gnu/java/util/prefs/EventDispatcher.java 1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,110 @@
+/* EventDispatcher.java -- Dispatch events for prefs
+ Copyright (C) 2006 Free Software Foundation, Inc.
+
+This file is part of GNU Classpath.
+
+GNU Classpath is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2, or (at your option)
+any later version.
+
+GNU Classpath is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with GNU Classpath; see the file COPYING. If not, write to the
+Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+02110-1301 USA.
+
+Linking this library statically or dynamically with other modules is
+making a combined work based on this library. Thus, the terms and
+conditions of the GNU General Public License cover the whole
+combination.
+
+As a special exception, the copyright holders of this library give you
+permission to link this library with independent modules to produce an
+executable, regardless of the license terms of these independent
+modules, and to copy and distribute the resulting executable under
+terms of your choice, provided that you also meet, for each linked
+independent module, the terms and conditions of the license of that
+module. An independent module is a module which is not derived from
+or based on this library. If you modify this library, you may extend
+this exception to your version of the library, but you are not
+obligated to do so. If you do not wish to do so, delete this
+exception statement from your version. */
+
+
+package gnu.java.util.prefs;
+
+import java.util.ArrayList;
+
+/**
+ * This is a helper class used for dispatching events for
+ * the prefs package.
+ */
+public class EventDispatcher extends Thread
+{
+ // This is a singleton class. We dispatch all events via a
+ // new Thread which is created on demand.
+ private static final Thread dispatchThread = new EventDispatcher();
+
+ // This is a queue of events to dispatch. This thread waits on
+ // the queue and when notified will remove events until the queue
+ // is empty.
+ private static final ArrayList queue = new ArrayList();
+
+ private EventDispatcher()
+ {
+ setDaemon(true);
+ start();
+ }
+
+ public void run()
+ {
+ while (true)
+ {
+ Runnable r;
+ synchronized (queue)
+ {
+ while (queue.size() == 0)
+ {
+ try
+ {
+ wait();
+ }
+ catch (InterruptedException _)
+ {
+ // Ignore.
+ }
+ }
+ r = (Runnable) queue.remove(0);
+ }
+ // Invoke outside the synchronization, so that
+ // we aren't blocking other threads from posting events.
+ try
+ {
+ r.run();
+ }
+ catch (Throwable _)
+ {
+ // Ignore.
+ }
+ }
+ }
+
+ /**
+ * Add a new runnable to the event dispatch queue. The
+ * runnable will be invoked in the event dispatch queue
+ * without any locks held.
+ * @param runner the Runnable to dispatch
+ */
+ public static void dispatch(Runnable runner)
+ {
+ synchronized (queue)
+ {
+ queue.add(runner);
+ }
+ }
+}