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