001 /* AbstractPreferences -- Partial implementation of a Preference node 002 Copyright (C) 2001, 2003, 2004, 2006 Free Software Foundation, Inc. 003 004 This file is part of GNU Classpath. 005 006 GNU Classpath is free software; you can redistribute it and/or modify 007 it under the terms of the GNU General Public License as published by 008 the Free Software Foundation; either version 2, or (at your option) 009 any later version. 010 011 GNU Classpath is distributed in the hope that it will be useful, but 012 WITHOUT ANY WARRANTY; without even the implied warranty of 013 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 014 General Public License for more details. 015 016 You should have received a copy of the GNU General Public License 017 along with GNU Classpath; see the file COPYING. If not, write to the 018 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 019 02110-1301 USA. 020 021 Linking this library statically or dynamically with other modules is 022 making a combined work based on this library. Thus, the terms and 023 conditions of the GNU General Public License cover the whole 024 combination. 025 026 As a special exception, the copyright holders of this library give you 027 permission to link this library with independent modules to produce an 028 executable, regardless of the license terms of these independent 029 modules, and to copy and distribute the resulting executable under 030 terms of your choice, provided that you also meet, for each linked 031 independent module, the terms and conditions of the license of that 032 module. An independent module is a module which is not derived from 033 or based on this library. If you modify this library, you may extend 034 this exception to your version of the library, but you are not 035 obligated to do so. If you do not wish to do so, delete this 036 exception statement from your version. */ 037 038 039 package java.util.prefs; 040 041 import gnu.java.util.prefs.EventDispatcher; 042 import gnu.java.util.prefs.NodeWriter; 043 044 import java.io.ByteArrayOutputStream; 045 import java.io.IOException; 046 import java.io.OutputStream; 047 import java.util.ArrayList; 048 import java.util.Collection; 049 import java.util.HashMap; 050 import java.util.Iterator; 051 import java.util.TreeSet; 052 053 /** 054 * Partial implementation of a Preference node. 055 * 056 * @since 1.4 057 * @author Mark Wielaard (mark@klomp.org) 058 */ 059 public abstract class AbstractPreferences extends Preferences { 060 061 // protected fields 062 063 /** 064 * Object used to lock this preference node. Any thread only locks nodes 065 * downwards when it has the lock on the current node. No method should 066 * synchronize on the lock of any of its parent nodes while holding the 067 * lock on the current node. 068 */ 069 protected final Object lock = new Object(); 070 071 /** 072 * Set to true in the contructor if the node did not exist in the backing 073 * store when this preference node object was created. Should be set in 074 * the constructor of a subclass. Defaults to false. Used to fire node 075 * changed events. 076 */ 077 protected boolean newNode = false; 078 079 // private fields 080 081 /** 082 * The parent preferences node or null when this is the root node. 083 */ 084 private final AbstractPreferences parent; 085 086 /** 087 * The name of this node. 088 * Only when this is a root node (parent == null) the name is empty. 089 * It has a maximum of 80 characters and cannot contain any '/' characters. 090 */ 091 private final String name; 092 093 /** True when this node has been remove, false otherwise. */ 094 private boolean removed = false; 095 096 /** 097 * Holds all the child names and nodes of this node that have been 098 * accessed by earlier <code>getChild()</code> or <code>childSpi()</code> 099 * invocations and that have not been removed. 100 */ 101 private HashMap<String, AbstractPreferences> childCache 102 = new HashMap<String, AbstractPreferences>(); 103 104 /** 105 * A list of all the registered NodeChangeListener objects. 106 */ 107 private ArrayList<NodeChangeListener> nodeListeners; 108 109 /** 110 * A list of all the registered PreferenceChangeListener objects. 111 */ 112 private ArrayList<PreferenceChangeListener> preferenceListeners; 113 114 // constructor 115 116 /** 117 * Creates a new AbstractPreferences node with the given parent and name. 118 * 119 * @param parent the parent of this node or null when this is the root node 120 * @param name the name of this node, can not be null, only 80 characters 121 * maximum, must be empty when parent is null and cannot 122 * contain any '/' characters 123 * @exception IllegalArgumentException when name is null, greater then 80 124 * characters, not the empty string but parent is null or 125 * contains a '/' character 126 */ 127 protected AbstractPreferences(AbstractPreferences parent, String name) { 128 if ( (name == null) // name should be given 129 || (name.length() > MAX_NAME_LENGTH) // 80 characters max 130 || (parent == null && name.length() != 0) // root has no name 131 || (parent != null && name.length() == 0) // all other nodes do 132 || (name.indexOf('/') != -1)) // must not contain '/' 133 throw new IllegalArgumentException("Illegal name argument '" 134 + name 135 + "' (parent is " 136 + (parent == null ? "" : "not ") 137 + "null)"); 138 this.parent = parent; 139 this.name = name; 140 } 141 142 // identification methods 143 144 /** 145 * Returns the absolute path name of this preference node. 146 * The absolute path name of a node is the path name of its parent node 147 * plus a '/' plus its own name. If the node is the root node and has no 148 * parent then its path name is "" and its absolute path name is "/". 149 */ 150 public String absolutePath() { 151 if (parent == null) 152 return "/"; 153 else 154 return parent.path() + '/' + name; 155 } 156 157 /** 158 * Private helper method for absolutePath. Returns the empty string for a 159 * root node and otherwise the parentPath of its parent plus a '/'. 160 */ 161 private String path() { 162 if (parent == null) 163 return ""; 164 else 165 return parent.path() + '/' + name; 166 } 167 168 /** 169 * Returns true if this node comes from the user preferences tree, false 170 * if it comes from the system preferences tree. 171 */ 172 public boolean isUserNode() { 173 AbstractPreferences root = this; 174 while (root.parent != null) 175 root = root.parent; 176 return root == Preferences.userRoot(); 177 } 178 179 /** 180 * Returns the name of this preferences node. The name of the node cannot 181 * be null, can be mostly 80 characters and cannot contain any '/' 182 * characters. The root node has as name "". 183 */ 184 public String name() { 185 return name; 186 } 187 188 /** 189 * Returns the String given by 190 * <code> 191 * (isUserNode() ? "User":"System") + " Preference Node: " + absolutePath() 192 * </code> 193 */ 194 public String toString() { 195 return (isUserNode() ? "User":"System") 196 + " Preference Node: " 197 + absolutePath(); 198 } 199 200 /** 201 * Returns all known unremoved children of this node. 202 * 203 * @return All known unremoved children of this node 204 */ 205 protected final AbstractPreferences[] cachedChildren() 206 { 207 Collection<AbstractPreferences> vals = childCache.values(); 208 return vals.toArray(new AbstractPreferences[vals.size()]); 209 } 210 211 /** 212 * Returns all the direct sub nodes of this preferences node. 213 * Needs access to the backing store to give a meaningfull answer. 214 * <p> 215 * This implementation locks this node, checks if the node has not yet 216 * been removed and throws an <code>IllegalStateException</code> when it 217 * has been. Then it creates a new <code>TreeSet</code> and adds any 218 * already cached child nodes names. To get any uncached names it calls 219 * <code>childrenNamesSpi()</code> and adds the result to the set. Finally 220 * it calls <code>toArray()</code> on the created set. When the call to 221 * <code>childrenNamesSpi</code> thows an <code>BackingStoreException</code> 222 * this method will not catch that exception but propagate the exception 223 * to the caller. 224 * 225 * @exception BackingStoreException when the backing store cannot be 226 * reached 227 * @exception IllegalStateException when this node has been removed 228 */ 229 public String[] childrenNames() throws BackingStoreException { 230 synchronized(lock) { 231 if (isRemoved()) 232 throw new IllegalStateException("Node removed"); 233 234 TreeSet<String> childrenNames = new TreeSet<String>(); 235 236 // First get all cached node names 237 childrenNames.addAll(childCache.keySet()); 238 239 // Then add any others 240 String names[] = childrenNamesSpi(); 241 for (int i = 0; i < names.length; i++) { 242 childrenNames.add(names[i]); 243 } 244 245 // And return the array of names 246 String[] children = new String[childrenNames.size()]; 247 childrenNames.toArray(children); 248 return children; 249 250 } 251 } 252 253 /** 254 * Returns a sub node of this preferences node if the given path is 255 * relative (does not start with a '/') or a sub node of the root 256 * if the path is absolute (does start with a '/'). 257 * <p> 258 * This method first locks this node and checks if the node has not been 259 * removed, if it has been removed it throws an exception. Then if the 260 * path is relative (does not start with a '/') it checks if the path is 261 * legal (does not end with a '/' and has no consecutive '/' characters). 262 * Then it recursively gets a name from the path, gets the child node 263 * from the child-cache of this node or calls the <code>childSpi()</code> 264 * method to create a new child sub node. This is done recursively on the 265 * newly created sub node with the rest of the path till the path is empty. 266 * If the path is absolute (starts with a '/') the lock on this node is 267 * droped and this method is called on the root of the preferences tree 268 * with as argument the complete path minus the first '/'. 269 * 270 * @exception IllegalStateException if this node has been removed 271 * @exception IllegalArgumentException if the path contains two or more 272 * consecutive '/' characters, ends with a '/' charactor and is not the 273 * string "/" (indicating the root node) or any name on the path is more 274 * than 80 characters long 275 */ 276 public Preferences node(String path) { 277 synchronized(lock) { 278 if (isRemoved()) 279 throw new IllegalStateException("Node removed"); 280 281 // Is it a relative path? 282 if (!path.startsWith("/")) { 283 284 // Check if it is a valid path 285 if (path.indexOf("//") != -1 || path.endsWith("/")) 286 throw new IllegalArgumentException(path); 287 288 return getNode(path); 289 } 290 } 291 292 // path started with a '/' so it is absolute 293 // we drop the lock and start from the root (omitting the first '/') 294 Preferences root = isUserNode() ? userRoot() : systemRoot(); 295 return root.node(path.substring(1)); 296 297 } 298 299 /** 300 * Private helper method for <code>node()</code>. Called with this node 301 * locked. Returns this node when path is the empty string, if it is not 302 * empty the next node name is taken from the path (all chars till the 303 * next '/' or end of path string) and the node is either taken from the 304 * child-cache of this node or the <code>childSpi()</code> method is called 305 * on this node with the name as argument. Then this method is called 306 * recursively on the just constructed child node with the rest of the 307 * path. 308 * 309 * @param path should not end with a '/' character and should not contain 310 * consecutive '/' characters 311 * @exception IllegalArgumentException if path begins with a name that is 312 * larger then 80 characters. 313 */ 314 private Preferences getNode(String path) { 315 // if mark is dom then goto end 316 317 // Empty String "" indicates this node 318 if (path.length() == 0) 319 return this; 320 321 // Calculate child name and rest of path 322 String childName; 323 String childPath; 324 int nextSlash = path.indexOf('/'); 325 if (nextSlash == -1) { 326 childName = path; 327 childPath = ""; 328 } else { 329 childName = path.substring(0, nextSlash); 330 childPath = path.substring(nextSlash+1); 331 } 332 333 // Get the child node 334 AbstractPreferences child; 335 child = (AbstractPreferences)childCache.get(childName); 336 if (child == null) { 337 338 if (childName.length() > MAX_NAME_LENGTH) 339 throw new IllegalArgumentException(childName); 340 341 // Not in childCache yet so create a new sub node 342 child = childSpi(childName); 343 childCache.put(childName, child); 344 if (child.newNode && nodeListeners != null) 345 fire(new NodeChangeEvent(this, child), true); 346 } 347 348 // Lock the child and go down 349 synchronized(child.lock) { 350 return child.getNode(childPath); 351 } 352 } 353 354 /** 355 * Returns true if the node that the path points to exists in memory or 356 * in the backing store. Otherwise it returns false or an exception is 357 * thrown. When this node is removed the only valid parameter is the 358 * empty string (indicating this node), the return value in that case 359 * will be false. 360 * 361 * @exception BackingStoreException when the backing store cannot be 362 * reached 363 * @exception IllegalStateException if this node has been removed 364 * and the path is not the empty string (indicating this node) 365 * @exception IllegalArgumentException if the path contains two or more 366 * consecutive '/' characters, ends with a '/' charactor and is not the 367 * string "/" (indicating the root node) or any name on the path is more 368 * then 80 characters long 369 */ 370 public boolean nodeExists(String path) throws BackingStoreException { 371 synchronized(lock) { 372 if (isRemoved() && path.length() != 0) 373 throw new IllegalStateException("Node removed"); 374 375 // Is it a relative path? 376 if (!path.startsWith("/")) { 377 378 // Check if it is a valid path 379 if (path.indexOf("//") != -1 || path.endsWith("/")) 380 throw new IllegalArgumentException(path); 381 382 return existsNode(path); 383 } 384 } 385 386 // path started with a '/' so it is absolute 387 // we drop the lock and start from the root (omitting the first '/') 388 Preferences root = isUserNode() ? userRoot() : systemRoot(); 389 return root.nodeExists(path.substring(1)); 390 391 } 392 393 private boolean existsNode(String path) throws BackingStoreException { 394 395 // Empty String "" indicates this node 396 if (path.length() == 0) 397 return(!isRemoved()); 398 399 // Calculate child name and rest of path 400 String childName; 401 String childPath; 402 int nextSlash = path.indexOf('/'); 403 if (nextSlash == -1) { 404 childName = path; 405 childPath = ""; 406 } else { 407 childName = path.substring(0, nextSlash); 408 childPath = path.substring(nextSlash+1); 409 } 410 411 // Get the child node 412 AbstractPreferences child; 413 child = (AbstractPreferences)childCache.get(childName); 414 if (child == null) { 415 416 if (childName.length() > MAX_NAME_LENGTH) 417 throw new IllegalArgumentException(childName); 418 419 // Not in childCache yet so create a new sub node 420 child = getChild(childName); 421 422 if (child == null) 423 return false; 424 425 childCache.put(childName, child); 426 } 427 428 // Lock the child and go down 429 synchronized(child.lock) { 430 return child.existsNode(childPath); 431 } 432 } 433 434 /** 435 * Returns the child sub node if it exists in the backing store or null 436 * if it does not exist. Called (indirectly) by <code>nodeExists()</code> 437 * when a child node name can not be found in the cache. 438 * <p> 439 * Gets the lock on this node, calls <code>childrenNamesSpi()</code> to 440 * get an array of all (possibly uncached) children and compares the 441 * given name with the names in the array. If the name is found in the 442 * array <code>childSpi()</code> is called to get an instance, otherwise 443 * null is returned. 444 * 445 * @exception BackingStoreException when the backing store cannot be 446 * reached 447 */ 448 protected AbstractPreferences getChild(String name) 449 throws BackingStoreException 450 { 451 synchronized(lock) { 452 // Get all the names (not yet in the cache) 453 String[] names = childrenNamesSpi(); 454 for (int i=0; i < names.length; i++) 455 if (name.equals(names[i])) 456 return childSpi(name); 457 458 // No child with that name found 459 return null; 460 } 461 } 462 463 /** 464 * Returns true if this node has been removed with the 465 * <code>removeNode()</code> method, false otherwise. 466 * <p> 467 * Gets the lock on this node and then returns a boolean field set by 468 * <code>removeNode</code> methods. 469 */ 470 protected boolean isRemoved() { 471 synchronized(lock) { 472 return removed; 473 } 474 } 475 476 /** 477 * Returns the parent preferences node of this node or null if this is 478 * the root of the preferences tree. 479 * <p> 480 * Gets the lock on this node, checks that the node has not been removed 481 * and returns the parent given to the constructor. 482 * 483 * @exception IllegalStateException if this node has been removed 484 */ 485 public Preferences parent() { 486 synchronized(lock) { 487 if (isRemoved()) 488 throw new IllegalStateException("Node removed"); 489 490 return parent; 491 } 492 } 493 494 // export methods 495 496 // Inherit javadoc. 497 public void exportNode(OutputStream os) 498 throws BackingStoreException, 499 IOException 500 { 501 NodeWriter nodeWriter = new NodeWriter(this, os); 502 nodeWriter.writePrefs(); 503 } 504 505 // Inherit javadoc. 506 public void exportSubtree(OutputStream os) 507 throws BackingStoreException, 508 IOException 509 { 510 NodeWriter nodeWriter = new NodeWriter(this, os); 511 nodeWriter.writePrefsTree(); 512 } 513 514 // preference entry manipulation methods 515 516 /** 517 * Returns an (possibly empty) array with all the keys of the preference 518 * entries of this node. 519 * <p> 520 * This method locks this node and checks if the node has not been 521 * removed, if it has been removed it throws an exception, then it returns 522 * the result of calling <code>keysSpi()</code>. 523 * 524 * @exception BackingStoreException when the backing store cannot be 525 * reached 526 * @exception IllegalStateException if this node has been removed 527 */ 528 public String[] keys() throws BackingStoreException { 529 synchronized(lock) { 530 if (isRemoved()) 531 throw new IllegalStateException("Node removed"); 532 533 return keysSpi(); 534 } 535 } 536 537 538 /** 539 * Returns the value associated with the key in this preferences node. If 540 * the default value of the key cannot be found in the preferences node 541 * entries or something goes wrong with the backing store the supplied 542 * default value is returned. 543 * <p> 544 * Checks that key is not null and not larger then 80 characters, 545 * locks this node, and checks that the node has not been removed. 546 * Then it calls <code>keySpi()</code> and returns 547 * the result of that method or the given default value if it returned 548 * null or throwed an exception. 549 * 550 * @exception IllegalArgumentException if key is larger then 80 characters 551 * @exception IllegalStateException if this node has been removed 552 * @exception NullPointerException if key is null 553 */ 554 public String get(String key, String defaultVal) { 555 if (key.length() > MAX_KEY_LENGTH) 556 throw new IllegalArgumentException(key); 557 558 synchronized(lock) { 559 if (isRemoved()) 560 throw new IllegalStateException("Node removed"); 561 562 String value; 563 try { 564 value = getSpi(key); 565 } catch (ThreadDeath death) { 566 throw death; 567 } catch (Throwable t) { 568 value = null; 569 } 570 571 if (value != null) { 572 return value; 573 } else { 574 return defaultVal; 575 } 576 } 577 } 578 579 /** 580 * Convenience method for getting the given entry as a boolean. 581 * When the string representation of the requested entry is either 582 * "true" or "false" (ignoring case) then that value is returned, 583 * otherwise the given default boolean value is returned. 584 * 585 * @exception IllegalArgumentException if key is larger then 80 characters 586 * @exception IllegalStateException if this node has been removed 587 * @exception NullPointerException if key is null 588 */ 589 public boolean getBoolean(String key, boolean defaultVal) { 590 String value = get(key, null); 591 592 if ("true".equalsIgnoreCase(value)) 593 return true; 594 595 if ("false".equalsIgnoreCase(value)) 596 return false; 597 598 return defaultVal; 599 } 600 601 /** 602 * Convenience method for getting the given entry as a byte array. 603 * When the string representation of the requested entry is a valid 604 * Base64 encoded string (without any other characters, such as newlines) 605 * then the decoded Base64 string is returned as byte array, 606 * otherwise the given default byte array value is returned. 607 * 608 * @exception IllegalArgumentException if key is larger then 80 characters 609 * @exception IllegalStateException if this node has been removed 610 * @exception NullPointerException if key is null 611 */ 612 public byte[] getByteArray(String key, byte[] defaultVal) { 613 String value = get(key, null); 614 615 byte[] b = null; 616 if (value != null) { 617 b = decode64(value); 618 } 619 620 if (b != null) 621 return b; 622 else 623 return defaultVal; 624 } 625 626 /** 627 * Helper method for decoding a Base64 string as an byte array. 628 * Returns null on encoding error. This method does not allow any other 629 * characters present in the string then the 65 special base64 chars. 630 */ 631 private static byte[] decode64(String s) { 632 ByteArrayOutputStream bs = new ByteArrayOutputStream((s.length()/4)*3); 633 char[] c = new char[s.length()]; 634 s.getChars(0, s.length(), c, 0); 635 636 // Convert from base64 chars 637 int endchar = -1; 638 for(int j = 0; j < c.length && endchar == -1; j++) { 639 if (c[j] >= 'A' && c[j] <= 'Z') { 640 c[j] -= 'A'; 641 } else if (c[j] >= 'a' && c[j] <= 'z') { 642 c[j] = (char) (c[j] + 26 - 'a'); 643 } else if (c[j] >= '0' && c[j] <= '9') { 644 c[j] = (char) (c[j] + 52 - '0'); 645 } else if (c[j] == '+') { 646 c[j] = 62; 647 } else if (c[j] == '/') { 648 c[j] = 63; 649 } else if (c[j] == '=') { 650 endchar = j; 651 } else { 652 return null; // encoding exception 653 } 654 } 655 656 int remaining = endchar == -1 ? c.length : endchar; 657 int i = 0; 658 while (remaining > 0) { 659 // Four input chars (6 bits) are decoded as three bytes as 660 // 000000 001111 111122 222222 661 662 byte b0 = (byte) (c[i] << 2); 663 if (remaining >= 2) { 664 b0 += (c[i+1] & 0x30) >> 4; 665 } 666 bs.write(b0); 667 668 if (remaining >= 3) { 669 byte b1 = (byte) ((c[i+1] & 0x0F) << 4); 670 b1 += (byte) ((c[i+2] & 0x3C) >> 2); 671 bs.write(b1); 672 } 673 674 if (remaining >= 4) { 675 byte b2 = (byte) ((c[i+2] & 0x03) << 6); 676 b2 += c[i+3]; 677 bs.write(b2); 678 } 679 680 i += 4; 681 remaining -= 4; 682 } 683 684 return bs.toByteArray(); 685 } 686 687 /** 688 * Convenience method for getting the given entry as a double. 689 * When the string representation of the requested entry can be decoded 690 * with <code>Double.parseDouble()</code> then that double is returned, 691 * otherwise the given default double value is returned. 692 * 693 * @exception IllegalArgumentException if key is larger then 80 characters 694 * @exception IllegalStateException if this node has been removed 695 * @exception NullPointerException if key is null 696 */ 697 public double getDouble(String key, double defaultVal) { 698 String value = get(key, null); 699 700 if (value != null) { 701 try { 702 return Double.parseDouble(value); 703 } catch (NumberFormatException nfe) { /* ignore */ } 704 } 705 706 return defaultVal; 707 } 708 709 /** 710 * Convenience method for getting the given entry as a float. 711 * When the string representation of the requested entry can be decoded 712 * with <code>Float.parseFloat()</code> then that float is returned, 713 * otherwise the given default float value is returned. 714 * 715 * @exception IllegalArgumentException if key is larger then 80 characters 716 * @exception IllegalStateException if this node has been removed 717 * @exception NullPointerException if key is null 718 */ 719 public float getFloat(String key, float defaultVal) { 720 String value = get(key, null); 721 722 if (value != null) { 723 try { 724 return Float.parseFloat(value); 725 } catch (NumberFormatException nfe) { /* ignore */ } 726 } 727 728 return defaultVal; 729 } 730 731 /** 732 * Convenience method for getting the given entry as an integer. 733 * When the string representation of the requested entry can be decoded 734 * with <code>Integer.parseInt()</code> then that integer is returned, 735 * otherwise the given default integer value is returned. 736 * 737 * @exception IllegalArgumentException if key is larger then 80 characters 738 * @exception IllegalStateException if this node has been removed 739 * @exception NullPointerException if key is null 740 */ 741 public int getInt(String key, int defaultVal) { 742 String value = get(key, null); 743 744 if (value != null) { 745 try { 746 return Integer.parseInt(value); 747 } catch (NumberFormatException nfe) { /* ignore */ } 748 } 749 750 return defaultVal; 751 } 752 753 /** 754 * Convenience method for getting the given entry as a long. 755 * When the string representation of the requested entry can be decoded 756 * with <code>Long.parseLong()</code> then that long is returned, 757 * otherwise the given default long value is returned. 758 * 759 * @exception IllegalArgumentException if key is larger then 80 characters 760 * @exception IllegalStateException if this node has been removed 761 * @exception NullPointerException if key is null 762 */ 763 public long getLong(String key, long defaultVal) { 764 String value = get(key, null); 765 766 if (value != null) { 767 try { 768 return Long.parseLong(value); 769 } catch (NumberFormatException nfe) { /* ignore */ } 770 } 771 772 return defaultVal; 773 } 774 775 /** 776 * Sets the value of the given preferences entry for this node. 777 * Key and value cannot be null, the key cannot exceed 80 characters 778 * and the value cannot exceed 8192 characters. 779 * <p> 780 * The result will be immediately visible in this VM, but may not be 781 * immediately written to the backing store. 782 * <p> 783 * Checks that key and value are valid, locks this node, and checks that 784 * the node has not been removed. Then it calls <code>putSpi()</code>. 785 * 786 * @exception NullPointerException if either key or value are null 787 * @exception IllegalArgumentException if either key or value are to large 788 * @exception IllegalStateException when this node has been removed 789 */ 790 public void put(String key, String value) { 791 if (key.length() > MAX_KEY_LENGTH 792 || value.length() > MAX_VALUE_LENGTH) 793 throw new IllegalArgumentException("key (" 794 + key.length() + ")" 795 + " or value (" 796 + value.length() + ")" 797 + " to large"); 798 synchronized(lock) { 799 if (isRemoved()) 800 throw new IllegalStateException("Node removed"); 801 802 putSpi(key, value); 803 804 if (preferenceListeners != null) 805 fire(new PreferenceChangeEvent(this, key, value)); 806 } 807 808 } 809 810 /** 811 * Convenience method for setting the given entry as a boolean. 812 * The boolean is converted with <code>Boolean.toString(value)</code> 813 * and then stored in the preference entry as that string. 814 * 815 * @exception NullPointerException if key is null 816 * @exception IllegalArgumentException if the key length is to large 817 * @exception IllegalStateException when this node has been removed 818 */ 819 public void putBoolean(String key, boolean value) { 820 put(key, Boolean.toString(value)); 821 } 822 823 /** 824 * Convenience method for setting the given entry as an array of bytes. 825 * The byte array is converted to a Base64 encoded string 826 * and then stored in the preference entry as that string. 827 * <p> 828 * Note that a byte array encoded as a Base64 string will be about 1.3 829 * times larger then the original length of the byte array, which means 830 * that the byte array may not be larger about 6 KB. 831 * 832 * @exception NullPointerException if either key or value are null 833 * @exception IllegalArgumentException if either key or value are to large 834 * @exception IllegalStateException when this node has been removed 835 */ 836 public void putByteArray(String key, byte[] value) { 837 put(key, encode64(value)); 838 } 839 840 /** 841 * Helper method for encoding an array of bytes as a Base64 String. 842 */ 843 private static String encode64(byte[] b) { 844 StringBuffer sb = new StringBuffer((b.length/3)*4); 845 846 int i = 0; 847 int remaining = b.length; 848 char c[] = new char[4]; 849 while (remaining > 0) { 850 // Three input bytes are encoded as four chars (6 bits) as 851 // 00000011 11112222 22333333 852 853 c[0] = (char) ((b[i] & 0xFC) >> 2); 854 c[1] = (char) ((b[i] & 0x03) << 4); 855 if (remaining >= 2) { 856 c[1] += (char) ((b[i+1] & 0xF0) >> 4); 857 c[2] = (char) ((b[i+1] & 0x0F) << 2); 858 if (remaining >= 3) { 859 c[2] += (char) ((b[i+2] & 0xC0) >> 6); 860 c[3] = (char) (b[i+2] & 0x3F); 861 } else { 862 c[3] = 64; 863 } 864 } else { 865 c[2] = 64; 866 c[3] = 64; 867 } 868 869 // Convert to base64 chars 870 for(int j = 0; j < 4; j++) { 871 if (c[j] < 26) { 872 c[j] += 'A'; 873 } else if (c[j] < 52) { 874 c[j] = (char) (c[j] - 26 + 'a'); 875 } else if (c[j] < 62) { 876 c[j] = (char) (c[j] - 52 + '0'); 877 } else if (c[j] == 62) { 878 c[j] = '+'; 879 } else if (c[j] == 63) { 880 c[j] = '/'; 881 } else { 882 c[j] = '='; 883 } 884 } 885 886 sb.append(c); 887 i += 3; 888 remaining -= 3; 889 } 890 891 return sb.toString(); 892 } 893 894 /** 895 * Convenience method for setting the given entry as a double. 896 * The double is converted with <code>Double.toString(double)</code> 897 * and then stored in the preference entry as that string. 898 * 899 * @exception NullPointerException if the key is null 900 * @exception IllegalArgumentException if the key length is to large 901 * @exception IllegalStateException when this node has been removed 902 */ 903 public void putDouble(String key, double value) { 904 put(key, Double.toString(value)); 905 } 906 907 /** 908 * Convenience method for setting the given entry as a float. 909 * The float is converted with <code>Float.toString(float)</code> 910 * and then stored in the preference entry as that string. 911 * 912 * @exception NullPointerException if the key is null 913 * @exception IllegalArgumentException if the key length is to large 914 * @exception IllegalStateException when this node has been removed 915 */ 916 public void putFloat(String key, float value) { 917 put(key, Float.toString(value)); 918 } 919 920 /** 921 * Convenience method for setting the given entry as an integer. 922 * The integer is converted with <code>Integer.toString(int)</code> 923 * and then stored in the preference entry as that string. 924 * 925 * @exception NullPointerException if the key is null 926 * @exception IllegalArgumentException if the key length is to large 927 * @exception IllegalStateException when this node has been removed 928 */ 929 public void putInt(String key, int value) { 930 put(key, Integer.toString(value)); 931 } 932 933 /** 934 * Convenience method for setting the given entry as a long. 935 * The long is converted with <code>Long.toString(long)</code> 936 * and then stored in the preference entry as that string. 937 * 938 * @exception NullPointerException if the key is null 939 * @exception IllegalArgumentException if the key length is to large 940 * @exception IllegalStateException when this node has been removed 941 */ 942 public void putLong(String key, long value) { 943 put(key, Long.toString(value)); 944 } 945 946 /** 947 * Removes the preferences entry from this preferences node. 948 * <p> 949 * The result will be immediately visible in this VM, but may not be 950 * immediately written to the backing store. 951 * <p> 952 * This implementation checks that the key is not larger then 80 953 * characters, gets the lock of this node, checks that the node has 954 * not been removed and calls <code>removeSpi</code> with the given key. 955 * 956 * @exception NullPointerException if the key is null 957 * @exception IllegalArgumentException if the key length is to large 958 * @exception IllegalStateException when this node has been removed 959 */ 960 public void remove(String key) { 961 if (key.length() > MAX_KEY_LENGTH) 962 throw new IllegalArgumentException(key); 963 964 synchronized(lock) { 965 if (isRemoved()) 966 throw new IllegalStateException("Node removed"); 967 968 removeSpi(key); 969 970 if (preferenceListeners != null) 971 fire(new PreferenceChangeEvent(this, key, null)); 972 } 973 } 974 975 /** 976 * Removes all entries from this preferences node. May need access to the 977 * backing store to get and clear all entries. 978 * <p> 979 * The result will be immediately visible in this VM, but may not be 980 * immediatly written to the backing store. 981 * <p> 982 * This implementation locks this node, checks that the node has not been 983 * removed and calls <code>keys()</code> to get a complete array of keys 984 * for this node. For every key found <code>removeSpi()</code> is called. 985 * 986 * @exception BackingStoreException when the backing store cannot be 987 * reached 988 * @exception IllegalStateException if this node has been removed 989 */ 990 public void clear() throws BackingStoreException { 991 synchronized(lock) { 992 if (isRemoved()) 993 throw new IllegalStateException("Node Removed"); 994 995 String[] keys = keys(); 996 for (int i = 0; i < keys.length; i++) { 997 removeSpi(keys[i]); 998 } 999 } 1000 } 1001 1002 /** 1003 * Writes all preference changes on this and any subnode that have not 1004 * yet been written to the backing store. This has no effect on the 1005 * preference entries in this VM, but it makes sure that all changes 1006 * are visible to other programs (other VMs might need to call the 1007 * <code>sync()</code> method to actually see the changes to the backing 1008 * store. 1009 * <p> 1010 * Locks this node, calls the <code>flushSpi()</code> method, gets all 1011 * the (cached - already existing in this VM) subnodes and then calls 1012 * <code>flushSpi()</code> on every subnode with this node unlocked and 1013 * only that particular subnode locked. 1014 * 1015 * @exception BackingStoreException when the backing store cannot be 1016 * reached 1017 */ 1018 public void flush() throws BackingStoreException { 1019 flushNode(false); 1020 } 1021 1022 /** 1023 * Writes and reads all preference changes to and from this and any 1024 * subnodes. This makes sure that all local changes are written to the 1025 * backing store and that all changes to the backing store are visible 1026 * in this preference node (and all subnodes). 1027 * <p> 1028 * Checks that this node is not removed, locks this node, calls the 1029 * <code>syncSpi()</code> method, gets all the subnodes and then calls 1030 * <code>syncSpi()</code> on every subnode with this node unlocked and 1031 * only that particular subnode locked. 1032 * 1033 * @exception BackingStoreException when the backing store cannot be 1034 * reached 1035 * @exception IllegalStateException if this node has been removed 1036 */ 1037 public void sync() throws BackingStoreException { 1038 flushNode(true); 1039 } 1040 1041 1042 /** 1043 * Private helper method that locks this node and calls either 1044 * <code>flushSpi()</code> if <code>sync</code> is false, or 1045 * <code>flushSpi()</code> if <code>sync</code> is true. Then it gets all 1046 * the currently cached subnodes. For every subnode it calls this method 1047 * recursively with this node no longer locked. 1048 * <p> 1049 * Called by either <code>flush()</code> or <code>sync()</code> 1050 */ 1051 private void flushNode(boolean sync) throws BackingStoreException { 1052 String[] keys = null; 1053 synchronized(lock) { 1054 if (sync) { 1055 syncSpi(); 1056 } else { 1057 flushSpi(); 1058 } 1059 keys = (String[]) childCache.keySet().toArray(new String[]{}); 1060 } 1061 1062 if (keys != null) { 1063 for (int i = 0; i < keys.length; i++) { 1064 // Have to lock this node again to access the childCache 1065 AbstractPreferences subNode; 1066 synchronized(lock) { 1067 subNode = (AbstractPreferences) childCache.get(keys[i]); 1068 } 1069 1070 // The child could already have been removed from the cache 1071 if (subNode != null) { 1072 subNode.flushNode(sync); 1073 } 1074 } 1075 } 1076 } 1077 1078 /** 1079 * Removes this and all subnodes from the backing store and clears all 1080 * entries. After removal this instance will not be useable (except for 1081 * a few methods that don't throw a <code>InvalidStateException</code>), 1082 * even when a new node with the same path name is created this instance 1083 * will not be usable again. 1084 * <p> 1085 * Checks that this is not a root node. If not it locks the parent node, 1086 * then locks this node and checks that the node has not yet been removed. 1087 * Then it makes sure that all subnodes of this node are in the child cache, 1088 * by calling <code>childSpi()</code> on any children not yet in the cache. 1089 * Then for all children it locks the subnode and removes it. After all 1090 * subnodes have been purged the child cache is cleared, this nodes removed 1091 * flag is set and any listeners are called. Finally this node is removed 1092 * from the child cache of the parent node. 1093 * 1094 * @exception BackingStoreException when the backing store cannot be 1095 * reached 1096 * @exception IllegalStateException if this node has already been removed 1097 * @exception UnsupportedOperationException if this is a root node 1098 */ 1099 public void removeNode() throws BackingStoreException { 1100 // Check if it is a root node 1101 if (parent == null) 1102 throw new UnsupportedOperationException("Cannot remove root node"); 1103 1104 synchronized (parent.lock) { 1105 synchronized(this.lock) { 1106 if (isRemoved()) 1107 throw new IllegalStateException("Node Removed"); 1108 1109 purge(); 1110 } 1111 parent.childCache.remove(name); 1112 } 1113 } 1114 1115 /** 1116 * Private helper method used to completely remove this node. 1117 * Called by <code>removeNode</code> with the parent node and this node 1118 * locked. 1119 * <p> 1120 * Makes sure that all subnodes of this node are in the child cache, 1121 * by calling <code>childSpi()</code> on any children not yet in the 1122 * cache. Then for all children it locks the subnode and calls this method 1123 * on that node. After all subnodes have been purged the child cache is 1124 * cleared, this nodes removed flag is set and any listeners are called. 1125 */ 1126 private void purge() throws BackingStoreException 1127 { 1128 // Make sure all children have an AbstractPreferences node in cache 1129 String children[] = childrenNamesSpi(); 1130 for (int i = 0; i < children.length; i++) { 1131 if (childCache.get(children[i]) == null) 1132 childCache.put(children[i], childSpi(children[i])); 1133 } 1134 1135 // purge all children 1136 Iterator i = childCache.values().iterator(); 1137 while (i.hasNext()) { 1138 AbstractPreferences node = (AbstractPreferences) i.next(); 1139 synchronized(node.lock) { 1140 node.purge(); 1141 } 1142 } 1143 1144 // Cache is empty now 1145 childCache.clear(); 1146 1147 // remove this node 1148 removeNodeSpi(); 1149 removed = true; 1150 1151 if (nodeListeners != null) 1152 fire(new NodeChangeEvent(parent, this), false); 1153 } 1154 1155 // listener methods 1156 1157 /** 1158 * Add a listener which is notified when a sub-node of this node 1159 * is added or removed. 1160 * @param listener the listener to add 1161 */ 1162 public void addNodeChangeListener(NodeChangeListener listener) 1163 { 1164 synchronized (lock) 1165 { 1166 if (isRemoved()) 1167 throw new IllegalStateException("node has been removed"); 1168 if (listener == null) 1169 throw new NullPointerException("listener is null"); 1170 if (nodeListeners == null) 1171 nodeListeners = new ArrayList<NodeChangeListener>(); 1172 nodeListeners.add(listener); 1173 } 1174 } 1175 1176 /** 1177 * Add a listener which is notified when a value in this node 1178 * is added, changed, or removed. 1179 * @param listener the listener to add 1180 */ 1181 public void addPreferenceChangeListener(PreferenceChangeListener listener) 1182 { 1183 synchronized (lock) 1184 { 1185 if (isRemoved()) 1186 throw new IllegalStateException("node has been removed"); 1187 if (listener == null) 1188 throw new NullPointerException("listener is null"); 1189 if (preferenceListeners == null) 1190 preferenceListeners = new ArrayList<PreferenceChangeListener>(); 1191 preferenceListeners.add(listener); 1192 } 1193 } 1194 1195 /** 1196 * Remove the indicated node change listener from the list of 1197 * listeners to notify. 1198 * @param listener the listener to remove 1199 */ 1200 public void removeNodeChangeListener(NodeChangeListener listener) 1201 { 1202 synchronized (lock) 1203 { 1204 if (isRemoved()) 1205 throw new IllegalStateException("node has been removed"); 1206 if (listener == null) 1207 throw new NullPointerException("listener is null"); 1208 if (nodeListeners != null) 1209 nodeListeners.remove(listener); 1210 } 1211 } 1212 1213 /** 1214 * Remove the indicated preference change listener from the list of 1215 * listeners to notify. 1216 * @param listener the listener to remove 1217 */ 1218 public void removePreferenceChangeListener (PreferenceChangeListener listener) 1219 { 1220 synchronized (lock) 1221 { 1222 if (isRemoved()) 1223 throw new IllegalStateException("node has been removed"); 1224 if (listener == null) 1225 throw new NullPointerException("listener is null"); 1226 if (preferenceListeners != null) 1227 preferenceListeners.remove(listener); 1228 } 1229 } 1230 1231 /** 1232 * Send a preference change event to all listeners. Note that 1233 * the caller is responsible for holding the node's lock, and 1234 * for checking that the list of listeners is not null. 1235 * @param event the event to send 1236 */ 1237 private void fire(final PreferenceChangeEvent event) 1238 { 1239 Iterator it = preferenceListeners.iterator(); 1240 while (it.hasNext()) 1241 { 1242 final PreferenceChangeListener l = (PreferenceChangeListener) it.next(); 1243 EventDispatcher.dispatch(new Runnable() 1244 { 1245 public void run() 1246 { 1247 l.preferenceChange(event); 1248 } 1249 }); 1250 } 1251 } 1252 1253 /** 1254 * Send a node change event to all listeners. Note that 1255 * the caller is responsible for holding the node's lock, and 1256 * for checking that the list of listeners is not null. 1257 * @param event the event to send 1258 */ 1259 private void fire(final NodeChangeEvent event, final boolean added) 1260 { 1261 Iterator it = nodeListeners.iterator(); 1262 while (it.hasNext()) 1263 { 1264 final NodeChangeListener l = (NodeChangeListener) it.next(); 1265 EventDispatcher.dispatch(new Runnable() 1266 { 1267 public void run() 1268 { 1269 if (added) 1270 l.childAdded(event); 1271 else 1272 l.childRemoved(event); 1273 } 1274 }); 1275 } 1276 } 1277 1278 // abstract spi methods 1279 1280 /** 1281 * Returns the names of the sub nodes of this preference node. 1282 * This method only has to return any not yet cached child names, 1283 * but may return all names if that is easier. It must not return 1284 * null when there are no children, it has to return an empty array 1285 * in that case. Since this method must consult the backing store to 1286 * get all the sub node names it may throw a BackingStoreException. 1287 * <p> 1288 * Called by <code>childrenNames()</code> with this node locked. 1289 */ 1290 protected abstract String[] childrenNamesSpi() throws BackingStoreException; 1291 1292 /** 1293 * Returns a child note with the given name. 1294 * This method is called by the <code>node()</code> method (indirectly 1295 * through the <code>getNode()</code> helper method) with this node locked 1296 * if a sub node with this name does not already exist in the child cache. 1297 * If the child node did not aleady exist in the backing store the boolean 1298 * field <code>newNode</code> of the returned node should be set. 1299 * <p> 1300 * Note that this method should even return a non-null child node if the 1301 * backing store is not available since it may not throw a 1302 * <code>BackingStoreException</code>. 1303 */ 1304 protected abstract AbstractPreferences childSpi(String name); 1305 1306 /** 1307 * Returns an (possibly empty) array with all the keys of the preference 1308 * entries of this node. 1309 * <p> 1310 * Called by <code>keys()</code> with this node locked if this node has 1311 * not been removed. May throw an exception when the backing store cannot 1312 * be accessed. 1313 * 1314 * @exception BackingStoreException when the backing store cannot be 1315 * reached 1316 */ 1317 protected abstract String[] keysSpi() throws BackingStoreException; 1318 1319 /** 1320 * Returns the value associated with the key in this preferences node or 1321 * null when the key does not exist in this preferences node. 1322 * <p> 1323 * Called by <code>key()</code> with this node locked after checking that 1324 * key is valid, not null and that the node has not been removed. 1325 * <code>key()</code> will catch any exceptions that this method throws. 1326 */ 1327 protected abstract String getSpi(String key); 1328 1329 /** 1330 * Sets the value of the given preferences entry for this node. 1331 * The implementation is not required to propagate the change to the 1332 * backing store immediately. It may not throw an exception when it tries 1333 * to write to the backing store and that operation fails, the failure 1334 * should be registered so a later invocation of <code>flush()</code> 1335 * or <code>sync()</code> can signal the failure. 1336 * <p> 1337 * Called by <code>put()</code> with this node locked after checking that 1338 * key and value are valid and non-null. 1339 */ 1340 protected abstract void putSpi(String key, String value); 1341 1342 /** 1343 * Removes the given key entry from this preferences node. 1344 * The implementation is not required to propagate the change to the 1345 * backing store immediately. It may not throw an exception when it tries 1346 * to write to the backing store and that operation fails, the failure 1347 * should be registered so a later invocation of <code>flush()</code> 1348 * or <code>sync()</code> can signal the failure. 1349 * <p> 1350 * Called by <code>remove()</code> with this node locked after checking 1351 * that the key is valid and non-null. 1352 */ 1353 protected abstract void removeSpi(String key); 1354 1355 /** 1356 * Writes all entries of this preferences node that have not yet been 1357 * written to the backing store and possibly creates this node in the 1358 * backing store, if it does not yet exist. Should only write changes to 1359 * this node and not write changes to any subnodes. 1360 * Note that the node can be already removed in this VM. To check if 1361 * that is the case the implementation can call <code>isRemoved()</code>. 1362 * <p> 1363 * Called (indirectly) by <code>flush()</code> with this node locked. 1364 */ 1365 protected abstract void flushSpi() throws BackingStoreException; 1366 1367 /** 1368 * Writes all entries of this preferences node that have not yet been 1369 * written to the backing store and reads any entries that have changed 1370 * in the backing store but that are not yet visible in this VM. 1371 * Should only sync this node and not change any of the subnodes. 1372 * Note that the node can be already removed in this VM. To check if 1373 * that is the case the implementation can call <code>isRemoved()</code>. 1374 * <p> 1375 * Called (indirectly) by <code>sync()</code> with this node locked. 1376 */ 1377 protected abstract void syncSpi() throws BackingStoreException; 1378 1379 /** 1380 * Clears this node from this VM and removes it from the backing store. 1381 * After this method has been called the node is marked as removed. 1382 * <p> 1383 * Called (indirectly) by <code>removeNode()</code> with this node locked 1384 * after all the sub nodes of this node have already been removed. 1385 */ 1386 protected abstract void removeNodeSpi() throws BackingStoreException; 1387 }