001 /* AbstractPreferences -- Partial implementation of a Preference node
002 Copyright (C) 2001, 2003, 2004, 2006 Free Software Foundation, Inc.
003
004 This file is part of GNU Classpath.
005
006 GNU Classpath is free software; you can redistribute it and/or modify
007 it under the terms of the GNU General Public License as published by
008 the Free Software Foundation; either version 2, or (at your option)
009 any later version.
010
011 GNU Classpath is distributed in the hope that it will be useful, but
012 WITHOUT ANY WARRANTY; without even the implied warranty of
013 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014 General Public License for more details.
015
016 You should have received a copy of the GNU General Public License
017 along with GNU Classpath; see the file COPYING. If not, write to the
018 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
019 02110-1301 USA.
020
021 Linking this library statically or dynamically with other modules is
022 making a combined work based on this library. Thus, the terms and
023 conditions of the GNU General Public License cover the whole
024 combination.
025
026 As a special exception, the copyright holders of this library give you
027 permission to link this library with independent modules to produce an
028 executable, regardless of the license terms of these independent
029 modules, and to copy and distribute the resulting executable under
030 terms of your choice, provided that you also meet, for each linked
031 independent module, the terms and conditions of the license of that
032 module. An independent module is a module which is not derived from
033 or based on this library. If you modify this library, you may extend
034 this exception to your version of the library, but you are not
035 obligated to do so. If you do not wish to do so, delete this
036 exception statement from your version. */
037
038
039 package java.util.prefs;
040
041 import gnu.java.util.prefs.EventDispatcher;
042 import gnu.java.util.prefs.NodeWriter;
043
044 import java.io.ByteArrayOutputStream;
045 import java.io.IOException;
046 import java.io.OutputStream;
047 import java.util.ArrayList;
048 import java.util.Collection;
049 import java.util.HashMap;
050 import java.util.Iterator;
051 import java.util.TreeSet;
052
053 /**
054 * Partial implementation of a Preference node.
055 *
056 * @since 1.4
057 * @author Mark Wielaard (mark@klomp.org)
058 */
059 public abstract class AbstractPreferences extends Preferences {
060
061 // protected fields
062
063 /**
064 * Object used to lock this preference node. Any thread only locks nodes
065 * downwards when it has the lock on the current node. No method should
066 * synchronize on the lock of any of its parent nodes while holding the
067 * lock on the current node.
068 */
069 protected final Object lock = new Object();
070
071 /**
072 * Set to true in the contructor if the node did not exist in the backing
073 * store when this preference node object was created. Should be set in
074 * the constructor of a subclass. Defaults to false. Used to fire node
075 * changed events.
076 */
077 protected boolean newNode = false;
078
079 // private fields
080
081 /**
082 * The parent preferences node or null when this is the root node.
083 */
084 private final AbstractPreferences parent;
085
086 /**
087 * The name of this node.
088 * Only when this is a root node (parent == null) the name is empty.
089 * It has a maximum of 80 characters and cannot contain any '/' characters.
090 */
091 private final String name;
092
093 /** True when this node has been remove, false otherwise. */
094 private boolean removed = false;
095
096 /**
097 * Holds all the child names and nodes of this node that have been
098 * accessed by earlier <code>getChild()</code> or <code>childSpi()</code>
099 * 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 }