| Config.java |
1 /**
2 * Distribution License:
3 * This is free software; you can redistribute it and/or modify it under
4 * the terms of the GNU Lesser General Public License, version 2.1 as published
5 * by the Free Software Foundation. This program is distributed in the hope
6 * that it will be useful, but WITHOUT ANY WARRANTY; without even the
7 * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
8 * See the GNU Lesser General Public License for more details.
9 *
10 * The License is available on the internet at:
11 * http://www.gnu.org/copyleft/llgpl.html
12 * or by writing to:
13 * Free Software Foundation, Inc.
14 * 59 Temple Place - Suite 330
15 * Boston, MA 02111-1307, USA
16 *
17 * Copyright: 2005
18 * The copyright to this program is held by it's authors.
19 *
20 * ID: $Id: Config.java 2099 2011-03-07 17:13:00Z dmsmith $
21 */
22 package org.crosswire.common.config;
23
24 import java.beans.PropertyChangeEvent;
25 import java.beans.PropertyChangeListener;
26 import java.beans.PropertyChangeSupport;
27 import java.io.IOException;
28 import java.net.URI;
29 import java.util.ArrayList;
30 import java.util.Iterator;
31 import java.util.List;
32 import java.util.ResourceBundle;
33
34 import org.crosswire.common.util.EventListenerList;
35 import org.crosswire.common.util.Logger;
36 import org.crosswire.common.util.LucidException;
37 import org.crosswire.common.util.NetUtil;
38 import org.crosswire.common.util.PropertyMap;
39 import org.crosswire.common.util.Reporter;
40 import org.crosswire.jsword.JSOtherMsg;
41 import org.jdom.Document;
42 import org.jdom.Element;
43
44 /**
45 * Config is the core part of the configuration system; it is simply a
46 * Collection of <code>Choice</code>s.
47 *
48 * <p>
49 * Config does the following things:
50 * <ul>
51 * <li>Provides a GUI independant API with which to create GUIs</li>
52 * <li>Stores a local store of settings</li>
53 * <li>Allows updates to the local store</li>
54 * </ul>
55 * </p>
56 *
57 * <p>
58 * Config does not attempt to make permanent copies of the config data because
59 * different apps may wish to store the data in different ways. Possible storage
60 * mechanisms include:
61 * <ul>
62 * <li>Properties Files</li>
63 * <li>Resource Objects (J2SE 1.4)</li>
64 * <li>Network Sockets (see Remote)</li>
65 * </ul>
66 * </p>
67 *
68 * The Config class stored the current Choices, and moves the data between the
69 * various places that it is stored. There are 4 storage areas:
70 * <ul>
71 * <li><b>Permanent:</b> This can be local file, a URI, or a remote server Data
72 * is stored here between invocations of the program.
73 * <li><b>Application:</b> This is the actual working copy of the data.
74 * <li><b>Screen:</b> This copy of the data is shown on screen whist a Config
75 * dialog box is showing.
76 * <li><b>Local:</b> This is required so that we can tell which bits of data
77 * have been changed in the screen data, and so that we can load data from disk
78 * to screen without involving the app.
79 * </ul>
80 *
81 * @see gnu.lgpl.License for license details.<br>
82 * The copyright to this program is held by it's authors.
83 * @author Joe Walker [joe at eireneh dot com]
84 */
85 public class Config implements Iterable<Choice> {
86 /**
87 * Config ctor
88 *
89 * @param title
90 * The name for dialog boxes and properties files
91 */
92 public Config(String title) {
93 this.title = title;
94 keys = new ArrayList<String>();
95 models = new ArrayList<Choice>();
96 local = new PropertyMap();
97 listenerList = new EventListenerList();
98 }
99
100 /**
101 */
102 public String getTitle() {
103 return title;
104 }
105
106 /**
107 * Add a key/model pairing
108 *
109 * @param model
110 * The Choice model to map to its key
111 */
112 public void add(Choice model) {
113 String key = model.getKey();
114 // log.debug("Adding key=" + key);
115
116 keys.add(key);
117 models.add(model);
118
119 String value = model.getString();
120 if (value == null) {
121 value = "";
122 log.info("key=" + key + " had a null value");
123 }
124
125 local.put(key, value);
126
127 fireChoiceAdded(key, model);
128 }
129
130 /**
131 * Add the set of configuration options specified in the xml file.
132 *
133 * @param xmlconfig
134 * The JDOM document to read.
135 * @param configResources
136 * contains the user level text for this config
137 */
138 public void add(Document xmlconfig, ResourceBundle configResources) {
139 // We are going to assume a DTD has validated the config file and
140 // just assume that everything is laid out properly.
141 Element root = xmlconfig.getRootElement();
142 Iterator<?> iter = root.getChildren().iterator();
143 while (iter.hasNext()) {
144 Element element = (Element) iter.next();
145 String key = element.getAttributeValue("key");
146
147 Exception ex = null;
148 try {
149 Choice choice = ChoiceFactory.getChoice(element, configResources);
150 if (!choice.isIgnored()) {
151 add(choice);
152 }
153 } catch (StartupException e) {
154 ex = e;
155 } catch (ClassNotFoundException e) {
156 ex = e;
157 } catch (IllegalAccessException e) {
158 ex = e;
159 } catch (InstantiationException e) {
160 ex = e;
161 }
162
163 if (ex != null) {
164 log.warn("Error creating config element, key=" + key, ex);
165 }
166 }
167 }
168
169 /**
170 * Remove a key/model pairing
171 *
172 * @param key
173 * The name to kill
174 */
175 public void remove(String key) {
176 Choice model = getChoice(key);
177 keys.remove(key);
178 models.remove(model);
179
180 // Leave the pair in local?
181 // local.put(key, value);
182
183 fireChoiceRemoved(key, model);
184 }
185
186 /**
187 * The set of Choice that we are controlling
188 *
189 * @return An enumeration over the choices
190 */
191 public Iterator<Choice> iterator() {
192 return models.iterator();
193 }
194
195 /**
196 * Get the Choice for a given key
197 *
198 * @return the requested choice
199 */
200 public Choice getChoice(String key) {
201 int index = keys.indexOf(key);
202 if (index == -1) {
203 return null;
204 }
205
206 return models.get(index);
207 }
208
209 /**
210 * The number of Choices
211 *
212 * @return The number of Choices
213 */
214 public int size() {
215 return keys.size();
216 }
217
218 /**
219 * Set a configuration Choice (by name) to a new value. This method is only
220 * of use to classes displaying config information
221 */
222 public void setLocal(String name, String value) {
223 assert name != null;
224 assert value != null;
225
226 local.put(name, value);
227 }
228
229 /**
230 * Get a configuration Choice (by name). This method is only of use to
231 * classes displaying config information
232 */
233 public String getLocal(String name) {
234 return local.get(name);
235 }
236
237 /**
238 * Take the data in the application and copy it to the local storage area.
239 */
240 public void applicationToLocal() {
241 for (String key : keys) {
242 Choice model = getChoice(key);
243 String value = model.getString();
244 local.put(key, value);
245 }
246 }
247
248 /**
249 * Take the data in the local storage area and copy it to the application.
250 */
251 public void localToApplication() {
252 for (String key : keys) {
253 Choice choice = getChoice(key);
254
255 String oldValue = choice.getString(); // never returns null
256 String newValue = local.get(key);
257
258 // The new value shouldn't really be blank - obviously this
259 // choice has just been added, substitute the default.
260 if ((newValue == null) || (newValue.length() == 0)) {
261 if ((oldValue == null) || (oldValue.length() == 0)) {
262 continue;
263 }
264 local.put(key, oldValue);
265 newValue = oldValue;
266 }
267
268 // If a value has not changed, we only call setString()
269 // if force==true or if a higher priority choice has
270 // changed.
271 if (!newValue.equals(oldValue)) {
272 log.info("Setting " + key + "=" + newValue + " (was " + oldValue + ")");
273 try {
274 choice.setString(newValue);
275 if (changeListeners != null) {
276 changeListeners.firePropertyChange(new PropertyChangeEvent(choice, choice.getKey(), oldValue, newValue));
277 }
278 } catch (LucidException ex) {
279 log.warn("Failure setting " + key + "=" + newValue, ex);
280 Reporter.informUser(this, new ConfigException(JSOtherMsg.lookupText("Failed to set option: {0}", choice.getFullPath()), ex));
281 }
282 }
283 }
284 }
285
286 /**
287 * Take the data stored permanently and copy it to the local storage area,
288 * using the specified stream
289 */
290 public void setProperties(PropertyMap prop) {
291 for (String key : prop.keySet()) {
292 String value = prop.get(key);
293
294 Choice model = getChoice(key);
295 // Only if a value was stored and it should be stored then we use
296 // it.
297 if (value != null && model != null && model.isSaveable()) {
298 local.put(key, value);
299 }
300 }
301 }
302
303 /**
304 * Take the data in the local storage area and store it permanently
305 */
306 public PropertyMap getProperties() {
307 PropertyMap prop = new PropertyMap();
308
309 for (String key : keys) {
310 String value = local.get(key);
311
312 Choice model = getChoice(key);
313 if (model.isSaveable()) {
314 prop.put(key, value);
315 } else {
316 prop.remove(key);
317 }
318 }
319
320 return prop;
321 }
322
323 /**
324 * Take the data stored permanently and copy it to the local storage area,
325 * using the configured storage area
326 *
327 * @throws IOException
328 */
329 public void permanentToLocal(URI uri) throws IOException {
330 setProperties(NetUtil.loadProperties(uri));
331 }
332
333 /**
334 * Take the data in the local storage area and store it permanently, using
335 * the configured storage area.
336 */
337 public void localToPermanent(URI uri) throws IOException {
338 NetUtil.storeProperties(getProperties(), uri, title);
339 }
340
341 /**
342 * What is the Path of this key
343 */
344 public static String getPath(String key) {
345 int lastDot = key.lastIndexOf('.');
346 if (lastDot == -1) {
347 throw new IllegalArgumentException("key=" + key + " does not contain a dot.");
348 }
349
350 return key.substring(0, lastDot);
351 }
352
353 /**
354 * What is the Path of this key
355 */
356 public static String getLeaf(String key) {
357 int lastDot = key.lastIndexOf('.');
358 if (lastDot == -1) {
359 throw new IllegalArgumentException("key=" + key + " does not contain a dot.");
360 }
361
362 return key.substring(lastDot + 1);
363 }
364
365 /**
366 * Add a PropertyChangeListener to the listener list. The listener is
367 * registered for all properties.
368 *
369 * @param listener
370 * The PropertyChangeListener to be added
371 */
372 public void addPropertyChangeListener(PropertyChangeListener listener) {
373 if (changeListeners == null) {
374 changeListeners = new PropertyChangeSupport(this);
375 }
376 changeListeners.addPropertyChangeListener(listener);
377 }
378
379 /**
380 * Remove a PropertyChangeListener from the listener list. This removes a
381 * PropertyChangeListener that was registered for all properties.
382 *
383 * @param listener
384 * The PropertyChangeListener to be removed
385 */
386 public void removePropertyChangeListener(PropertyChangeListener listener) {
387 if (changeListeners != null) {
388 changeListeners.removePropertyChangeListener(listener);
389 }
390 }
391
392 /**
393 * Add a PropertyChangeListener for a specific property. The listener will
394 * be invoked only when a call on firePropertyChange names that specific
395 * property.
396 *
397 * @param propertyName
398 * The name of the property to listen on.
399 * @param listener
400 * The PropertyChangeListener to be added
401 */
402
403 public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
404 if (changeListeners == null) {
405 changeListeners = new PropertyChangeSupport(this);
406 }
407 changeListeners.addPropertyChangeListener(propertyName, listener);
408 }
409
410 /**
411 * Remove a PropertyChangeListener for a specific property.
412 *
413 * @param propertyName
414 * The name of the property that was listened on.
415 * @param listener
416 * The PropertyChangeListener to be removed
417 */
418
419 public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
420 if (changeListeners != null) {
421 changeListeners.removePropertyChangeListener(propertyName, listener);
422 }
423 }
424
425 /**
426 * Add an Exception listener to the list of things wanting to know whenever
427 * we capture an Exception
428 */
429 public void addConfigListener(ConfigListener li) {
430 listenerList.add(ConfigListener.class, li);
431 }
432
433 /**
434 * Remove an Exception listener from the list of things wanting to know
435 * whenever we capture an Exception
436 */
437 public void removeConfigListener(ConfigListener li) {
438 listenerList.remove(ConfigListener.class, li);
439 }
440
441 /**
442 * A Choice got added
443 */
444 protected void fireChoiceAdded(String key, Choice model) {
445 // Guaranteed to return a non-null array
446 Object[] listeners = listenerList.getListenerList();
447
448 // Process the listeners last to first, notifying
449 // those that are interested in this event
450 ConfigEvent ev = null;
451 for (int i = listeners.length - 2; i >= 0; i -= 2) {
452 if (listeners[i] == ConfigListener.class) {
453 if (ev == null) {
454 ev = new ConfigEvent(this, key, model);
455 }
456
457 ((ConfigListener) listeners[i + 1]).choiceAdded(ev);
458 }
459 }
460 }
461
462 /**
463 * A Choice got added
464 */
465 protected void fireChoiceRemoved(String key, Choice model) {
466 // Guaranteed to return a non-null array
467 Object[] listeners = listenerList.getListenerList();
468
469 // Process the listeners last to first, notifying
470 // those that are interested in this event
471 ConfigEvent ev = null;
472 for (int i = listeners.length - 2; i >= 0; i -= 2) {
473 if (listeners[i] == ConfigListener.class) {
474 if (ev == null) {
475 ev = new ConfigEvent(this, key, model);
476 }
477
478 ((ConfigListener) listeners[i + 1]).choiceRemoved(ev);
479 }
480 }
481 }
482
483 /**
484 * The name for dialog boxes and properties files
485 */
486 protected String title;
487
488 /**
489 * The array that stores the keys
490 */
491 protected List<String> keys = new ArrayList<String>();
492
493 /**
494 * The array that stores the models
495 */
496 protected List<Choice> models = new ArrayList<Choice>();
497
498 /**
499 * The set of local values
500 */
501 protected PropertyMap local;
502
503 /**
504 * The set of property change listeners.
505 */
506 protected PropertyChangeSupport changeListeners;
507
508 /**
509 * The list of listeners
510 */
511 protected EventListenerList listenerList = new EventListenerList();
512
513 /**
514 * The log stream
515 */
516 private static final Logger log = Logger.getLogger(Config.class);
517
518 }
519