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