Imported GNU Classpath 0.90
[official-gcc.git] / libjava / classpath / java / util / prefs / AbstractPreferences.java
blobe676dc3105c040d67d1dc3a9b8bdfdcc98af7d77
1 /* AbstractPreferences -- Partial implementation of a Preference node
2 Copyright (C) 2001, 2003, 2004, 2006 Free Software Foundation, Inc.
4 This file is part of GNU Classpath.
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.
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.
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.
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.
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. */
39 package java.util.prefs;
41 import gnu.java.util.prefs.EventDispatcher;
42 import gnu.java.util.prefs.NodeWriter;
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;
52 /**
53 * Partial implementation of a Preference node.
55 * @since 1.4
56 * @author Mark Wielaard (mark@klomp.org)
58 public abstract class AbstractPreferences extends Preferences {
60 // protected fields
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.
68 protected final Object lock = new Object();
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.
76 protected boolean newNode = false;
78 // private fields
80 /**
81 * The parent preferences node or null when this is the root node.
83 private final AbstractPreferences parent;
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.
90 private final String name;
92 /** True when this node has been remove, false otherwise. */
93 private boolean removed = false;
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.
100 private HashMap childCache = new HashMap();
103 * A list of all the registered NodeChangeListener objects.
105 private ArrayList nodeListeners;
108 * A list of all the registered PreferenceChangeListener objects.
110 private ArrayList preferenceListeners;
112 // constructor
115 * Creates a new AbstractPreferences node with the given parent and name.
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
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;
140 // identification methods
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 "/".
148 public String absolutePath() {
149 if (parent == null)
150 return "/";
151 else
152 return parent.path() + '/' + name;
156 * Private helper method for absolutePath. Returns the empty string for a
157 * root node and otherwise the parentPath of its parent plus a '/'.
159 private String path() {
160 if (parent == null)
161 return "";
162 else
163 return parent.path() + '/' + name;
167 * Returns true if this node comes from the user preferences tree, false
168 * if it comes from the system preferences tree.
170 public boolean isUserNode() {
171 AbstractPreferences root = this;
172 while (root.parent != null)
173 root = root.parent;
174 return root == Preferences.userRoot();
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 "".
182 public String name() {
183 return name;
187 * Returns the String given by
188 * <code>
189 * (isUserNode() ? "User":"System") + " Preference Node: " + absolutePath()
190 * </code>
192 public String toString() {
193 return (isUserNode() ? "User":"System")
194 + " Preference Node: "
195 + absolutePath();
199 * Returns all known unremoved children of this node.
201 * @return All known unremoved children of this node
203 protected final AbstractPreferences[] cachedChildren()
205 return (AbstractPreferences[]) childCache.values().toArray();
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.
222 * @exception BackingStoreException when the backing store cannot be
223 * reached
224 * @exception IllegalStateException when this node has been removed
226 public String[] childrenNames() throws BackingStoreException {
227 synchronized(lock) {
228 if (isRemoved())
229 throw new IllegalStateException("Node removed");
231 TreeSet childrenNames = new TreeSet();
233 // First get all cached node names
234 childrenNames.addAll(childCache.keySet());
236 // Then add any others
237 String names[] = childrenNamesSpi();
238 for (int i = 0; i < names.length; i++) {
239 childrenNames.add(names[i]);
242 // And return the array of names
243 String[] children = new String[childrenNames.size()];
244 childrenNames.toArray(children);
245 return children;
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 '/'.
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
273 public Preferences node(String path) {
274 synchronized(lock) {
275 if (isRemoved())
276 throw new IllegalStateException("Node removed");
278 // Is it a relative path?
279 if (!path.startsWith("/")) {
281 // Check if it is a valid path
282 if (path.indexOf("//") != -1 || path.endsWith("/"))
283 throw new IllegalArgumentException(path);
285 return getNode(path);
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));
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.
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.
311 private Preferences getNode(String path) {
312 // if mark is dom then goto end
314 // Empty String "" indicates this node
315 if (path.length() == 0)
316 return this;
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);
330 // Get the child node
331 AbstractPreferences child;
332 child = (AbstractPreferences)childCache.get(childName);
333 if (child == null) {
335 if (childName.length() > MAX_NAME_LENGTH)
336 throw new IllegalArgumentException(childName);
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);
345 // Lock the child and go down
346 synchronized(child.lock) {
347 return child.getNode(childPath);
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.
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
367 public boolean nodeExists(String path) throws BackingStoreException {
368 synchronized(lock) {
369 if (isRemoved() && path.length() != 0)
370 throw new IllegalStateException("Node removed");
372 // Is it a relative path?
373 if (!path.startsWith("/")) {
375 // Check if it is a valid path
376 if (path.indexOf("//") != -1 || path.endsWith("/"))
377 throw new IllegalArgumentException(path);
379 return existsNode(path);
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));
390 private boolean existsNode(String path) throws BackingStoreException {
392 // Empty String "" indicates this node
393 if (path.length() == 0)
394 return(!isRemoved());
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);
408 // Get the child node
409 AbstractPreferences child;
410 child = (AbstractPreferences)childCache.get(childName);
411 if (child == null) {
413 if (childName.length() > MAX_NAME_LENGTH)
414 throw new IllegalArgumentException(childName);
416 // Not in childCache yet so create a new sub node
417 child = getChild(childName);
419 if (child == null)
420 return false;
422 childCache.put(childName, child);
425 // Lock the child and go down
426 synchronized(child.lock) {
427 return child.existsNode(childPath);
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.
442 * @exception BackingStoreException when the backing store cannot be
443 * reached
445 protected AbstractPreferences getChild(String name)
446 throws BackingStoreException
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);
455 // No child with that name found
456 return null;
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.
467 protected boolean isRemoved() {
468 synchronized(lock) {
469 return removed;
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.
480 * @exception IllegalStateException if this node has been removed
482 public Preferences parent() {
483 synchronized(lock) {
484 if (isRemoved())
485 throw new IllegalStateException("Node removed");
487 return parent;
491 // export methods
493 // Inherit javadoc.
494 public void exportNode(OutputStream os)
495 throws BackingStoreException,
496 IOException
498 NodeWriter nodeWriter = new NodeWriter(this, os);
499 nodeWriter.writePrefs();
502 // Inherit javadoc.
503 public void exportSubtree(OutputStream os)
504 throws BackingStoreException,
505 IOException
507 NodeWriter nodeWriter = new NodeWriter(this, os);
508 nodeWriter.writePrefsTree();
511 // preference entry manipulation methods
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>.
521 * @exception BackingStoreException when the backing store cannot be
522 * reached
523 * @exception IllegalStateException if this node has been removed
525 public String[] keys() throws BackingStoreException {
526 synchronized(lock) {
527 if (isRemoved())
528 throw new IllegalStateException("Node removed");
530 return keysSpi();
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.
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
551 public String get(String key, String defaultVal) {
552 if (key.length() > MAX_KEY_LENGTH)
553 throw new IllegalArgumentException(key);
555 synchronized(lock) {
556 if (isRemoved())
557 throw new IllegalStateException("Node removed");
559 String value;
560 try {
561 value = getSpi(key);
562 } catch (ThreadDeath death) {
563 throw death;
564 } catch (Throwable t) {
565 value = null;
568 if (value != null) {
569 return value;
570 } else {
571 return defaultVal;
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.
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
586 public boolean getBoolean(String key, boolean defaultVal) {
587 String value = get(key, null);
589 if ("true".equalsIgnoreCase(value))
590 return true;
592 if ("false".equalsIgnoreCase(value))
593 return false;
595 return defaultVal;
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.
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
609 public byte[] getByteArray(String key, byte[] defaultVal) {
610 String value = get(key, null);
612 byte[] b = null;
613 if (value != null) {
614 b = decode64(value);
617 if (b != null)
618 return b;
619 else
620 return defaultVal;
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.
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);
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
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
659 byte b0 = (byte) (c[i] << 2);
660 if (remaining >= 2) {
661 b0 += (c[i+1] & 0x30) >> 4;
663 bs.write(b0);
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);
671 if (remaining >= 4) {
672 byte b2 = (byte) ((c[i+2] & 0x03) << 6);
673 b2 += c[i+3];
674 bs.write(b2);
677 i += 4;
678 remaining -= 4;
681 return bs.toByteArray();
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.
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
694 public double getDouble(String key, double defaultVal) {
695 String value = get(key, null);
697 if (value != null) {
698 try {
699 return Double.parseDouble(value);
700 } catch (NumberFormatException nfe) { /* ignore */ }
703 return defaultVal;
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.
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
716 public float getFloat(String key, float defaultVal) {
717 String value = get(key, null);
719 if (value != null) {
720 try {
721 return Float.parseFloat(value);
722 } catch (NumberFormatException nfe) { /* ignore */ }
725 return defaultVal;
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.
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
738 public int getInt(String key, int defaultVal) {
739 String value = get(key, null);
741 if (value != null) {
742 try {
743 return Integer.parseInt(value);
744 } catch (NumberFormatException nfe) { /* ignore */ }
747 return defaultVal;
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.
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
760 public long getLong(String key, long defaultVal) {
761 String value = get(key, null);
763 if (value != null) {
764 try {
765 return Long.parseLong(value);
766 } catch (NumberFormatException nfe) { /* ignore */ }
769 return defaultVal;
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>.
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
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");
799 putSpi(key, value);
801 if (preferenceListeners != null)
802 fire(new PreferenceChangeEvent(this, key, value));
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.
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
816 public void putBoolean(String key, boolean value) {
817 put(key, Boolean.toString(value));
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.
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
833 public void putByteArray(String key, byte[] value) {
834 put(key, encode64(value));
838 * Helper method for encoding an array of bytes as a Base64 String.
840 private static String encode64(byte[] b) {
841 StringBuffer sb = new StringBuffer((b.length/3)*4);
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
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;
861 } else {
862 c[2] = 64;
863 c[3] = 64;
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] = '=';
883 sb.append(c);
884 i += 3;
885 remaining -= 3;
888 return sb.toString();
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.
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
900 public void putDouble(String key, double value) {
901 put(key, Double.toString(value));
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.
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
913 public void putFloat(String key, float value) {
914 put(key, Float.toString(value));
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.
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
926 public void putInt(String key, int value) {
927 put(key, Integer.toString(value));
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.
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
939 public void putLong(String key, long value) {
940 put(key, Long.toString(value));
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.
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
957 public void remove(String key) {
958 if (key.length() > MAX_KEY_LENGTH)
959 throw new IllegalArgumentException(key);
961 synchronized(lock) {
962 if (isRemoved())
963 throw new IllegalStateException("Node removed");
965 removeSpi(key);
967 if (preferenceListeners != null)
968 fire(new PreferenceChangeEvent(this, key, null));
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.
983 * @exception BackingStoreException when the backing store cannot be
984 * reached
985 * @exception IllegalStateException if this node has been removed
987 public void clear() throws BackingStoreException {
988 synchronized(lock) {
989 if (isRemoved())
990 throw new IllegalStateException("Node Removed");
992 String[] keys = keys();
993 for (int i = 0; i < keys.length; i++) {
994 removeSpi(keys[i]);
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.
1012 * @exception BackingStoreException when the backing store cannot be
1013 * reached
1015 public void flush() throws BackingStoreException {
1016 flushNode(false);
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.
1030 * @exception BackingStoreException when the backing store cannot be
1031 * reached
1032 * @exception IllegalStateException if this node has been removed
1034 public void sync() throws BackingStoreException {
1035 flushNode(true);
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>
1048 private void flushNode(boolean sync) throws BackingStoreException {
1049 String[] keys = null;
1050 synchronized(lock) {
1051 if (sync) {
1052 syncSpi();
1053 } else {
1054 flushSpi();
1056 keys = (String[]) childCache.keySet().toArray(new String[]{});
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]);
1067 // The child could already have been removed from the cache
1068 if (subNode != null) {
1069 subNode.flushNode(sync);
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.
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
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");
1101 synchronized (parent.lock) {
1102 synchronized(this.lock) {
1103 if (isRemoved())
1104 throw new IllegalStateException("Node Removed");
1106 purge();
1108 parent.childCache.remove(name);
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.
1123 private void purge() throws BackingStoreException
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]));
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();
1141 // Cache is empty now
1142 childCache.clear();
1144 // remove this node
1145 removeNodeSpi();
1146 removed = true;
1148 if (nodeListeners != null)
1149 fire(new NodeChangeEvent(parent, this), false);
1152 // listener methods
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
1159 public void addNodeChangeListener(NodeChangeListener listener)
1161 synchronized (lock)
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);
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
1178 public void addPreferenceChangeListener(PreferenceChangeListener listener)
1180 synchronized (lock)
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);
1193 * Remove the indicated node change listener from the list of
1194 * listeners to notify.
1195 * @param listener the listener to remove
1197 public void removeNodeChangeListener(NodeChangeListener listener)
1199 synchronized (lock)
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);
1211 * Remove the indicated preference change listener from the list of
1212 * listeners to notify.
1213 * @param listener the listener to remove
1215 public void removePreferenceChangeListener (PreferenceChangeListener listener)
1217 synchronized (lock)
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);
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
1234 private void fire(final PreferenceChangeEvent event)
1236 Iterator it = preferenceListeners.iterator();
1237 while (it.hasNext())
1239 final PreferenceChangeListener l = (PreferenceChangeListener) it.next();
1240 EventDispatcher.dispatch(new Runnable()
1242 public void run()
1244 l.preferenceChange(event);
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
1256 private void fire(final NodeChangeEvent event, final boolean added)
1258 Iterator it = nodeListeners.iterator();
1259 while (it.hasNext())
1261 final NodeChangeListener l = (NodeChangeListener) it.next();
1262 EventDispatcher.dispatch(new Runnable()
1264 public void run()
1266 if (added)
1267 l.childAdded(event);
1268 else
1269 l.childRemoved(event);
1275 // abstract spi methods
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.
1287 protected abstract String[] childrenNamesSpi() throws BackingStoreException;
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>.
1301 protected abstract AbstractPreferences childSpi(String name);
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.
1311 * @exception BackingStoreException when the backing store cannot be
1312 * reached
1314 protected abstract String[] keysSpi() throws BackingStoreException;
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.
1324 protected abstract String getSpi(String key);
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.
1337 protected abstract void putSpi(String key, String value);
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.
1350 protected abstract void removeSpi(String key);
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.
1362 protected abstract void flushSpi() throws BackingStoreException;
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.
1374 protected abstract void syncSpi() throws BackingStoreException;
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.
1383 protected abstract void removeNodeSpi() throws BackingStoreException;