1/*
2 *  Licensed to the Apache Software Foundation (ASF) under one or more
3 *  contributor license agreements.  See the NOTICE file distributed with
4 *  this work for additional information regarding copyright ownership.
5 *  The ASF licenses this file to You under the Apache License, Version 2.0
6 *  (the "License"); you may not use this file except in compliance with
7 *  the License.  You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 *  Unless required by applicable law or agreed to in writing, software
12 *  distributed under the License is distributed on an "AS IS" BASIS,
13 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *  See the License for the specific language governing permissions and
15 *  limitations under the License.
16 */
17
18package java.util.prefs;
19
20import java.io.IOException;
21import java.io.OutputStream;
22import java.nio.charset.StandardCharsets;
23import java.util.Collection;
24import java.util.EventListener;
25import java.util.EventObject;
26import java.util.HashMap;
27import java.util.LinkedList;
28import java.util.List;
29import java.util.Map;
30import java.util.TreeSet;
31import libcore.io.Base64;
32import libcore.util.EmptyArray;
33
34/**
35 * This abstract class is a partial implementation of the abstract class
36 * Preferences, which can be used to simplify {@code Preferences} provider's
37 * implementation. This class defines nine abstract SPI methods, which must be
38 * implemented by a preference provider.
39 *
40 * @since 1.4
41 * @see Preferences
42 */
43public abstract class AbstractPreferences extends Preferences {
44    /*
45     * -----------------------------------------------------------
46     * Class fields
47     * -----------------------------------------------------------
48     */
49    /** the unhandled events collection */
50    private static final List<EventObject> events = new LinkedList<EventObject>();
51    /** the event dispatcher thread */
52    private static final EventDispatcher dispatcher = new EventDispatcher("Preference Event Dispatcher");
53
54    /*
55     * -----------------------------------------------------------
56     * Class initializer
57     * -----------------------------------------------------------
58     */
59    static {
60        dispatcher.setDaemon(true);
61        dispatcher.start();
62        Runtime.getRuntime().addShutdownHook(new Thread() {
63            @Override
64            public void run() {
65                Preferences uroot = Preferences.userRoot();
66                Preferences sroot = Preferences.systemRoot();
67                try {
68                    uroot.flush();
69                } catch (BackingStoreException e) {
70                    // ignore
71                }
72                try {
73                    sroot.flush();
74                } catch (BackingStoreException e) {
75                    // ignore
76                }
77            }
78        });
79    }
80
81    /*
82     * -----------------------------------------------------------
83     * Instance fields (package-private)
84     * -----------------------------------------------------------
85     */
86    /** true if this node is in user preference hierarchy */
87    boolean userNode;
88
89    /*
90     * -----------------------------------------------------------
91     * Instance fields (private)
92     * -----------------------------------------------------------
93     */
94    /**
95     * The object used to lock this node.
96     */
97    protected final Object lock;
98
99    /**
100     * This field is true if this node is created while it doesn't exist in the
101     * backing store. This field's default value is false, and it is checked
102     * when the node creation is completed, and if it is true, the node change
103     * event will be fired for this node's parent.
104     */
105    protected boolean newNode;
106
107    /** cached child nodes */
108    private Map<String, AbstractPreferences> cachedNode;
109
110    //the collections of listeners
111    private List<EventListener> nodeChangeListeners;
112    private List<EventListener> preferenceChangeListeners;
113
114    //this node's name
115    private String nodeName;
116
117    //handler to this node's parent
118    private AbstractPreferences parentPref;
119
120    //true if this node has been removed
121    private boolean isRemoved;
122
123    //handler to this node's root node
124    private AbstractPreferences root;
125
126    /*
127     * -----------------------------------------------------------
128     * Constructors
129     * -----------------------------------------------------------
130     */
131    /**
132     * Constructs a new {@code AbstractPreferences} instance using the given
133     * parent node and node name.
134     *
135     * @param parent
136     *            the parent node of the new node or {@code null} to indicate
137     *            that the new node is a root node.
138     * @param name
139     *            the name of the new node or an empty string to indicate that
140     *            this node is called "root".
141     * @throws IllegalArgumentException
142     *             if the name contains a slash character or is empty if {@code
143     *             parent} is not {@code null}.
144     */
145    protected AbstractPreferences(AbstractPreferences parent, String name) {
146        if ((parent == null ^ name.length() == 0) || name.indexOf("/") >= 0) {
147            throw new IllegalArgumentException();
148        }
149        root = (parent == null) ? this : parent.root;
150        nodeChangeListeners = new LinkedList<EventListener>();
151        preferenceChangeListeners = new LinkedList<EventListener>();
152        isRemoved = false;
153        cachedNode = new HashMap<String, AbstractPreferences>();
154        nodeName = name;
155        parentPref = parent;
156        lock = new Object();
157        userNode = root.userNode;
158    }
159
160    /*
161     * -----------------------------------------------------------
162     * Methods
163     * -----------------------------------------------------------
164     */
165    /**
166     * Returns an array of all cached child nodes.
167     *
168     * @return the array of cached child nodes.
169     */
170    protected final AbstractPreferences[] cachedChildren() {
171        return cachedNode.values().toArray(new AbstractPreferences[cachedNode.size()]);
172    }
173
174    /**
175     * Returns the child node with the specified name or {@code null} if it
176     * doesn't exist. Implementers can assume that the name supplied to this
177     * method will be a valid node name string (conforming to the node naming
178     * format) and will not correspond to a node that has been cached or
179     * removed.
180     *
181     * @param name
182     *            the name of the desired child node.
183     * @return the child node with the given name or {@code null} if it doesn't
184     *         exist.
185     * @throws BackingStoreException
186     *             if the backing store is unavailable or causes an operation
187     *             failure.
188     */
189    protected AbstractPreferences getChild(String name)
190            throws BackingStoreException {
191        synchronized (lock) {
192            checkState();
193            AbstractPreferences result = null;
194            String[] childrenNames = childrenNames();
195            for (String childrenName : childrenNames) {
196                if (childrenName.equals(name)) {
197                    result = childSpi(name);
198                    break;
199                }
200            }
201            return result;
202        }
203
204    }
205
206    /**
207     * Returns whether this node has been removed by invoking the method {@code
208     * removeNode()}.
209     *
210     * @return {@code true}, if this node has been removed, {@code false}
211     *         otherwise.
212     */
213    protected boolean isRemoved() {
214        synchronized (lock) {
215            return isRemoved;
216        }
217    }
218
219    /**
220     * Flushes changes of this node to the backing store. This method should
221     * only flush this node and should not include the descendant nodes. Any
222     * implementation that wants to provide functionality to flush all nodes
223     * at once should override the method {@link #flush() flush()}.
224     *
225     * @throws BackingStoreException
226     *             if the backing store is unavailable or causes an operation
227     *             failure.
228     */
229    protected abstract void flushSpi() throws BackingStoreException;
230
231    /**
232     * Returns the names of all of the child nodes of this node or an empty
233     * array if this node has no children. The names of cached children are not
234     * required to be returned.
235     *
236     * @return the names of this node's children.
237     * @throws BackingStoreException
238     *             if the backing store is unavailable or causes an operation
239     *             failure.
240     */
241    protected abstract String[] childrenNamesSpi() throws BackingStoreException;
242
243    /**
244     * Returns the child preference node with the given name, creating it
245     * if it does not exist. The caller of this method should ensure that the
246     * given name is valid and that this node has not been removed or cached.
247     * If the named node has just been removed, the implementation
248     * of this method must create a new one instead of reactivating the removed
249     * one.
250     * <p>
251     * The new creation is not required to be persisted immediately until the
252     * flush method will be invoked.
253     * </p>
254     *
255     * @param name
256     *            the name of the child preference to be returned.
257     * @return the child preference node.
258     */
259    protected abstract AbstractPreferences childSpi(String name);
260
261
262    /**
263     * Puts the given key-value pair into this node. Caller of this method
264     * should ensure that both of the given values are valid and that this
265     * node has not been removed.
266     *
267     * @param name
268     *            the given preference key.
269     * @param value
270     *            the given preference value.
271     */
272    protected abstract void putSpi(String name, String value);
273
274    /**
275     * Gets the preference value mapped to the given key. The caller of this
276     * method should ensure that the given key is valid and that this node has
277     * not been removed. This method should not throw any exceptions but if it
278     * does, the caller will ignore the exception, regarding it as a {@code
279     * null} return value.
280     *
281     * @param key
282     *            the given key to be searched for.
283     * @return the preference value mapped to the given key.
284     */
285    protected abstract String getSpi(String key);
286
287
288    /**
289     * Returns an array of all preference keys of this node or an empty array if
290     * no preferences have been found. The caller of this method should ensure
291     * that this node has not been removed.
292     *
293     * @return the array of all preference keys.
294     * @throws BackingStoreException
295     *             if the backing store is unavailable or causes an operation
296     *             failure.
297     */
298    protected abstract String[] keysSpi() throws BackingStoreException;
299
300    /**
301     * Removes this node from the preference hierarchy tree. The caller of this
302     * method should ensure that this node has no child nodes, which means the
303     * method {@link Preferences#removeNode() Preferences.removeNode()} should
304     * invoke this method multiple-times in bottom-up pattern. The removal is
305     * not required to be persisted until after it is flushed.
306     *
307     * @throws BackingStoreException
308     *             if the backing store is unavailable or causes an operation
309     *             failure.
310     */
311    protected abstract void removeNodeSpi() throws BackingStoreException;
312
313    /**
314     * Removes the preference with the specified key. The caller of this method
315     * should ensure that the given key is valid and that this node has not been
316     * removed.
317     *
318     * @param key
319     *            the key of the preference that is to be removed.
320     */
321    protected abstract void removeSpi(String key);
322
323    /**
324     * Synchronizes this node with the backing store. This method should only
325     * synchronize this node and should not include the descendant nodes. An
326     * implementation that wants to provide functionality to synchronize all
327     * nodes at once should override the method {@link #sync() sync()}.
328     *
329     * @throws BackingStoreException
330     *             if the backing store is unavailable or causes an operation
331     *             failure.
332     */
333    protected abstract void syncSpi() throws BackingStoreException;
334
335    /*
336     * -----------------------------------------------------------
337     * Methods inherited from Preferences
338     * -----------------------------------------------------------
339     */
340    @Override
341    public String absolutePath() {
342        if (parentPref == null) {
343            return "/";
344        } else if (parentPref == root) {
345            return "/" + nodeName;
346        }
347        return parentPref.absolutePath() + "/" + nodeName;
348    }
349
350    @Override
351    public String[] childrenNames() throws BackingStoreException {
352        synchronized (lock) {
353            checkState();
354            TreeSet<String> result = new TreeSet<String>(cachedNode.keySet());
355            String[] names = childrenNamesSpi();
356            for (int i = 0; i < names.length; i++) {
357                result.add(names[i]);
358            }
359            return result.toArray(new String[result.size()]);
360        }
361    }
362
363    @Override
364    public void clear() throws BackingStoreException {
365        synchronized (lock) {
366            for (String key : keys()) {
367                remove(key);
368            }
369        }
370    }
371
372    @Override
373    public void exportNode(OutputStream ostream) throws IOException, BackingStoreException {
374        if (ostream == null) {
375            throw new NullPointerException("ostream == null");
376        }
377        checkState();
378        XMLParser.exportPrefs(this, ostream, false);
379    }
380
381    @Override
382    public void exportSubtree(OutputStream ostream) throws IOException, BackingStoreException {
383        if (ostream == null) {
384            throw new NullPointerException("ostream == null");
385        }
386        checkState();
387        XMLParser.exportPrefs(this, ostream, true);
388    }
389
390    @Override
391    public void flush() throws BackingStoreException {
392        synchronized (lock) {
393            flushSpi();
394        }
395        AbstractPreferences[] cc = cachedChildren();
396        int i;
397        for (i = 0; i < cc.length; i++) {
398            cc[i].flush();
399        }
400    }
401
402    @Override
403    public String get(String key, String deflt) {
404        if (key == null) {
405            throw new NullPointerException("key == null");
406        }
407        String result = null;
408        synchronized (lock) {
409            checkState();
410            try {
411                result = getSpi(key);
412            } catch (Exception e) {
413                // ignored
414            }
415        }
416        return (result == null ? deflt : result);
417    }
418
419    @Override
420    public boolean getBoolean(String key, boolean deflt) {
421        String result = get(key, null);
422        if (result == null) {
423            return deflt;
424        }
425        if ("true".equalsIgnoreCase(result)) {
426            return true;
427        } else if ("false".equalsIgnoreCase(result)) {
428            return false;
429        } else {
430            return deflt;
431        }
432    }
433
434    @Override
435    public byte[] getByteArray(String key, byte[] deflt) {
436        String svalue = get(key, null);
437        if (svalue == null) {
438            return deflt;
439        }
440        if (svalue.length() == 0) {
441            return EmptyArray.BYTE;
442        }
443        try {
444            byte[] bavalue = svalue.getBytes(StandardCharsets.US_ASCII);
445            if (bavalue.length % 4 != 0) {
446                return deflt;
447            }
448            return Base64.decode(bavalue);
449        } catch (Exception e) {
450            return deflt;
451        }
452    }
453
454    @Override
455    public double getDouble(String key, double deflt) {
456        String result = get(key, null);
457        if (result == null) {
458            return deflt;
459        }
460        try {
461            return Double.parseDouble(result);
462        } catch (NumberFormatException e) {
463            return deflt;
464        }
465    }
466
467    @Override
468    public float getFloat(String key, float deflt) {
469        String result = get(key, null);
470        if (result == null) {
471            return deflt;
472        }
473        try {
474            return Float.parseFloat(result);
475        } catch (NumberFormatException e) {
476            return deflt;
477        }
478    }
479
480    @Override
481    public int getInt(String key, int deflt) {
482        String result = get(key, null);
483        if (result == null) {
484            return deflt;
485        }
486        try {
487            return Integer.parseInt(result);
488        } catch (NumberFormatException e) {
489            return deflt;
490        }
491    }
492
493    @Override
494    public long getLong(String key, long deflt) {
495        String result = get(key, null);
496        if (result == null) {
497            return deflt;
498        }
499        try {
500            return Long.parseLong(result);
501        } catch (NumberFormatException e) {
502            return deflt;
503        }
504    }
505
506    @Override
507    public boolean isUserNode() {
508        return root == Preferences.userRoot();
509    }
510
511    @Override
512    public String[] keys() throws BackingStoreException {
513        synchronized (lock) {
514            checkState();
515            return keysSpi();
516        }
517    }
518
519    @Override
520    public String name() {
521        return nodeName;
522    }
523
524    @Override
525    public Preferences node(String name) {
526        AbstractPreferences startNode = null;
527        synchronized (lock) {
528            checkState();
529            validateName(name);
530            if (name.isEmpty()) {
531                return this;
532            } else if ("/".equals(name)) {
533                return root;
534            }
535            if (name.startsWith("/")) {
536                startNode = root;
537                name = name.substring(1);
538            } else {
539                startNode = this;
540            }
541        }
542        try {
543            return startNode.nodeImpl(name, true);
544        } catch (BackingStoreException e) {
545            // should not happen
546            return null;
547        }
548    }
549
550    private void validateName(String name) {
551        if (name.endsWith("/") && name.length() > 1) {
552            throw new IllegalArgumentException("Name cannot end with '/'");
553        }
554        if (name.indexOf("//") >= 0) {
555            throw new IllegalArgumentException("Name cannot contain consecutive '/' characters");
556        }
557    }
558
559    private AbstractPreferences nodeImpl(String path, boolean createNew)
560            throws BackingStoreException {
561        String[] names = path.split("/");
562        AbstractPreferences currentNode = this;
563        AbstractPreferences temp;
564        for (String name : names) {
565            synchronized (currentNode.lock) {
566                temp = currentNode.cachedNode.get(name);
567                if (temp == null) {
568                    temp = getNodeFromBackend(createNew, currentNode, name);
569                }
570            }
571            currentNode = temp;
572            if (currentNode == null) {
573                break;
574            }
575        }
576        return currentNode;
577    }
578
579    private AbstractPreferences getNodeFromBackend(boolean createNew,
580            AbstractPreferences currentNode, String name) throws BackingStoreException {
581        if (name.length() > MAX_NAME_LENGTH) {
582            throw new IllegalArgumentException("Name '" + name + "' too long");
583        }
584        AbstractPreferences temp;
585        if (createNew) {
586            temp = currentNode.childSpi(name);
587            currentNode.cachedNode.put(name, temp);
588            if (temp.newNode && currentNode.nodeChangeListeners.size() > 0) {
589                currentNode.notifyChildAdded(temp);
590            }
591        } else {
592            temp = currentNode.getChild(name);
593        }
594        return temp;
595    }
596
597    @Override
598    public boolean nodeExists(String name) throws BackingStoreException {
599        if (name == null) {
600            throw new NullPointerException("name == null");
601        }
602        AbstractPreferences startNode = null;
603        synchronized (lock) {
604            if (isRemoved()) {
605                if (name.isEmpty()) {
606                    return false;
607                }
608                throw new IllegalStateException("This node has been removed");
609            }
610            validateName(name);
611            if (name.isEmpty() || "/".equals(name)) {
612                return true;
613            }
614            if (name.startsWith("/")) {
615                startNode = root;
616                name = name.substring(1);
617            } else {
618                startNode = this;
619            }
620        }
621        try {
622            Preferences result = startNode.nodeImpl(name, false);
623            return (result != null);
624        } catch(IllegalArgumentException e) {
625            return false;
626        }
627    }
628
629    @Override
630    public Preferences parent() {
631        checkState();
632        return parentPref;
633    }
634
635    private void checkState() {
636        if (isRemoved()) {
637            throw new IllegalStateException("This node has been removed");
638        }
639    }
640
641    @Override
642    public void put(String key, String value) {
643        if (key == null) {
644            throw new NullPointerException("key == null");
645        } else if (value == null) {
646            throw new NullPointerException("value == null");
647        }
648        if (key.length() > MAX_KEY_LENGTH || value.length() > MAX_VALUE_LENGTH) {
649            throw new IllegalArgumentException();
650        }
651        synchronized (lock) {
652            checkState();
653            putSpi(key, value);
654        }
655        notifyPreferenceChange(key, value);
656    }
657
658    @Override
659    public void putBoolean(String key, boolean value) {
660        put(key, String.valueOf(value));
661    }
662
663    @Override
664    public void putByteArray(String key, byte[] value) {
665        put(key, Base64.encode(value));
666    }
667
668    @Override
669    public void putDouble(String key, double value) {
670        put(key, Double.toString(value));
671    }
672
673    @Override
674    public void putFloat(String key, float value) {
675        put(key, Float.toString(value));
676    }
677
678    @Override
679    public void putInt(String key, int value) {
680        put(key, Integer.toString(value));
681    }
682
683    @Override
684    public void putLong(String key, long value) {
685        put(key, Long.toString(value));
686    }
687
688    @Override
689    public void remove(String key) {
690        synchronized (lock) {
691            checkState();
692            removeSpi(key);
693        }
694        notifyPreferenceChange(key, null);
695    }
696
697    @Override
698    public void removeNode() throws BackingStoreException {
699        if (root == this) {
700            throw new UnsupportedOperationException("Cannot remove root node");
701        }
702        synchronized (parentPref.lock) {
703            removeNodeImpl();
704        }
705    }
706
707    private void removeNodeImpl() throws BackingStoreException {
708        synchronized (lock) {
709            checkState();
710            String[] childrenNames = childrenNamesSpi();
711            for (String childrenName : childrenNames) {
712                if (cachedNode.get(childrenName) == null) {
713                    AbstractPreferences child = childSpi(childrenName);
714                    cachedNode.put(childrenName, child);
715                }
716            }
717
718            final Collection<AbstractPreferences> values = cachedNode.values();
719            final AbstractPreferences[] children = values.toArray(new AbstractPreferences[values.size()]);
720            for (AbstractPreferences child : children) {
721                child.removeNodeImpl();
722            }
723            removeNodeSpi();
724            isRemoved = true;
725            parentPref.cachedNode.remove(nodeName);
726        }
727        if (parentPref.nodeChangeListeners.size() > 0) {
728            parentPref.notifyChildRemoved(this);
729        }
730    }
731
732    @Override
733    public void addNodeChangeListener(NodeChangeListener ncl) {
734        if (ncl == null) {
735            throw new NullPointerException("ncl == null");
736        }
737        checkState();
738        synchronized (nodeChangeListeners) {
739            nodeChangeListeners.add(ncl);
740        }
741    }
742
743    @Override
744    public void addPreferenceChangeListener(PreferenceChangeListener pcl) {
745        if (pcl == null) {
746            throw new NullPointerException("pcl == null");
747        }
748        checkState();
749        synchronized (preferenceChangeListeners) {
750            preferenceChangeListeners.add(pcl);
751        }
752    }
753
754    @Override
755    public void removeNodeChangeListener(NodeChangeListener ncl) {
756        checkState();
757        synchronized (nodeChangeListeners) {
758            int pos;
759            if ((pos = nodeChangeListeners.indexOf(ncl)) == -1) {
760                throw new IllegalArgumentException();
761            }
762            nodeChangeListeners.remove(pos);
763        }
764    }
765
766    @Override
767    public void removePreferenceChangeListener(PreferenceChangeListener pcl) {
768        checkState();
769        synchronized (preferenceChangeListeners) {
770            int pos;
771            if ((pos = preferenceChangeListeners.indexOf(pcl)) == -1) {
772                throw new IllegalArgumentException();
773            }
774            preferenceChangeListeners.remove(pos);
775        }
776    }
777
778    @Override
779    public void sync() throws BackingStoreException {
780        synchronized (lock) {
781            checkState();
782            syncSpi();
783        }
784        for (AbstractPreferences child : cachedChildren()) {
785            child.sync();
786        }
787    }
788
789    @Override
790    public String toString() {
791        return (isUserNode() ? "User" : "System") + " Preference Node: " + absolutePath();
792    }
793
794    private void notifyChildAdded(Preferences child) {
795        NodeChangeEvent nce = new NodeAddEvent(this, child);
796        synchronized (events) {
797            events.add(nce);
798            events.notifyAll();
799        }
800    }
801
802    private void notifyChildRemoved(Preferences child) {
803        NodeChangeEvent nce = new NodeRemoveEvent(this, child);
804        synchronized (events) {
805            events.add(nce);
806            events.notifyAll();
807        }
808    }
809
810    private void notifyPreferenceChange(String key, String newValue) {
811        PreferenceChangeEvent pce = new PreferenceChangeEvent(this, key, newValue);
812        synchronized (events) {
813            events.add(pce);
814            events.notifyAll();
815        }
816    }
817
818    private static class EventDispatcher extends Thread {
819        EventDispatcher(String name){
820            super(name);
821        }
822
823        @Override
824        public void run() {
825            while (true) {
826                EventObject event;
827                try {
828                    event = getEventObject();
829                } catch (InterruptedException e) {
830                    e.printStackTrace();
831                    continue;
832                }
833                AbstractPreferences pref = (AbstractPreferences) event.getSource();
834                if (event instanceof NodeAddEvent) {
835                    dispatchNodeAdd((NodeChangeEvent) event,
836                            pref.nodeChangeListeners);
837                } else if (event instanceof NodeRemoveEvent) {
838                    dispatchNodeRemove((NodeChangeEvent) event,
839                            pref.nodeChangeListeners);
840                } else if (event instanceof PreferenceChangeEvent) {
841                    dispatchPrefChange((PreferenceChangeEvent) event,
842                            pref.preferenceChangeListeners);
843                }
844            }
845        }
846
847        private EventObject getEventObject() throws InterruptedException {
848            synchronized (events) {
849                if (events.isEmpty()) {
850                    events.wait();
851                }
852                EventObject event = events.get(0);
853                events.remove(0);
854                return event;
855            }
856        }
857
858        private void dispatchPrefChange(PreferenceChangeEvent event,
859                List<EventListener> preferenceChangeListeners) {
860            synchronized (preferenceChangeListeners) {
861                for (EventListener preferenceChangeListener : preferenceChangeListeners) {
862                    ((PreferenceChangeListener) preferenceChangeListener).preferenceChange(event);
863                }
864            }
865        }
866
867        private void dispatchNodeRemove(NodeChangeEvent event,
868                List<EventListener> nodeChangeListeners) {
869            synchronized (nodeChangeListeners) {
870                for (EventListener nodeChangeListener : nodeChangeListeners) {
871                    ((NodeChangeListener) nodeChangeListener).childRemoved(event);
872                }
873            }
874        }
875
876        private void dispatchNodeAdd(NodeChangeEvent event,
877                List<EventListener> nodeChangeListeners) {
878            synchronized (nodeChangeListeners) {
879                for (EventListener nodeChangeListener : nodeChangeListeners) {
880                    NodeChangeListener ncl = (NodeChangeListener) nodeChangeListener;
881                    ncl.childAdded(event);
882                }
883            }
884        }
885    }
886
887    private static class NodeAddEvent extends NodeChangeEvent {
888        //The base class is NOT serializable, so this class isn't either.
889        private static final long serialVersionUID = 1L;
890
891        public NodeAddEvent(Preferences p, Preferences c) {
892            super(p, c);
893        }
894    }
895
896    private static class NodeRemoveEvent extends NodeChangeEvent {
897        //The base class is NOT serializable, so this class isn't either.
898        private static final long serialVersionUID = 1L;
899
900        public NodeRemoveEvent(Preferences p, Preferences c) {
901            super(p, c);
902        }
903    }
904}
905