blob: ba7420c5cfacd1250ecd3664af38e16877b8a53f [file] [log] [blame]
Carsten Ziegeler122a4042007-07-30 12:57:54 +00001/*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
Carsten Ziegeler6c673232007-07-30 13:03:55 +000019package org.apache.felix.prefs;
Carsten Ziegeler122a4042007-07-30 12:57:54 +000020
21import java.io.UnsupportedEncodingException;
Carsten Ziegeler50161ac2007-12-28 15:22:45 +000022import java.util.*;
Carsten Ziegeler122a4042007-07-30 12:57:54 +000023
Carsten Ziegeler2234ede2007-07-30 13:05:50 +000024import org.apache.commons.codec.binary.Base64;
Carsten Ziegeler122a4042007-07-30 12:57:54 +000025import org.osgi.service.prefs.BackingStoreException;
26import org.osgi.service.prefs.Preferences;
27
28/**
29 * This is an implementation of the preferences.
30 *
31 * The access to the preferences is synchronized on the instance
32 * by making (nearly) all public methods synchronized. This avoids the
33 * heavy management of a separate read/write lock. Such a lock
34 * is too heavy for the simple operations preferences support.
35 * The various getXX and putXX methods are not synchronized as they
36 * all use the get/put methods which are synchronized.
37 */
38public class PreferencesImpl implements Preferences {
39
40 /** The properties. */
41 protected final Map properties = new HashMap();
42
43 /** Has this node been removed? */
44 protected boolean valid = true;
45
46 /** The parent. */
47 protected final PreferencesImpl parent;
48
49 /** The child nodes. */
50 protected final Map children = new HashMap();
51
52 /** The name of the properties. */
53 protected final String name;
54
55 /** The description for this preferences. */
56 protected final PreferencesDescription description;
57
58 /** The backing store manager. */
59 protected final BackingStoreManager storeManager;
60
61 /** The change set keeps track of all changes. */
62 protected final ChangeSet changeSet = new ChangeSet();
63
64 /**
65 * Construct the root node of the tree.
66 * @param d The unique description.
67 * @param storeManager The backing store.
68 */
69 public PreferencesImpl(PreferencesDescription d, BackingStoreManager storeManager) {
70 this.parent = null;
71 this.name = "";
72 this.description = d;
73 this.storeManager = storeManager;
74 }
75
76 /**
77 * Construct a child node.
78 * @param p The parent node.
79 * @param name The node name
80 */
81 public PreferencesImpl(PreferencesImpl p, String name) {
82 this.parent = p;
83 this.name = name;
84 this.description = p.description;
85 this.storeManager = p.storeManager;
86 }
87
88 /**
89 * Return the change set.
90 */
91 public ChangeSet getChangeSet() {
92 return this.changeSet;
93 }
94
95 /**
96 * Return the preferences description.
Carsten Ziegeler50161ac2007-12-28 15:22:45 +000097 * @return The preferences description.
Carsten Ziegeler122a4042007-07-30 12:57:54 +000098 */
99 public PreferencesDescription getDescription() {
100 return this.description;
101 }
102
103 /**
104 * Get the root preferences.
105 */
106 public PreferencesImpl getRoot() {
107 PreferencesImpl root = this;
108 while ( root.parent != null ) {
109 root = root.parent;
110 }
111 return root;
112 }
113
114 /**
115 * Return all children or an empty collection.
Carsten Ziegeler50161ac2007-12-28 15:22:45 +0000116 * @return A collection containing the children.
Carsten Ziegeler122a4042007-07-30 12:57:54 +0000117 */
118 public Collection getChildren() {
119 return this.children.values();
120 }
121
122 /**
123 * Return the properties set.
124 */
125 public Map getProperties() {
126 return this.properties;
127 }
128
129 /**
130 * Return the backing store manager.
131 */
132 public BackingStoreManager getBackingStoreManager() {
133 return this.storeManager;
134 }
135
136 /**
137 * Check if this node is still valid.
138 * It gets invalid if it has been removed.
139 */
140 protected void checkValidity() throws IllegalStateException {
141 if ( !this.valid ) {
142 throw new IllegalStateException("The preferences node has been removed.");
143 }
144 }
145
146 /**
147 * The key is not allowed to be null.
148 */
149 protected void checkKey(String key) throws NullPointerException {
150 if ( key == null ) {
151 throw new NullPointerException("Key must not be null.");
152 }
153 }
154
155 /**
156 * The value is not allowed to be null.
157 */
158 protected void checkValue(Object value) throws NullPointerException {
159 if ( value == null ) {
160 throw new NullPointerException("Value must not be null.");
161 }
162 }
163
Marcel Offermansc46a6482008-06-23 19:51:37 +0000164 public synchronized boolean isValid() {
165 return this.valid;
166 }
167
Carsten Ziegeler122a4042007-07-30 12:57:54 +0000168 /**
169 * @see org.osgi.service.prefs.Preferences#put(java.lang.String, java.lang.String)
170 */
171 public synchronized void put(String key, String value) {
172 this.checkKey(key);
173 this.checkValue(value);
174
175 this.checkValidity();
176
177 this.properties.put(key, value);
178 this.changeSet.propertyChanged(key);
179 }
180
181 /**
182 * @see org.osgi.service.prefs.Preferences#get(java.lang.String, java.lang.String)
183 */
184 public synchronized String get(String key, String def) {
185 this.checkValidity();
186 String value = (String) this.properties.get(key);
187 if ( value == null ) {
188 value = def;
189 }
190 return value;
191 }
192
193 /**
194 * @see org.osgi.service.prefs.Preferences#remove(java.lang.String)
195 */
196 public synchronized void remove(String key) {
197 this.checkKey(key);
198 this.checkValidity();
199
200 this.properties.remove(key);
201 this.changeSet.propertyRemoved(key);
202 }
203
204 /**
205 * @see org.osgi.service.prefs.Preferences#clear()
206 */
207 public synchronized void clear() throws BackingStoreException {
208 this.checkValidity();
209
210 final Iterator i = this.properties.keySet().iterator();
211 while ( i.hasNext() ) {
212 final String key = (String)i.next();
213 this.changeSet.propertyRemoved(key);
214 }
215 this.properties.clear();
216 }
217
218 /**
219 * @see org.osgi.service.prefs.Preferences#putInt(java.lang.String, int)
220 */
221 public void putInt(String key, int value) {
222 this.put(key, String.valueOf(value));
223 }
224
225 /**
226 * @see org.osgi.service.prefs.Preferences#getInt(java.lang.String, int)
227 */
228 public int getInt(String key, int def) {
229 int result = def;
230 final String value = this.get(key, null);
231 if ( value != null ) {
232 try {
233 result = Integer.parseInt(value);
234 } catch (NumberFormatException ignore) {
235 // return the default value
236 }
237 }
238 return result;
239 }
240
241 /**
242 * @see org.osgi.service.prefs.Preferences#putLong(java.lang.String, long)
243 */
244 public void putLong(String key, long value) {
245 this.put(key, String.valueOf(value));
246 }
247
248 /**
249 * @see org.osgi.service.prefs.Preferences#getLong(java.lang.String, long)
250 */
251 public long getLong(String key, long def) {
252 long result = def;
253 final String value = this.get(key, null);
254 if ( value != null ) {
255 try {
256 result = Long.parseLong(value);
257 } catch (NumberFormatException ignore) {
258 // return the default value
259 }
260 }
261 return result;
262 }
263
264 /**
265 * @see org.osgi.service.prefs.Preferences#putBoolean(java.lang.String, boolean)
266 */
267 public void putBoolean(String key, boolean value) {
268 this.put(key, String.valueOf(value));
269 }
270
271 /**
272 * @see org.osgi.service.prefs.Preferences#getBoolean(java.lang.String, boolean)
273 */
274 public boolean getBoolean(String key, boolean def) {
275 boolean result = def;
276 final String value = this.get(key, null);
277 if ( value != null ) {
278 if ( value.equalsIgnoreCase("true") ) {
279 result = true;
280 } else if ( value.equalsIgnoreCase("false") ) {
281 result = false;
282 }
283 }
284 return result;
285 }
286
287 /**
288 * @see org.osgi.service.prefs.Preferences#putFloat(java.lang.String, float)
289 */
290 public void putFloat(String key, float value) {
291 this.put(key, String.valueOf(value));
292 }
293
294 /**
295 * @see org.osgi.service.prefs.Preferences#getFloat(java.lang.String, float)
296 */
297 public float getFloat(String key, float def) {
298 float result = def;
299 final String value = this.get(key, null);
300 if ( value != null ) {
301 try {
302 result = Float.parseFloat(value);
303 } catch (NumberFormatException ignore) {
304 // return the default value
305 }
306 }
307 return result;
308 }
309
310 /**
311 * @see org.osgi.service.prefs.Preferences#putDouble(java.lang.String, double)
312 */
313 public void putDouble(String key, double value) {
314 this.put(key, String.valueOf(value));
315 }
316
317 /**
318 * @see org.osgi.service.prefs.Preferences#getDouble(java.lang.String, double)
319 */
320 public double getDouble(String key, double def) {
321 double result = def;
322 final String value = this.get(key, null);
323 if ( value != null ) {
324 try {
325 result = Double.parseDouble(value);
326 } catch (NumberFormatException ignore) {
327 // return the default value
328 }
329 }
330 return result;
331 }
332
333 /**
334 * @see org.osgi.service.prefs.Preferences#putByteArray(java.lang.String, byte[])
335 */
336 public void putByteArray(String key, byte[] value) {
337 this.checkKey(key);
338 this.checkValue(value);
339 try {
Carsten Ziegeler4d4ed462009-05-14 06:58:16 +0000340 this.put(key, new String(Base64.encodeBase64(value), "utf-8"));
Carsten Ziegeler122a4042007-07-30 12:57:54 +0000341 } catch (UnsupportedEncodingException ignore) {
342 // utf-8 is always available
343 }
344 }
345
346 /**
347 * @see org.osgi.service.prefs.Preferences#getByteArray(java.lang.String, byte[])
348 */
349 public byte[] getByteArray(String key, byte[] def) {
350 byte[] result = def;
351 String value = this.get(key, null);
352 if ( value != null ) {
353 try {
354 result = Base64.decodeBase64(value.getBytes("utf-8"));
355 } catch (UnsupportedEncodingException ignore) {
356 // utf-8 is always available
357 }
358 }
359 return result;
360 }
361
362 /**
363 * @see org.osgi.service.prefs.Preferences#keys()
364 */
365 public synchronized String[] keys() throws BackingStoreException {
366 this.sync();
367 final Set keys = this.properties.keySet();
368 return (String[])keys.toArray(new String[keys.size()]);
369 }
370
371 /**
372 * @see org.osgi.service.prefs.Preferences#childrenNames()
373 */
374 public synchronized String[] childrenNames() throws BackingStoreException {
375 this.sync();
376 final Set names = this.children.keySet();
377 return (String[])names.toArray(new String[names.size()]);
378 }
379
380 /**
381 * @see org.osgi.service.prefs.Preferences#parent()
382 */
383 public Preferences parent() {
384 this.checkValidity();
385 return this.parent;
386 }
387
388 /**
389 * We do not synchronize this method to avoid dead locks as this
390 * method might call another preferences object in the hierarchy.
391 * @see org.osgi.service.prefs.Preferences#node(java.lang.String)
392 */
393 public Preferences node(String pathName) {
394 if ( pathName == null ) {
395 throw new NullPointerException("Path must not be null.");
396 }
397 PreferencesImpl executingNode= this;
398 synchronized ( this ) {
399 this.checkValidity();
400 if ( pathName.length() == 0 ) {
401 return this;
402 }
403 if ( pathName.startsWith("/") && this.parent != null ) {
404 executingNode = this.getRoot();
405 }
406 if ( pathName.startsWith("/") ) {
407 pathName = pathName.substring(1);
408 }
409 }
410 return executingNode.getNode(pathName, true, true);
411 }
412
413 /**
414 * Get or create the node.
415 * If the node already exists, it's just returned. If not
416 * it is created.
417 * @param pathName
Carsten Ziegeler50161ac2007-12-28 15:22:45 +0000418 * @return The preferences impl for the path.
Carsten Ziegeler122a4042007-07-30 12:57:54 +0000419 */
420 public PreferencesImpl getOrCreateNode(String pathName) {
421 if ( pathName == null ) {
422 throw new NullPointerException("Path must not be null.");
423 }
424 PreferencesImpl executingNode= this;
425 if ( pathName.length() == 0 ) {
426 return this;
427 }
428 if ( pathName.startsWith("/") && this.parent != null ) {
429 executingNode = this.getRoot();
430 }
431 if ( pathName.startsWith("/") ) {
432 pathName = pathName.substring(1);
433 }
434 return executingNode.getNode(pathName, false, true);
435 }
436
437 /**
438 * Get a relative node.
439 * @param path
440 * @return
441 */
442 protected PreferencesImpl getNode(String path, boolean saveNewlyCreatedNode, boolean create) {
443 if ( path.startsWith("/") ) {
444 throw new IllegalArgumentException("Path must not contained consecutive slashes");
445 }
446 if ( path.length() == 0 ) {
447 return this;
448 }
449 synchronized ( this ) {
450 this.checkValidity();
451
452 String subPath = null;
453 int pos = path.indexOf('/');
454 if ( pos != -1 ) {
455 subPath = path.substring(pos+1);
456 path = path.substring(0, pos);
457 }
458 boolean save = false;
459 PreferencesImpl child = (PreferencesImpl) this.children.get(path);
460 if ( child == null ) {
461 if ( !create ) {
462 return null;
463 }
464 child = new PreferencesImpl(this, path);
465 this.children.put(path, child);
466 this.changeSet.childAdded(path);
467 if ( saveNewlyCreatedNode ) {
468 save = true;
469 }
470 saveNewlyCreatedNode = false;
471 }
472 final PreferencesImpl result;
473 if ( subPath == null ) {
474 result = child;
475 } else {
476 result = child.getNode(subPath, saveNewlyCreatedNode, create);
477 }
478 if ( save ) {
479 try {
480 result.flush();
481 } catch (BackingStoreException ignore) {
482 // we ignore this for now
483 }
484 }
485 return result;
486 }
487 }
488
489 /**
490 * We do not synchronize this method to avoid dead locks as this
491 * method might call another preferences object in the hierarchy.
492 * @see org.osgi.service.prefs.Preferences#nodeExists(java.lang.String)
493 */
494 public boolean nodeExists(String pathName) throws BackingStoreException {
495 if ( pathName == null ) {
496 throw new NullPointerException("Path must not be null.");
497 }
498 if ( pathName.length() == 0 ) {
499 return this.valid;
500 }
501 PreferencesImpl node = this;
502 synchronized ( this ) {
503 this.checkValidity();
504 if ( pathName.startsWith("/") && this.parent != null ) {
505 node = this.getRoot();
506 }
507 if ( pathName.startsWith("/") ) {
508 pathName = pathName.substring(1);
509 }
510 }
511 final Preferences searchNode = node.getNode(pathName, false, false);
512 return searchNode != null;
513 }
514
515 /**
516 * @see org.osgi.service.prefs.Preferences#removeNode()
517 */
518 public void removeNode() throws BackingStoreException {
519 this.checkValidity();
520 this.safelyRemoveNode();
521
522 if ( this.parent != null ) {
523 this.parent.removeChild(this);
524 }
525 }
526
527 /**
528 * Safely remove a node by resetting all properties and calling
529 * this method on all children recursively.
530 */
531 protected void safelyRemoveNode() {
532 if ( this.valid ) {
533 Collection c = null;
534 synchronized ( this ) {
535 this.valid = false;
536 this.properties.clear();
537 c = new ArrayList(this.children.values());
538 this.children.clear();
539 }
540 final Iterator i = c.iterator();
541 while ( i.hasNext() ) {
542 final PreferencesImpl child = (PreferencesImpl) i.next();
543 child.safelyRemoveNode();
544 }
545 }
546 }
547
548 protected synchronized void removeChild(PreferencesImpl child) {
549 this.children.remove(child.name());
550 this.changeSet.childRemoved(child.name());
551 }
552
553 /**
554 * @see org.osgi.service.prefs.Preferences#name()
555 */
556 public String name() {
557 return this.name;
558 }
559
560 /**
561 * @see org.osgi.service.prefs.Preferences#absolutePath()
562 */
563 public String absolutePath() {
564 if (this.parent == null) {
565 return "/";
Carsten Ziegeler122a4042007-07-30 12:57:54 +0000566 }
Carsten Ziegeler50161ac2007-12-28 15:22:45 +0000567 final String parentPath = this.parent.absolutePath();
568 if ( parentPath.length() == 1 ) {
569 return parentPath + this.name;
570 }
571 return parentPath + '/' + this.name;
Carsten Ziegeler122a4042007-07-30 12:57:54 +0000572 }
573
574 /**
575 * @see org.osgi.service.prefs.Preferences#flush()
576 */
577 public synchronized void flush() throws BackingStoreException {
578 this.checkValidity();
579 this.storeManager.getStore().store(this);
580 this.changeSet.clear();
581 }
582
583 /**
584 * @see org.osgi.service.prefs.Preferences#sync()
585 */
586 public synchronized void sync() throws BackingStoreException {
587 this.checkValidity();
588 this.storeManager.getStore().update(this);
589 this.storeManager.getStore().store(this);
590 }
591
592 /**
593 * Update from the preferences impl.
594 * @param impl
595 */
596 public void update(PreferencesImpl impl) {
597 final Iterator i = impl.properties.entrySet().iterator();
598 while ( i.hasNext() ) {
599 final Map.Entry entry = (Map.Entry)i.next();
600 if ( !this.properties.containsKey(entry.getKey()) ) {
601 this.properties.put(entry.getKey(), entry.getValue());
602 }
603 }
604 final Iterator cI = impl.children.entrySet().iterator();
605 while ( cI.hasNext() ) {
606 final Map.Entry entry = (Map.Entry)cI.next();
607 final String name = entry.getKey().toString();
608 final PreferencesImpl child = (PreferencesImpl)entry.getValue();
609 if ( !this.children.containsKey(name) ) {
610 // create node
611 this.node(name);
612 }
613 ((PreferencesImpl)this.children.get(name)).update(child);
614 }
615 }
616
617 /***
618 * Apply the changes done to the passed preferences object.
619 * @param prefs
620 */
621 public void applyChanges(PreferencesImpl prefs) {
622 final ChangeSet changeSet = prefs.getChangeSet();
623 if ( changeSet.hasChanges ) {
624 this.changeSet.importChanges(prefs.changeSet);
625 Iterator i;
626 // remove properties
627 i = changeSet.removedProperties.iterator();
628 while ( i.hasNext() ) {
629 this.properties.remove(i.next());
630 }
631 // set/update properties
632 i = changeSet.changedProperties.iterator();
633 while ( i.hasNext() ) {
634 final String key = (String)i.next();
635 this.properties.put(key, prefs.properties.get(key));
636 }
637 // remove children
638 i = changeSet.removedChildren.iterator();
639 while ( i.hasNext() ) {
640 final String name = (String)i.next();
641 this.children.remove(name);
642 }
643 // added childs are processed in the next loop
644 }
645 final Iterator cI = prefs.getChildren().iterator();
646 while ( cI.hasNext() ) {
647 final PreferencesImpl current = (PreferencesImpl)cI.next();
648 final PreferencesImpl child = this.getOrCreateNode(current.name());
649 child.applyChanges(current);
650 }
651 }
Marcel Offermansc46a6482008-06-23 19:51:37 +0000652}