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.Charsets; 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(Charsets.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