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>&lt;!DOCTYPE preferences SYSTEM 
"http://java.sun.com/dtd/preferences.dtd"&gt;</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>&lt;!DOCTYPE preferences SYSTEM 
"http://java.sun.com/dtd/preferences.dtd"&gt;</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);
+      }
+  }
+}

Reply via email to