1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
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
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
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;
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;
184 try {
185 listener.stop();
186 } catch (BackingStoreException e) {
187 throw new ProntoConfigException(e);
188 }
189 listener = null;
190 }
191 }
192
193
194
195 /**
196 * Construct the properties if necessary, updating the lastModified time if
197 * we do so.
198 */
199
200
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
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
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
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
260
261 private final HashSet<Preferences> nodes = new HashSet<Preferences>();
262
263
264
265
266 void start() throws BackingStoreException {
267 addAllNodes(preferences);
268 }
269
270
271 void stop() throws BackingStoreException {
272 clearNodes();
273 }
274
275
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
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
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
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 }