View Javadoc

1   /*
2    * Copyright (C) 2006  Tom Gibara
3    *
4    * This library is free software; you can redistribute it and/or
5    * modify it under the terms of the GNU Lesser General Public
6    * License as published by the Free Software Foundation; either
7    * version 2.1 of the License, or (at your option) any later version.
8    *
9    * This library is distributed in the hope that it will be useful,
10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12   * Lesser General Public License for more details.
13   *
14   * You should have received a copy of the GNU Lesser General Public
15   * License along with this library; if not, write to the Free Software
16   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
17   */
18  package com.tomgibara.pronto.config.source;
19  
20  import java.util.HashMap;
21  import java.util.HashSet;
22  import java.util.Iterator;
23  import java.util.Map;
24  import java.util.prefs.BackingStoreException;
25  import java.util.prefs.NodeChangeEvent;
26  import java.util.prefs.NodeChangeListener;
27  import java.util.prefs.PreferenceChangeEvent;
28  import java.util.prefs.PreferenceChangeListener;
29  import java.util.prefs.Preferences;
30  
31  import com.tomgibara.pronto.config.ConfigSource;
32  import com.tomgibara.pronto.config.ProntoConfigException;
33  import com.tomgibara.pronto.util.Arguments;
34  
35  /**
36   * A configuration source which draws its properties from a
37   * <code>Preferences</code> node. As per the <code>ConfigSource</code>
38   * interface, all properties are exposed as strings. The properties of
39   * descendant preference nodes are represented in the property map under the dot
40   * delimited combination of the descendant node names and the property name.
41   * 
42   * If the <code>startListening()</code> method is called, the source will
43   * monitor the preferences for changes until the <code>stopListening()</code>
44   * method is called. Note that, as per the Preference API specification, changes
45   * to preferences will typically need to be flushed before events that this
46   * object can observe will be reported.
47   * 
48   * This source is safe for multithreaded use.
49   * 
50   * @author Tom Gibara
51   */
52  
53  public class PreferencesConfigSource implements ConfigSource {
54  
55      /**
56       * A lock which synchronizes access to the properties, lastModified fields,
57       * and in addition, the listener's nodes map.
58       */
59      private final Object lock = new Object();
60  
61      /**
62       * The preferences node with which this object was constructed.
63       */
64      private final Preferences preferences;
65  
66      /**
67       * A listener for preference changes, this field is null iff this object is
68       * not listening for preference changes.
69       */
70      private Listener listener = null;
71  
72      /**
73       * Caches the last generated properties map.
74       */
75      private HashMap<String, String> properties = null;
76  
77      /**
78       * Records the time at which the properties field was last changed.
79       */
80      private long lastModified = 0L;
81  
82      /**
83       * Create a configuration source which draws its properties from the
84       * supplied preferences node.
85       * 
86       * @param preferences
87       *            a node containing configuration information, not null
88       */
89  
90      public PreferencesConfigSource(final Preferences preferences) {
91          Arguments.notNull(preferences, "preferences");
92          this.preferences = preferences;
93      }
94  
95      // config source methods
96  
97      /**
98       * The time at which the properties returned by this property source, were
99       * last known to have changed. This will be constant if this source is not
100      * listening to changes in the preferences.
101      * 
102      * @return the time at which the node's properties, descendent nodes, or
103      *         their properties were last known to have changed
104      * 
105      * @throws ProntoConfigException
106      *             if an exception occurs accessing the preferences backing
107      *             store.
108      */
109 
110     public long lastModified() throws ProntoConfigException {
111         synchronized (lock) {
112             update();
113             return lastModified;
114         }
115     }
116 
117     /**
118      * The properties obtained from the preferences node. This includes the
119      * properties of descendant nodes as described in the class documentation.
120      * 
121      * @return the properties obtained from the preferences node
122      * 
123      * @throws ProntoConfigException
124      *             if an exception occurs accessing the preferences backing
125      *             store.
126      */
127 
128     public Map<String, String> getProperties() throws ProntoConfigException {
129         synchronized (lock) {
130             update();
131             return properties;
132         }
133     }
134 
135     // accessors
136 
137     /**
138      * The preferences which are used to generate the properties for this
139      * source.
140      * 
141      * @return the preferences node with which this object was constructed
142      */
143 
144     public Preferences getPreferences() {
145         return preferences;
146     }
147 
148     // methods
149 
150     /**
151      * Causes this source to start listening for property changes and node
152      * changes to the preference node. If this source is already listening for
153      * such changes, then this method does nothing.
154      * 
155      * @throws ProntoConfigException
156      *             if an exception occurs accessing the preferences backing
157      *             store.
158      */
159 
160     public void startListening() throws ProntoConfigException {
161         synchronized (lock) {
162             if (listener != null) return; // already listening
163             listener = new Listener();
164             try {
165                 listener.start();
166             } catch (BackingStoreException e) {
167                 throw new ProntoConfigException(e);
168             }
169         }
170     }
171 
172     /**
173      * Causes this source to stop listening for changes to the property values.
174      * If this source is not listening for changes, then no action is performed.
175      * 
176      * @throws ProntoConfigException
177      *             if an exception occurs accessing the preferences backing
178      *             store.
179      */
180 
181     public void stopListening() throws ProntoConfigException {
182         synchronized (lock) {
183             if (listener == null) return; // already deaf
184             try {
185                 listener.stop();
186             } catch (BackingStoreException e) {
187                 throw new ProntoConfigException(e);
188             }
189             listener = null;
190         }
191     }
192 
193     // private utility methods
194 
195     /**
196      * Construct the properties if necessary, updating the lastModified time if
197      * we do so.
198      */
199 
200     // must be called with lock
201     private void update() throws ProntoConfigException {
202         if (properties == null) {
203             long now = System.currentTimeMillis();
204             try {
205                 properties = buildProperties("", preferences, new HashMap<String, String>());
206             } catch (BackingStoreException e) {
207                 throw new ProntoConfigException(e);
208             }
209             lastModified = now;
210         }
211     }
212 
213     /**
214      * Build a properties map by recursively visiting the child nodes of
215      * preferences.
216      * 
217      * @param prefix
218      *            the string which prefixes property names - used to build the
219      *            ancestor name chain, not null
220      * @param preferences
221      *            the node who's properties are to be added to the supplied
222      *            properties map
223      * @param properties
224      *            a map for accumulating the properties of nodes during
225      *            recursion
226      * @return the supplied properties map (for convenience)
227      * @throws BackingStoreException
228      *             if one occurs on any preference node
229      */
230 
231     private HashMap<String, String> buildProperties(final String prefix, final Preferences preferences,
232             final HashMap<String, String> properties) throws BackingStoreException {
233         // first add direct properties
234         String[] keys = preferences.keys();
235         for (String key : keys) {
236             String value = preferences.get(key, null);
237             if (value != null) properties.put(prefix + key, value);
238         }
239         // then add sub-properties
240         String[] children = preferences.childrenNames();
241         for (String name : children) {
242             Preferences child = preferences.node(name);
243             if (child != null) buildProperties(prefix + name + ".", child, properties);
244         }
245         return properties;
246     }
247 
248     // inner classes
249 
250     /**
251      * An adaptor class for listening to changes to preferences. Instances of
252      * this class recursively attach themselves as listeners to every descendant
253      * node. This is necessary to esnure that any change to any descendant node
254      * is observed.
255      */
256 
257     private class Listener implements PreferenceChangeListener, NodeChangeListener {
258 
259         // fields
260 
261         private final HashSet<Preferences> nodes = new HashSet<Preferences>();
262 
263         // lifetime methods
264 
265         // must be called while holding lock
266         void start() throws BackingStoreException {
267             addAllNodes(preferences);
268         }
269 
270         // must be called while holding lock
271         void stop() throws BackingStoreException {
272             clearNodes();
273         }
274 
275         // preference listening methods
276 
277         public void childAdded(final NodeChangeEvent evt) {
278             Preferences child = evt.getChild();
279             synchronized (lock) {
280                 try {
281                     if (!nodes.contains(child)) addNode(preferences);
282                 } catch (BackingStoreException e) {
283                     // TODO decide on the best thing to do with this exception
284                     e.printStackTrace();
285                 }
286                 properties = null;
287             }
288         }
289 
290         public void childRemoved(final NodeChangeEvent evt) {
291             Preferences child = evt.getChild();
292             synchronized (lock) {
293                 try {
294                     if (nodes.contains(child)) removeNode(preferences);
295                 } catch (BackingStoreException e) {
296                     // TODO decide on the best thing to do with this exception
297                     e.printStackTrace();
298                 }
299                 properties = null;
300             }
301         }
302 
303         public void preferenceChange(final PreferenceChangeEvent evt) {
304             synchronized (lock) {
305                 properties = null;
306             }
307         }
308 
309         // private utility methods
310 
311         private void addNode(final Preferences preferences) throws BackingStoreException {
312             if (preferences.nodeExists("")) {
313                 preferences.addNodeChangeListener(this);
314                 preferences.addPreferenceChangeListener(this);
315             }
316             nodes.add(preferences);
317         }
318 
319         private void removeNode(final Preferences preferences) throws BackingStoreException {
320             if (preferences.nodeExists("")) {
321                 preferences.removeNodeChangeListener(this);
322                 preferences.removePreferenceChangeListener(this);
323             }
324             nodes.remove(preferences);
325         }
326 
327         private void addAllNodes(final Preferences preferences) throws BackingStoreException {
328             addNode(preferences);
329             String[] children = preferences.childrenNames();
330             for (String name : children) {
331                 Preferences child = preferences.node(name);
332                 if (child != null) addAllNodes(child);
333             }
334         }
335 
336         private void clearNodes() {
337             for (Iterator<Preferences> i = nodes.iterator(); i.hasNext();) {
338                 Preferences preferences = i.next();
339                 preferences.removeNodeChangeListener(this);
340                 preferences.removePreferenceChangeListener(this);
341                 i.remove();
342             }
343         }
344 
345     }
346 
347 }