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