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