Source for java.util.prefs.AbstractPreferences

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