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