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