Source for java.util.prefs.AbstractPreferences

   1: /* AbstractPreferences -- Partial implementation of a Preference node
   2:    Copyright (C) 2001, 2003, 2004, 2006  Free Software Foundation, Inc.
   3: 
   4: This file is part of GNU Classpath.
   5: 
   6: GNU Classpath is free software; you can redistribute it and/or modify
   7: it under the terms of the GNU General Public License as published by
   8: the Free Software Foundation; either version 2, or (at your option)
   9: any later version.
  10:  
  11: GNU Classpath is distributed in the hope that it will be useful, but
  12: WITHOUT ANY WARRANTY; without even the implied warranty of
  13: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  14: General Public License for more details.
  15: 
  16: You should have received a copy of the GNU General Public License
  17: along with GNU Classpath; see the file COPYING.  If not, write to the
  18: Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
  19: 02110-1301 USA.
  20: 
  21: Linking this library statically or dynamically with other modules is
  22: making a combined work based on this library.  Thus, the terms and
  23: conditions of the GNU General Public License cover the whole
  24: combination.
  25: 
  26: As a special exception, the copyright holders of this library give you
  27: permission to link this library with independent modules to produce an
  28: executable, regardless of the license terms of these independent
  29: modules, and to copy and distribute the resulting executable under
  30: terms of your choice, provided that you also meet, for each linked
  31: independent module, the terms and conditions of the license of that
  32: module.  An independent module is a module which is not derived from
  33: or based on this library.  If you modify this library, you may extend
  34: this exception to your version of the library, but you are not
  35: obligated to do so.  If you do not wish to do so, delete this
  36: exception statement from your version. */
  37: 
  38: 
  39: package java.util.prefs;
  40: 
  41: import gnu.java.util.prefs.EventDispatcher;
  42: import gnu.java.util.prefs.NodeWriter;
  43: 
  44: import java.io.ByteArrayOutputStream;
  45: import java.io.IOException;
  46: import java.io.OutputStream;
  47: import java.util.ArrayList;
  48: import java.util.Collection;
  49: import java.util.HashMap;
  50: import java.util.Iterator;
  51: import java.util.TreeSet;
  52: 
  53: /**
  54:  * Partial implementation of a Preference node.
  55:  *
  56:  * @since 1.4
  57:  * @author Mark Wielaard (mark@klomp.org)
  58:  */
  59: public abstract class AbstractPreferences extends Preferences {
  60: 
  61:     // protected fields
  62: 
  63:     /**
  64:      * Object used to lock this preference node. Any thread only locks nodes
  65:      * downwards when it has the lock on the current node. No method should
  66:      * synchronize on the lock of any of its parent nodes while holding the
  67:      * lock on the current node.
  68:      */
  69:     protected final Object lock = new Object();
  70: 
  71:     /**
  72:      * Set to true in the contructor if the node did not exist in the backing
  73:      * store when this preference node object was created. Should be set in
  74:      * the constructor of a subclass. Defaults to false. Used to fire node
  75:      * changed events.
  76:      */
  77:     protected boolean newNode = false;
  78: 
  79:     // private fields
  80: 
  81:     /**
  82:      * The parent preferences node or null when this is the root node.
  83:      */
  84:     private final AbstractPreferences parent;
  85: 
  86:     /**
  87:      * The name of this node.
  88:      * Only when this is a root node (parent == null) the name is empty.
  89:      * It has a maximum of 80 characters and cannot contain any '/' characters.
  90:      */
  91:     private final String name;
  92: 
  93:     /** True when this node has been remove, false otherwise. */
  94:     private boolean removed = false;
  95: 
  96:     /**
  97:      * Holds all the child names and nodes of this node that have been
  98:      * accessed by earlier <code>getChild()</code> or <code>childSpi()</code>
  99:      * invocations and that have not been removed.
 100:      */
 101:     private HashMap<String, AbstractPreferences> childCache
 102:       = new HashMap<String, AbstractPreferences>();
 103: 
 104:     /**
 105:      * A list of all the registered NodeChangeListener objects.
 106:      */
 107:     private ArrayList<NodeChangeListener> nodeListeners;
 108: 
 109:     /**
 110:      * A list of all the registered PreferenceChangeListener objects.
 111:      */
 112:     private ArrayList<PreferenceChangeListener> preferenceListeners;
 113: 
 114:     // constructor
 115: 
 116:     /**
 117:      * Creates a new AbstractPreferences node with the given parent and name.
 118:      * 
 119:      * @param parent the parent of this node or null when this is the root node
 120:      * @param name the name of this node, can not be null, only 80 characters
 121:      *             maximum, must be empty when parent is null and cannot
 122:      *             contain any '/' characters
 123:      * @exception IllegalArgumentException when name is null, greater then 80
 124:      *            characters, not the empty string but parent is null or
 125:      *            contains a '/' character
 126:      */
 127:     protected AbstractPreferences(AbstractPreferences parent, String name) {
 128:         if (  (name == null)                            // name should be given
 129:            || (name.length() > MAX_NAME_LENGTH)         // 80 characters max
 130:            || (parent == null && name.length() != 0)    // root has no name
 131:            || (parent != null && name.length() == 0)    // all other nodes do
 132:            || (name.indexOf('/') != -1))                // must not contain '/'
 133:             throw new IllegalArgumentException("Illegal name argument '"
 134:                                                + name
 135:                                                + "' (parent is "
 136:                                                + (parent == null ? "" : "not ")
 137:                                                + "null)");
 138:         this.parent = parent;
 139:         this.name = name;
 140:     }
 141: 
 142:     // identification methods
 143: 
 144:     /**
 145:      * Returns the absolute path name of this preference node.
 146:      * The absolute path name of a node is the path name of its parent node
 147:      * plus a '/' plus its own name. If the node is the root node and has no
 148:      * parent then its path name is "" and its absolute path name is "/".
 149:      */
 150:     public String absolutePath() {
 151:         if (parent == null)
 152:             return "/";
 153:         else
 154:             return parent.path() + '/' + name;
 155:     }
 156: 
 157:     /**
 158:      * Private helper method for absolutePath. Returns the empty string for a
 159:      * root node and otherwise the parentPath of its parent plus a '/'.
 160:      */
 161:     private String path() {
 162:         if (parent == null)
 163:             return "";
 164:         else
 165:             return parent.path() + '/' + name;
 166:     }
 167: 
 168:     /**
 169:      * Returns true if this node comes from the user preferences tree, false
 170:      * if it comes from the system preferences tree.
 171:      */
 172:     public boolean isUserNode() {
 173:         AbstractPreferences root = this;
 174:     while (root.parent != null)
 175:         root = root.parent;
 176:     return root == Preferences.userRoot();
 177:     }
 178: 
 179:     /**
 180:      * Returns the name of this preferences node. The name of the node cannot
 181:      * be null, can be mostly 80 characters and cannot contain any '/'
 182:      * characters. The root node has as name "".
 183:      */
 184:     public String name() {
 185:         return name;
 186:     }
 187: 
 188:     /**
 189:      * Returns the String given by
 190:      * <code>
 191:      * (isUserNode() ? "User":"System") + " Preference Node: " + absolutePath()
 192:      * </code>
 193:      */
 194:     public String toString() {
 195:         return (isUserNode() ? "User":"System")
 196:                + " Preference Node: "
 197:                + absolutePath();
 198:     }
 199: 
 200:     /**
 201:      * Returns all known unremoved children of this node.
 202:      *
 203:      * @return All known unremoved children of this node
 204:      */
 205:     protected final AbstractPreferences[] cachedChildren()
 206:     {
 207:       Collection<AbstractPreferences> vals = childCache.values();
 208:       return vals.toArray(new AbstractPreferences[vals.size()]);
 209:     }
 210: 
 211:     /**
 212:      * Returns all the direct sub nodes of this preferences node.
 213:      * Needs access to the backing store to give a meaningfull answer.
 214:      * <p>
 215:      * This implementation locks this node, checks if the node has not yet
 216:      * been removed and throws an <code>IllegalStateException</code> when it
 217:      * has been. Then it creates a new <code>TreeSet</code> and adds any
 218:      * already cached child nodes names. To get any uncached names it calls
 219:      * <code>childrenNamesSpi()</code> and adds the result to the set. Finally
 220:      * it calls <code>toArray()</code> on the created set. When the call to
 221:      * <code>childrenNamesSpi</code> thows an <code>BackingStoreException</code>
 222:      * this method will not catch that exception but propagate the exception
 223:      * to the caller.
 224:      *
 225:      * @exception BackingStoreException when the backing store cannot be
 226:      *            reached
 227:      * @exception IllegalStateException when this node has been removed
 228:      */
 229:     public String[] childrenNames() throws BackingStoreException {
 230:         synchronized(lock) {
 231:             if (isRemoved())
 232:                 throw new IllegalStateException("Node removed");
 233: 
 234:             TreeSet<String> childrenNames = new TreeSet<String>();
 235: 
 236:             // First get all cached node names
 237:             childrenNames.addAll(childCache.keySet());
 238:             
 239:             // Then add any others
 240:             String names[] = childrenNamesSpi();
 241:             for (int i = 0; i < names.length; i++) {
 242:                 childrenNames.add(names[i]);
 243:             }
 244: 
 245:             // And return the array of names
 246:             String[] children = new String[childrenNames.size()];
 247:             childrenNames.toArray(children);
 248:             return children;
 249: 
 250:         }
 251:     }
 252: 
 253:     /**
 254:      * Returns a sub node of this preferences node if the given path is
 255:      * relative (does not start with a '/') or a sub node of the root
 256:      * if the path is absolute (does start with a '/').
 257:      * <p>
 258:      * This method first locks this node and checks if the node has not been
 259:      * removed, if it has been removed it throws an exception. Then if the
 260:      * path is relative (does not start with a '/') it checks if the path is
 261:      * legal (does not end with a '/' and has no consecutive '/' characters).
 262:      * Then it recursively gets a name from the path, gets the child node
 263:      * from the child-cache of this node or calls the <code>childSpi()</code>
 264:      * method to create a new child sub node. This is done recursively on the
 265:      * newly created sub node with the rest of the path till the path is empty.
 266:      * If the path is absolute (starts with a '/') the lock on this node is
 267:      * droped and this method is called on the root of the preferences tree
 268:      * with as argument the complete path minus the first '/'.
 269:      *
 270:      * @exception IllegalStateException if this node has been removed
 271:      * @exception IllegalArgumentException if the path contains two or more
 272:      * consecutive '/' characters, ends with a '/' charactor and is not the
 273:      * string "/" (indicating the root node) or any name on the path is more
 274:      * than 80 characters long
 275:      */
 276:     public Preferences node(String path) {
 277:         synchronized(lock) {
 278:             if (isRemoved())
 279:                 throw new IllegalStateException("Node removed");
 280: 
 281:             // Is it a relative path?
 282:             if (!path.startsWith("/")) {
 283: 
 284:                 // Check if it is a valid path
 285:                 if (path.indexOf("//") != -1 || path.endsWith("/"))
 286:                     throw new IllegalArgumentException(path);
 287: 
 288:                 return getNode(path);
 289:             }
 290:         }
 291: 
 292:         // path started with a '/' so it is absolute
 293:         // we drop the lock and start from the root (omitting the first '/')
 294:         Preferences root = isUserNode() ? userRoot() : systemRoot();
 295:         return root.node(path.substring(1));
 296: 
 297:     }
 298: 
 299:     /**
 300:      * Private helper method for <code>node()</code>. Called with this node
 301:      * locked. Returns this node when path is the empty string, if it is not
 302:      * empty the next node name is taken from the path (all chars till the
 303:      * next '/' or end of path string) and the node is either taken from the
 304:      * child-cache of this node or the <code>childSpi()</code> method is called
 305:      * on this node with the name as argument. Then this method is called
 306:      * recursively on the just constructed child node with the rest of the
 307:      * path.
 308:      *
 309:      * @param path should not end with a '/' character and should not contain
 310:      *        consecutive '/' characters
 311:      * @exception IllegalArgumentException if path begins with a name that is
 312:      *            larger then 80 characters.
 313:      */
 314:     private Preferences getNode(String path) {
 315:         // if mark is dom then goto end
 316: 
 317:         // Empty String "" indicates this node
 318:         if (path.length() == 0)
 319:             return this;
 320: 
 321:         // Calculate child name and rest of path
 322:         String childName;
 323:         String childPath;
 324:         int nextSlash = path.indexOf('/');
 325:         if (nextSlash == -1) {
 326:             childName = path;
 327:             childPath = "";
 328:         } else {
 329:             childName = path.substring(0, nextSlash);
 330:             childPath = path.substring(nextSlash+1);
 331:         }
 332: 
 333:         // Get the child node
 334:         AbstractPreferences child;
 335:         child = (AbstractPreferences)childCache.get(childName);
 336:         if (child == null) {
 337: 
 338:             if (childName.length() > MAX_NAME_LENGTH)
 339:                throw new IllegalArgumentException(childName); 
 340: 
 341:             // Not in childCache yet so create a new sub node
 342:             child = childSpi(childName);
 343:             childCache.put(childName, child);
 344:             if (child.newNode && nodeListeners != null)
 345:               fire(new NodeChangeEvent(this, child), true);
 346:         }
 347: 
 348:         // Lock the child and go down
 349:         synchronized(child.lock) {
 350:             return child.getNode(childPath);
 351:         }
 352:     }
 353: 
 354:     /**
 355:      * Returns true if the node that the path points to exists in memory or
 356:      * in the backing store. Otherwise it returns false or an exception is
 357:      * thrown. When this node is removed the only valid parameter is the
 358:      * empty string (indicating this node), the return value in that case
 359:      * will be false.
 360:      *
 361:      * @exception BackingStoreException when the backing store cannot be
 362:      *            reached
 363:      * @exception IllegalStateException if this node has been removed
 364:      *            and the path is not the empty string (indicating this node)
 365:      * @exception IllegalArgumentException if the path contains two or more
 366:      * consecutive '/' characters, ends with a '/' charactor and is not the
 367:      * string "/" (indicating the root node) or any name on the path is more
 368:      * then 80 characters long
 369:      */
 370:     public boolean nodeExists(String path) throws BackingStoreException {
 371:         synchronized(lock) {
 372:             if (isRemoved() && path.length() != 0)
 373:                 throw new IllegalStateException("Node removed");
 374: 
 375:             // Is it a relative path?
 376:             if (!path.startsWith("/")) {
 377: 
 378:                 // Check if it is a valid path
 379:                 if (path.indexOf("//") != -1 || path.endsWith("/"))
 380:                     throw new IllegalArgumentException(path);
 381: 
 382:                 return existsNode(path);
 383:             }
 384:         }
 385: 
 386:         // path started with a '/' so it is absolute
 387:         // we drop the lock and start from the root (omitting the first '/')
 388:         Preferences root = isUserNode() ? userRoot() : systemRoot();
 389:         return root.nodeExists(path.substring(1));
 390: 
 391:     }
 392: 
 393:     private boolean existsNode(String path) throws BackingStoreException {
 394: 
 395:         // Empty String "" indicates this node
 396:         if (path.length() == 0)
 397:             return(!isRemoved());
 398: 
 399:         // Calculate child name and rest of path
 400:         String childName;
 401:         String childPath;
 402:         int nextSlash = path.indexOf('/');
 403:         if (nextSlash == -1) {
 404:             childName = path;
 405:             childPath = "";
 406:         } else {
 407:             childName = path.substring(0, nextSlash);
 408:             childPath = path.substring(nextSlash+1);
 409:         }
 410: 
 411:         // Get the child node
 412:         AbstractPreferences child;
 413:         child = (AbstractPreferences)childCache.get(childName);
 414:         if (child == null) {
 415: 
 416:             if (childName.length() > MAX_NAME_LENGTH)
 417:                throw new IllegalArgumentException(childName);
 418: 
 419:             // Not in childCache yet so create a new sub node
 420:             child = getChild(childName);
 421: 
 422:             if (child == null)
 423:                 return false;
 424: 
 425:             childCache.put(childName, child);
 426:         }
 427: 
 428:         // Lock the child and go down
 429:         synchronized(child.lock) {
 430:             return child.existsNode(childPath);
 431:         }
 432:     }
 433: 
 434:     /**
 435:      * Returns the child sub node if it exists in the backing store or null
 436:      * if it does not exist. Called (indirectly) by <code>nodeExists()</code>
 437:      * when a child node name can not be found in the cache.
 438:      * <p>
 439:      * Gets the lock on this node, calls <code>childrenNamesSpi()</code> to
 440:      * get an array of all (possibly uncached) children and compares the
 441:      * given name with the names in the array. If the name is found in the
 442:      * array <code>childSpi()</code> is called to get an instance, otherwise
 443:      * null is returned.
 444:      *
 445:      * @exception BackingStoreException when the backing store cannot be
 446:      *            reached
 447:      */
 448:     protected AbstractPreferences getChild(String name)
 449:                                     throws BackingStoreException
 450:     {
 451:         synchronized(lock) {
 452:             // Get all the names (not yet in the cache)
 453:             String[] names = childrenNamesSpi();
 454:             for (int i=0; i < names.length; i++)
 455:                 if (name.equals(names[i]))
 456:                     return childSpi(name);
 457:            
 458:             // No child with that name found
 459:             return null;
 460:         }
 461:     }
 462: 
 463:     /**
 464:      * Returns true if this node has been removed with the
 465:      * <code>removeNode()</code> method, false otherwise.
 466:      * <p>
 467:      * Gets the lock on this node and then returns a boolean field set by
 468:      * <code>removeNode</code> methods.
 469:      */
 470:     protected boolean isRemoved() {
 471:         synchronized(lock) {
 472:             return removed;
 473:         }
 474:     }
 475: 
 476:     /**
 477:      * Returns the parent preferences node of this node or null if this is
 478:      * the root of the preferences tree.
 479:      * <p>
 480:      * Gets the lock on this node, checks that the node has not been removed
 481:      * and returns the parent given to the constructor.
 482:      *
 483:      * @exception IllegalStateException if this node has been removed
 484:      */
 485:     public Preferences parent() {
 486:         synchronized(lock) {
 487:             if (isRemoved())
 488:                 throw new IllegalStateException("Node removed");
 489: 
 490:             return parent;
 491:         }
 492:     }
 493: 
 494:     // export methods
 495: 
 496:     // Inherit javadoc.
 497:     public void exportNode(OutputStream os)
 498:                                     throws BackingStoreException,
 499:                                            IOException
 500:     {
 501:         NodeWriter nodeWriter = new NodeWriter(this, os);
 502:         nodeWriter.writePrefs();
 503:     }
 504: 
 505:     // Inherit javadoc.
 506:     public void exportSubtree(OutputStream os)
 507:                                     throws BackingStoreException,
 508:                                            IOException
 509:     {
 510:         NodeWriter nodeWriter = new NodeWriter(this, os);
 511:         nodeWriter.writePrefsTree();
 512:     }
 513: 
 514:     // preference entry manipulation methods
 515: 
 516:     /**
 517:      * Returns an (possibly empty) array with all the keys of the preference
 518:      * entries of this node.
 519:      * <p>
 520:      * This method locks this node and checks if the node has not been
 521:      * removed, if it has been removed it throws an exception, then it returns
 522:      * the result of calling <code>keysSpi()</code>.
 523:      * 
 524:      * @exception BackingStoreException when the backing store cannot be     
 525:      *            reached
 526:      * @exception IllegalStateException if this node has been removed
 527:      */
 528:     public String[] keys() throws BackingStoreException {
 529:         synchronized(lock) {
 530:             if (isRemoved())
 531:                 throw new IllegalStateException("Node removed");
 532: 
 533:             return keysSpi();
 534:         }
 535:     }
 536: 
 537: 
 538:     /**
 539:      * Returns the value associated with the key in this preferences node. If
 540:      * the default value of the key cannot be found in the preferences node
 541:      * entries or something goes wrong with the backing store the supplied
 542:      * default value is returned.
 543:      * <p>
 544:      * Checks that key is not null and not larger then 80 characters,
 545:      * locks this node, and checks that the node has not been removed.
 546:      * Then it calls <code>keySpi()</code> and returns
 547:      * the result of that method or the given default value if it returned
 548:      * null or throwed an exception.
 549:      *
 550:      * @exception IllegalArgumentException if key is larger then 80 characters
 551:      * @exception IllegalStateException if this node has been removed
 552:      * @exception NullPointerException if key is null
 553:      */
 554:     public String get(String key, String defaultVal) {
 555:         if (key.length() > MAX_KEY_LENGTH)
 556:             throw new IllegalArgumentException(key);
 557: 
 558:         synchronized(lock) {
 559:             if (isRemoved())
 560:                 throw new IllegalStateException("Node removed");
 561: 
 562:             String value;
 563:             try {
 564:                 value = getSpi(key);
 565:             } catch (ThreadDeath death) {
 566:                 throw death;
 567:             } catch (Throwable t) {
 568:                 value = null;
 569:             }
 570: 
 571:             if (value != null) {
 572:                 return value;
 573:             } else {
 574:                 return defaultVal;
 575:             }
 576:         }
 577:     }
 578: 
 579:     /**
 580:      * Convenience method for getting the given entry as a boolean.
 581:      * When the string representation of the requested entry is either
 582:      * "true" or "false" (ignoring case) then that value is returned,
 583:      * otherwise the given default boolean value is returned.
 584:      *
 585:      * @exception IllegalArgumentException if key is larger then 80 characters
 586:      * @exception IllegalStateException if this node has been removed
 587:      * @exception NullPointerException if key is null
 588:      */
 589:     public boolean getBoolean(String key, boolean defaultVal) {
 590:         String value = get(key, null);
 591: 
 592:         if ("true".equalsIgnoreCase(value))
 593:             return true;
 594: 
 595:         if ("false".equalsIgnoreCase(value))
 596:             return false;
 597:         
 598:         return defaultVal;
 599:     }
 600: 
 601:     /**
 602:      * Convenience method for getting the given entry as a byte array.
 603:      * When the string representation of the requested entry is a valid
 604:      * Base64 encoded string (without any other characters, such as newlines)
 605:      * then the decoded Base64 string is returned as byte array,
 606:      * otherwise the given default byte array value is returned.
 607:      *
 608:      * @exception IllegalArgumentException if key is larger then 80 characters
 609:      * @exception IllegalStateException if this node has been removed
 610:      * @exception NullPointerException if key is null
 611:      */
 612:     public byte[] getByteArray(String key, byte[] defaultVal) {
 613:         String value = get(key, null);
 614: 
 615:         byte[] b = null;
 616:         if (value != null) {
 617:             b = decode64(value);
 618:         }
 619: 
 620:         if (b != null)
 621:             return b;
 622:         else
 623:             return defaultVal;
 624:     }
 625:     
 626:     /**
 627:      * Helper method for decoding a Base64 string as an byte array.
 628:      * Returns null on encoding error. This method does not allow any other
 629:      * characters present in the string then the 65 special base64 chars.
 630:      */
 631:     private static byte[] decode64(String s) {
 632:         ByteArrayOutputStream bs = new ByteArrayOutputStream((s.length()/4)*3);
 633:         char[] c = new char[s.length()];
 634:         s.getChars(0, s.length(), c, 0);
 635: 
 636:         // Convert from base64 chars
 637:         int endchar = -1;
 638:         for(int j = 0; j < c.length && endchar == -1; j++) {
 639:             if (c[j] >= 'A' && c[j] <= 'Z') {
 640:                 c[j] -= 'A';
 641:             } else if (c[j] >= 'a' && c[j] <= 'z') {
 642:                 c[j] = (char) (c[j] + 26 - 'a');
 643:             } else if (c[j] >= '0' && c[j] <= '9') {
 644:                 c[j] = (char) (c[j] + 52 - '0');
 645:             } else if (c[j] == '+') {
 646:                 c[j] = 62;
 647:             } else if (c[j] == '/') {
 648:                 c[j] = 63;
 649:             } else if (c[j] == '=') {
 650:                 endchar = j;
 651:             } else {
 652:                 return null; // encoding exception
 653:             }
 654:         }
 655: 
 656:         int remaining = endchar == -1 ? c.length : endchar;
 657:         int i = 0;
 658:         while (remaining > 0) {
 659:             // Four input chars (6 bits) are decoded as three bytes as
 660:             // 000000 001111 111122 222222
 661: 
 662:             byte b0 = (byte) (c[i] << 2);
 663:             if (remaining >= 2) {
 664:                 b0 += (c[i+1] & 0x30) >> 4;
 665:             }
 666:             bs.write(b0);
 667: 
 668:             if (remaining >= 3) {
 669:                 byte b1 = (byte) ((c[i+1] & 0x0F) << 4);
 670:                 b1 += (byte) ((c[i+2] & 0x3C) >> 2);
 671:                 bs.write(b1);
 672:             }
 673: 
 674:             if (remaining >= 4) {
 675:                 byte b2 = (byte) ((c[i+2] & 0x03) << 6);
 676:                 b2 += c[i+3];
 677:                 bs.write(b2);
 678:             }
 679: 
 680:             i += 4;
 681:             remaining -= 4;
 682:         }
 683: 
 684:         return bs.toByteArray();
 685:     }
 686: 
 687:     /**
 688:      * Convenience method for getting the given entry as a double.
 689:      * When the string representation of the requested entry can be decoded
 690:      * with <code>Double.parseDouble()</code> then that double is returned,
 691:      * otherwise the given default double value is returned.
 692:      *
 693:      * @exception IllegalArgumentException if key is larger then 80 characters
 694:      * @exception IllegalStateException if this node has been removed
 695:      * @exception NullPointerException if key is null
 696:      */
 697:     public double getDouble(String key, double defaultVal) {
 698:         String value = get(key, null);
 699: 
 700:         if (value != null) {
 701:             try {
 702:                 return Double.parseDouble(value);
 703:             } catch (NumberFormatException nfe) { /* ignore */ }
 704:         }
 705: 
 706:         return defaultVal;
 707:     }
 708: 
 709:     /**
 710:      * Convenience method for getting the given entry as a float.
 711:      * When the string representation of the requested entry can be decoded
 712:      * with <code>Float.parseFloat()</code> then that float is returned,
 713:      * otherwise the given default float value is returned.
 714:      *
 715:      * @exception IllegalArgumentException if key is larger then 80 characters
 716:      * @exception IllegalStateException if this node has been removed
 717:      * @exception NullPointerException if key is null
 718:      */
 719:     public float getFloat(String key, float defaultVal) {
 720:         String value = get(key, null);
 721: 
 722:         if (value != null) {
 723:             try {
 724:                 return Float.parseFloat(value);
 725:             } catch (NumberFormatException nfe) { /* ignore */ }
 726:         }
 727: 
 728:         return defaultVal;
 729:     }
 730: 
 731:     /**
 732:      * Convenience method for getting the given entry as an integer.
 733:      * When the string representation of the requested entry can be decoded
 734:      * with <code>Integer.parseInt()</code> then that integer is returned,
 735:      * otherwise the given default integer value is returned.
 736:      *
 737:      * @exception IllegalArgumentException if key is larger then 80 characters
 738:      * @exception IllegalStateException if this node has been removed
 739:      * @exception NullPointerException if key is null
 740:      */
 741:     public int getInt(String key, int defaultVal) {
 742:         String value = get(key, null);
 743: 
 744:         if (value != null) {
 745:             try {
 746:                 return Integer.parseInt(value);
 747:             } catch (NumberFormatException nfe) { /* ignore */ }
 748:         }
 749: 
 750:         return defaultVal;
 751:     }
 752: 
 753:     /**
 754:      * Convenience method for getting the given entry as a long.
 755:      * When the string representation of the requested entry can be decoded
 756:      * with <code>Long.parseLong()</code> then that long is returned,
 757:      * otherwise the given default long value is returned.
 758:      *
 759:      * @exception IllegalArgumentException if key is larger then 80 characters
 760:      * @exception IllegalStateException if this node has been removed
 761:      * @exception NullPointerException if key is null
 762:      */
 763:     public long getLong(String key, long defaultVal) {
 764:         String value = get(key, null);
 765: 
 766:         if (value != null) {
 767:             try {
 768:                 return Long.parseLong(value);
 769:             } catch (NumberFormatException nfe) { /* ignore */ }
 770:         }
 771: 
 772:         return defaultVal;
 773:     }
 774: 
 775:     /**
 776:      * Sets the value of the given preferences entry for this node.
 777:      * Key and value cannot be null, the key cannot exceed 80 characters
 778:      * and the value cannot exceed 8192 characters.
 779:      * <p>
 780:      * The result will be immediately visible in this VM, but may not be
 781:      * immediately written to the backing store.
 782:      * <p>
 783:      * Checks that key and value are valid, locks this node, and checks that
 784:      * the node has not been removed. Then it calls <code>putSpi()</code>.
 785:      *
 786:      * @exception NullPointerException if either key or value are null
 787:      * @exception IllegalArgumentException if either key or value are to large
 788:      * @exception IllegalStateException when this node has been removed
 789:      */
 790:     public void put(String key, String value) {
 791:         if (key.length() > MAX_KEY_LENGTH
 792:             || value.length() > MAX_VALUE_LENGTH)
 793:             throw new IllegalArgumentException("key ("
 794:                                                + key.length() + ")"
 795:                                                + " or value ("
 796:                                                + value.length() + ")"
 797:                                                + " to large");
 798:         synchronized(lock) {
 799:             if (isRemoved())
 800:                 throw new IllegalStateException("Node removed");
 801: 
 802:             putSpi(key, value);
 803: 
 804:             if (preferenceListeners != null)
 805:               fire(new PreferenceChangeEvent(this, key, value));
 806:         }
 807:             
 808:     }
 809: 
 810:     /**
 811:      * Convenience method for setting the given entry as a boolean.
 812:      * The boolean is converted with <code>Boolean.toString(value)</code>
 813:      * and then stored in the preference entry as that string.
 814:      *
 815:      * @exception NullPointerException if key is null
 816:      * @exception IllegalArgumentException if the key length is to large
 817:      * @exception IllegalStateException when this node has been removed
 818:      */
 819:     public void putBoolean(String key, boolean value) {
 820:         put(key, Boolean.toString(value));
 821:     }
 822: 
 823:     /**
 824:      * Convenience method for setting the given entry as an array of bytes.
 825:      * The byte array is converted to a Base64 encoded string
 826:      * and then stored in the preference entry as that string.
 827:      * <p>
 828:      * Note that a byte array encoded as a Base64 string will be about 1.3
 829:      * times larger then the original length of the byte array, which means
 830:      * that the byte array may not be larger about 6 KB.
 831:      *
 832:      * @exception NullPointerException if either key or value are null
 833:      * @exception IllegalArgumentException if either key or value are to large
 834:      * @exception IllegalStateException when this node has been removed
 835:      */
 836:     public void putByteArray(String key, byte[] value) {
 837:         put(key, encode64(value));
 838:     }
 839: 
 840:     /**
 841:      * Helper method for encoding an array of bytes as a Base64 String.
 842:      */
 843:     private static String encode64(byte[] b) {
 844:         StringBuffer sb = new StringBuffer((b.length/3)*4);
 845: 
 846:         int i = 0;
 847:         int remaining = b.length;