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