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