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