Source code: com/eireneh/config/Config.java
1
2 package com.eireneh.config;
3
4 import java.io.*;
5 import java.util.*;
6 import java.net.*;
7
8 import com.eireneh.config.choices.*;
9 import com.eireneh.util.*;
10
11 /**
12 * Config is the core part of the configuration system; it is simply a
13 * Collection of <code>Choice</code>s.
14 *
15 * <p>Config does the following things:<ul>
16 * <li>Provides a GUI independant API with which to create GUIs</li>
17 * <li>Stores a local store of settings</li>
18 * <li>Allows updates to the local store</li>
19 * </ul></p>
20 *
21 * <p>Config does not attempt to make permanent copies of the config data
22 * because different apps may wish to store the data in different ways.
23 * Possible storage mechanisms include:<ul>
24 * <li>Properties Files</li>
25 * <li>Resource Objects (Merlin, JDK 1.4)</li>
26 * <li>Network Sockets (see Remote)</li>
27 * </ul></p>
28 *
29 * The Config class stored the current Choices, and moves the data
30 * between the various places that it is stored. There are 4 storage
31 * areas:<ul>
32 * <li><b>Permanent:</b> This can be local file, a URL, or a remote server
33 * Data is stored here between invocations of the program.
34 * <li><b>Application:</b> This is the actual working copy of the data.
35 * <li><b>Screen:</b> This copy of the data is shown on screen whist a
36 * Config dialog box is showing.
37 * <li><b>Local:</b> This is required so that we can tell which bits of
38 * data have been changed in the screen data, and so that we can
39 * load data from disk to screen without involving the app.
40 * </ul>
41 * TODO: Questions that fail on load - ask
42 * TODO: I18N
43 *
44 * <table border='1' cellPadding='3' cellSpacing='0' width="100%">
45 * <tr><td bgColor='white'class='TableRowColor'><font size='-7'>
46 * Distribution Licence:<br />
47 * Project B is free software; you can redistribute it
48 * and/or modify it under the terms of the GNU General Public License,
49 * version 2 as published by the Free Software Foundation.<br />
50 * This program is distributed in the hope that it will be useful,
51 * but WITHOUT ANY WARRANTY; without even the implied warranty of
52 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
53 * General Public License for more details.<br />
54 * The License is available on the internet
55 * <a href='http://www.gnu.org/copyleft/gpl.html'>here</a>, by writing to
56 * <i>Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston,
57 * MA 02111-1307, USA</i>, Or locally at the Licence link below.<br />
58 * The copyright to this program is held by it's authors.
59 * </font></td></tr></table>
60 * @see <a href='http://www.eireneh.com/servlets/Web'>Project B Home</a>
61 * @see docs.Licence
62 * @author Joe Walker
63 */
64 public class Config implements Serializable
65 {
66 /**
67 * Ensure that we can not be instansiated
68 * @param title The name for dialog boxes and properties files
69 */
70 public Config(String title)
71 {
72 this.title = title;
73 }
74
75 /**
76 */
77 public String getTitle()
78 {
79 return title;
80 }
81
82 /**
83 * Add a key/model pairing
84 * @param key The new name
85 * @param model The Field model to map to the key
86 */
87 public void add(String key, Choice model)
88 {
89 log.config("Adding key="+key);
90
91 keys.addElement(key);
92 models.addElement(model);
93
94 String value = model.getString();
95 if (value == null)
96 {
97 value = "";
98 log.config("key="+key+" has a null value");
99 }
100
101 local.put(key, value);
102
103 fireChoiceAdded(key, model);
104 }
105
106 /**
107 * Remove a key/model pairing
108 * @param key The name to kill
109 */
110 public void remove(String key)
111 {
112 Choice model = getChoice(key);
113 keys.removeElement(key);
114 models.removeElement(model);
115
116 // Leave the pair in local?
117 //local.put(key, value);
118
119 fireChoiceRemoved(key, model);
120 }
121
122 /**
123 * The set of Choice Names that we are controlling
124 * @return An enumeration over the keys
125 */
126 public Enumeration getPaths()
127 {
128 Vector paths = new Vector();
129
130 Enumeration en = keys.elements();
131 while (en.hasMoreElements())
132 {
133 String key = (String) en.nextElement();
134 String path = getPath(key);
135
136 if (!paths.contains(path))
137 paths.addElement(path);
138 }
139
140 return paths.elements();
141 }
142
143 /**
144 * The set of Choice Names that we are controlling
145 * @return An enumeration over the keys
146 */
147 public Enumeration getNames()
148 {
149 return keys.elements();
150 }
151
152 /**
153 * Step through the keys
154 * @return an enum of the keys
155 */
156 public Choice getChoice(String key)
157 {
158 int index = keys.indexOf(key);
159 if (index == -1)
160 return null;
161
162 return (Choice) models.elementAt(index);
163 }
164
165 /**
166 * The number of Choices
167 * @return The number of Choices
168 */
169 public int size()
170 {
171 return keys.size();
172 }
173
174 /**
175 * Set a configuration Choice (by name) to a new value. This method
176 * is only of use to classes displaying config information
177 */
178 public void setLocal(String name, String value)
179 {
180 local.put(name, value);
181 }
182
183 /**
184 * Get a configuration Choice (by name). This method
185 * is only of use to classes displaying config information
186 */
187 public String getLocal(String name)
188 {
189 return local.getProperty(name);
190 }
191
192 /**
193 * Take the data in the application and copy it to the local
194 * storage area.
195 */
196 public void applicationToLocal()
197 {
198 Enumeration en = keys.elements();
199 while (en.hasMoreElements())
200 {
201 String key = (String) en.nextElement();
202
203 try
204 {
205 Choice model = getChoice(key);
206 String value = model.getString();
207 local.put(key, value);
208 }
209 catch (Throwable ex)
210 {
211 log.warning("Failure with setting "+key);
212 Reporter.informUser(this, ex);
213 }
214 }
215 }
216
217 /**
218 * Take the data in the local storage area and copy it to the
219 * application.
220 * @param force If the new value is the same as the current do we set anyway
221 */
222 public void localToApplication(boolean force)
223 {
224 int highest_change = Choice.PRIORITY_LOWEST;
225
226 if (force)
227 log.config("Force=true, all changes will propogate regardless");
228
229 for (int priority=Choice.PRIORITY_SYSTEM; priority>=Choice.PRIORITY_LOWEST; priority--)
230 {
231 log.config("Settings for priority level="+priority);
232
233 Enumeration en = keys.elements();
234 while (en.hasMoreElements())
235 {
236 String key = (String) en.nextElement();
237 Choice model = getChoice(key);
238
239 if (model.priority() == priority)
240 {
241 String old_value = model.getString();
242 String new_value = local.getProperty(key);
243
244 // The new value shouldn't really be blank - obviously this
245 // choice has just been added, substitute the default.
246 if (new_value == null)
247 {
248 local.put(key, old_value);
249 new_value = old_value;
250 }
251
252 try
253 {
254 // If a value has not changed, we only call setString()
255 // if force==true or if a higher priority choice has
256 // changed.
257 if (force ||
258 priority < highest_change ||
259 !new_value.equals(old_value))
260 {
261 log.config("Setting "+key+"="+new_value+" (was "+old_value+")");
262 model.setString(new_value);
263
264 if (priority > highest_change)
265 {
266 highest_change = priority;
267
268 if (!force)
269 log.config("Change at level "+highest_change+", all changes will propogate regardless");
270 }
271 }
272 }
273 catch (Throwable ex)
274 {
275 log.warning("Failure with "+key+"="+new_value);
276 Reporter.informUser(this, ex);
277 }
278 }
279 }
280 }
281 }
282
283 /**
284 * Take the data stored permanetly and copy it to the local
285 * storage area, using the specified stream
286 */
287 public void setProperties(Properties prop)
288 {
289 Enumeration en = prop.keys();
290 while (en.hasMoreElements())
291 {
292 String key = (String) en.nextElement();
293 String value = prop.getProperty(key);
294
295 if (value != null)
296 local.put(key, value);
297 }
298 }
299
300 /**
301 * Take the data in the local storage area and store it permanently,
302 * using the specified stream.
303 * @param out an output stream.
304 */
305 public Properties getProperties()
306 {
307 Properties prop = new Properties();
308
309 Enumeration en = keys.elements();
310 while (en.hasMoreElements())
311 {
312 String key = (String) en.nextElement();
313 String value = local.getProperty(key);
314
315 Choice model = getChoice(key);
316 if (model.isSaveable())
317 prop.put(key, value);
318 else
319 prop.remove(key);
320 }
321
322 return prop;
323 }
324
325 /**
326 * Take the data stored permanently and copy it to the local
327 * storage area, using the configured storage area
328 */
329 public void permanentToLocal(URL url) throws IOException
330 {
331 InputStream in = url.openStream();
332
333 Properties prop = new Properties();
334 PropertiesUtil.load(prop, in);
335
336 setProperties(prop);
337 }
338
339 /**
340 * Take the data in the local storage area and store it permanently,
341 * using the configured storage area.
342 */
343 public void localToPermanent(URL url) throws IOException
344 {
345 File file = new File(url.getFile());
346 OutputStream out = new FileOutputStream(file);
347
348 // Send our updates
349 PropertiesUtil.save(getProperties(), out, title);
350 }
351
352 /**
353 * Take the data stored permanently and copy it to the local
354 * storage area, using the configured storage area
355 */
356 public void permanentToLocal(String host, int port) throws IOException
357 {
358 try
359 {
360 Socket sock = new Socket(host, port);
361 InputStream in = sock.getInputStream();
362 ObjectInputStream sin = new ObjectInputStream(in);
363 Config config = (Config) sin.readObject();
364
365 Properties prop = new Properties();
366 PropertiesUtil.load(prop, in);
367
368 // Politeness: Send nothing to the server in return.
369 PropertiesUtil.save(new Properties(), sock.getOutputStream(), "Dump");
370 sock.close();
371
372 setProperties(prop);
373 }
374 catch (ClassNotFoundException ex)
375 {
376 throw new IOException("Serialization Error: "+ex);
377 }
378 }
379
380 /**
381 * Take the data in the local storage area and store it permanently,
382 * using the configured storage area.
383 */
384 public void localToPermanent(String host, int port) throws IOException
385 {
386 try
387 {
388 Socket sock = new Socket(host, port);
389 OutputStream out = sock.getOutputStream();
390
391 // Politeness: Read the stuff the server sends to us, but ignore it.
392 InputStream in = sock.getInputStream();
393
394 ObjectInputStream sin = new ObjectInputStream(in);
395 Config config = (Config) sin.readObject();
396
397 PropertiesUtil.load(new Properties(), in);
398
399 // Send our updates
400 PropertiesUtil.save(getProperties(), out, title);
401
402 sock.close();
403 }
404 catch (ClassNotFoundException ex)
405 {
406 throw new IOException("Serialization Error");
407 }
408 }
409
410 /**
411 * What is the Path of this key
412 */
413 public static String getPath(String key)
414 {
415 int last_dot = key.lastIndexOf('.');
416 if (last_dot == -1)
417 throw new IllegalArgumentException("key="+key+" does not contain a dot.");
418
419 return key.substring(0, last_dot);
420 }
421
422 /**
423 * What is the Path of this key
424 */
425 public static String getLeaf(String key)
426 {
427 int last_dot = key.lastIndexOf('.');
428 if (last_dot == -1)
429 throw new IllegalArgumentException("key="+key+" does not contain a dot.");
430
431 return key.substring(last_dot+1);
432 }
433
434 /**
435 * Add an Exception listener to the list of things wanting
436 * to know whenever we capture an Exception
437 */
438 public void addConfigListener(ConfigListener li)
439 {
440 listener_list.add(ConfigListener.class, li);
441 }
442
443 /**
444 * Remove an Exception listener from the list of things wanting
445 * to know whenever we capture an Exception
446 */
447 public void removeConfigListener(ConfigListener li)
448 {
449 listener_list.remove(ConfigListener.class, li);
450 }
451
452 /**
453 * A Choice got added
454 */
455 protected void fireChoiceAdded(String key, Choice model)
456 {
457 // Guaranteed to return a non-null array
458 Object[] listeners = listener_list.getListenerList();
459
460 // Process the listeners last to first, notifying
461 // those that are interested in this event
462 ConfigEvent ev = null;
463 for (int i=listeners.length-2; i>=0; i-=2)
464 {
465 if (listeners[i] == ConfigListener.class)
466 {
467 if (ev == null)
468 ev = new ConfigEvent(this, key, model);
469
470 ((ConfigListener) listeners[i+1]).choiceAdded(ev);
471 }
472 }
473 }
474
475 /**
476 * A Choice got added
477 */
478 protected void fireChoiceRemoved(String key, Choice model)
479 {
480 // Guaranteed to return a non-null array
481 Object[] listeners = listener_list.getListenerList();
482
483 // Process the listeners last to first, notifying
484 // those that are interested in this event
485 ConfigEvent ev = null;
486 for (int i=listeners.length-2; i>=0; i-=2)
487 {
488 if (listeners[i] == ConfigListener.class)
489 {
490 if (ev == null)
491 ev = new ConfigEvent(this, key, model);
492
493 ((ConfigListener) listeners[i+1]).choiceRemoved(ev);
494 }
495 }
496 }
497
498 /** The log stream */
499 protected static Logger log = Logger.getLogger("util.config");
500
501 /** The name for dialog boxes and properties files */
502 protected String title;
503
504 /** The array that stores the keys */
505 protected RobustList keys = new RobustList();
506
507 /** The array that stores the models */
508 protected RobustList models = new RobustList();
509
510 /** The set of local values */
511 protected Properties local = new Properties();
512
513 /** The list of listeners */
514 protected EventListenerList listener_list = new EventListenerList();
515 }