Source for java.util.prefs.AbstractPreferences

   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: }