1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package org.xnap.util;
21
22 import java.awt.Color;
23 import java.awt.Font;
24 import java.awt.event.KeyEvent;
25 import java.beans.PropertyChangeListener;
26 import java.beans.PropertyChangeSupport;
27 import java.io.File;
28 import java.io.FileInputStream;
29 import java.io.FileNotFoundException;
30 import java.io.FileOutputStream;
31 import java.io.IOException;
32 import java.util.Arrays;
33 import java.util.Hashtable;
34 import java.util.Properties;
35 import java.util.StringTokenizer;
36 import javax.swing.KeyStroke;
37
38 import org.apache.log4j.Logger;
39 import org.xnap.util.prefs.Validator;
40
41 /***
42 * This class provides a default implementation for a preferences framework.
43 * Methods are provided that can read and write native types, arrays and a few
44 * custom types.
45 */
46 public abstract class AbstractPreferences {
47
48
49
50 public static final String ARRAY_SEPARATOR = ";";
51
52
53
54 protected static Logger logger = Logger.getLogger(AbstractPreferences.class);
55
56 protected transient PropertyChangeSupport propertyChange
57 = new PropertyChangeSupport(this);
58
59 /***
60 * Determines if preferences need to be saved.
61 */
62 protected boolean changedFlag = false;
63
64 /***
65 * Preferences database.
66 */
67 protected File prefsFile;
68
69 /***
70 * Version of database format.
71 */
72 protected int version;
73
74 /***
75 * Version of database when read.
76 */
77 protected int oldVersion = -1;
78
79 /***
80 * The namespace is prefixed to each key.
81 */
82 protected String namespace;
83
84 /***
85 * Preferences.
86 */
87 protected Properties props = new Properties();
88
89 /***
90 * Maps keys to validators. Validators are used to validate input of
91 * set methods.
92 */
93 private Hashtable validatorsByKey = new Hashtable();
94
95
96
97 /***
98 * Constructs a <code>PreferencesSupport</code> object.
99 *
100 * @param filename the filename of the preferences file
101 * @param version the current version of the preferences, this version
102 * can be more recent than the version of the file
103 * @param namespace the namespace, used as a prefix for all keys
104 */
105 public AbstractPreferences(String filename, int version, String namespace)
106 {
107 this.prefsFile = new File(filename);
108 this.version = version;
109
110 if (!namespace.endsWith(".")) {
111 namespace += ".";
112 }
113 this.namespace = namespace;
114 }
115
116
117
118 /***
119 * Returns the absolute path of the preferences file.
120 */
121 public String getFilename()
122 {
123 return prefsFile.getAbsolutePath();
124 }
125
126 /***
127 * Reads preferences from <code>f</code>.
128 */
129 public void read(File f)
130 {
131 logger.info("reading " + f);
132
133 FileInputStream in = null;
134 try {
135 in = new FileInputStream(f);
136
137 props.load(in);
138
139 try {
140 oldVersion = Integer.parseInt
141 (props.getProperty("props.ver", "-1"));
142 }
143 catch (NumberFormatException e) {
144 oldVersion = -1;
145 }
146
147 if (oldVersion != -1 && oldVersion < getVersion()) {
148 logger.debug("converting from version " + oldVersion + " to "
149 + getVersion());
150 convert(oldVersion);
151 }
152 }
153 catch (FileNotFoundException e) {
154 }
155 catch (IOException e) {
156 }
157 finally {
158 try {
159 if (in != null) {
160 in.close();
161 }
162 }
163 catch (Exception e) {
164 }
165 }
166 }
167
168 /***
169 * Reads preferences from default preferences file.
170 */
171 public void read()
172 {
173 read(prefsFile);
174 }
175
176 /***
177 * Writes preferences to default preferences file. If preferences have not
178 * changed since the last read or write operation, no action is taken.
179 *
180 * @return true, if file is written successfully or preferences were not
181 * changed; false, if an <code>IOException</code> has occured
182 */
183 public boolean write()
184 {
185 if (!changedFlag) {
186 logger.info("nothing changed, not writing " + prefsFile);
187 return true;
188 }
189
190 logger.info("writing " + prefsFile);
191
192 FileOutputStream out = null;
193 try {
194 out = new FileOutputStream(prefsFile);
195
196 props.put("props.ver", version + "");
197 props.store(out, "This file was automatically generated.");
198 }
199 catch (IOException e) {
200 return false;
201 }
202 finally {
203 try {
204 if (out != null) {
205 out.close();
206 }
207 }
208 catch (Exception e) {
209 }
210 }
211
212 changedFlag = false;
213
214 return true;
215 }
216
217 /***
218 * Returns the version of the preferences in the file at the point
219 * of the last read operation.
220 */
221 public int getOldVersion()
222 {
223 return oldVersion;
224 }
225
226 /***
227 * Returns the current version of the preferences.
228 */
229 public int getVersion()
230 {
231 return version;
232 }
233
234 /***
235 * Invoked by {@link #read()} to converts preferences from
236 * <code>oldVersion</code> to current version.
237 */
238 public abstract void convert(int oldVersion);
239
240 /***
241 * Adds a preferences listener.
242 */
243 public synchronized
244 void addPropertyChangeListener(PropertyChangeListener l)
245 {
246 propertyChange.addPropertyChangeListener(l);
247 }
248
249 /***
250 * Adds a preferences listener for a specific key.
251 */
252 public synchronized
253 void addPropertyChangeListener(String key, PropertyChangeListener l)
254 {
255 propertyChange.addPropertyChangeListener(key, l);
256 }
257
258 /***
259 * Fires PropertyChangeEvent without namespace.
260 */
261 public void firePropertyChange(String key, Object oldValue,
262 Object newValue) {
263
264
265
266
267 int i = key.lastIndexOf(".");
268 if (key.length() > i + 1) {
269 key = key.substring(i + 1);
270 }
271
272 propertyChange.firePropertyChange(key, oldValue, newValue);
273 }
274
275 /***
276 * Removes a preferences listener.
277 */
278 public synchronized
279 void removePropertyChangeListener(PropertyChangeListener l) {
280 propertyChange.removePropertyChangeListener(l);
281 }
282
283 public synchronized void removePropertyChangeListener
284 (String key, PropertyChangeListener l) {
285 propertyChange.removePropertyChangeListener(key, l);
286 }
287
288 public String get(String key)
289 {
290 return getProperty(key, "");
291 }
292
293 public String[] getArray(String key)
294 {
295 return StringHelper.toArray(getProperty(key, ""), ARRAY_SEPARATOR);
296 }
297
298 public boolean getBoolean(String key)
299 {
300 String val = getProperty(key, "false");
301 return val.equalsIgnoreCase("true");
302 }
303
304 public Color getColor(String key)
305 {
306 int val = getInt(key);
307 return new Color(val & 0xFF, (val >> 8) & 0xFF, (val >> 16) & 0xFF);
308 }
309
310 /***
311 * Always returns a valid font.
312 */
313 public Font getFont(String key)
314 {
315 String val = getProperty(key, "");
316 StringTokenizer t = new StringTokenizer(val, ARRAY_SEPARATOR);
317
318 if (t.countTokens() >= 3) {
319 try {
320 String name = t.nextToken();
321 int style = Integer.parseInt(t.nextToken());
322 int size = Integer.parseInt(t.nextToken());
323
324 return new Font(name, style, size);
325 }
326 catch (NumberFormatException e) {
327 }
328 }
329
330 return new Font("Monospaced", 0, 12);
331 }
332
333 /***
334 * Reads a keystroke from the properties.
335 *
336 * @return null, if key was not found or value is invalid
337 */
338 public KeyStroke getKeyStroke(String key)
339 {
340 String val = getProperty(key, "");
341
342
343
344
345
346 StringTokenizer t = new StringTokenizer(val, ARRAY_SEPARATOR);
347
348 if (t.countTokens() >= 3) {
349 try {
350 int keyCode = Integer.parseInt(t.nextToken());
351 int modifiers = Integer.parseInt(t.nextToken());
352 Character character = new Character(t.nextToken().charAt(0));
353 return (keyCode == KeyEvent.VK_UNDEFINED)
354 ? KeyStroke.getKeyStroke(character, modifiers)
355 : KeyStroke.getKeyStroke(keyCode, modifiers);
356 }
357 catch (NumberFormatException e) {
358 }
359 }
360
361 return null;
362 }
363
364 /***
365 * Reads an integer from the properties.
366 *
367 * @return 0, if conversion fails; the value, otherwise
368 */
369 public int getInt(String key)
370 {
371 try {
372 return Integer.parseInt(getProperty(key, "0"));
373 }
374 catch (NumberFormatException e) {
375 return 0;
376 }
377 }
378
379 public int[] getIntArray(String key)
380 {
381 return StringHelper.toIntArray(getProperty(key, ""), ARRAY_SEPARATOR);
382 }
383
384 public long getLong(String key)
385 {
386 try {
387 return Long.parseLong(getProperty(key, "0"));
388 }
389 catch (NumberFormatException e) {
390 return 0;
391 }
392 }
393
394 public void set(String key, String newValue)
395 {
396 String oldValue = get(key);
397 if (!areObjectsEqual(newValue, oldValue)) {
398 setProperty(key, newValue);
399 firePropertyChange(key, oldValue, newValue);
400 }
401 }
402
403 public void set(String key, String[] newValue)
404 {
405 String[] oldValue = getArray(key);
406 if (!Arrays.equals(newValue, oldValue)) {
407 setProperty(key, StringHelper.toString(newValue, ARRAY_SEPARATOR));
408 firePropertyChange(key, oldValue, newValue);
409 }
410 }
411
412 public void set(String key, boolean newValue)
413 {
414 boolean oldValue = getBoolean(key);
415 if (newValue != oldValue) {
416 setProperty(key, newValue + "");
417 firePropertyChange(key, new Boolean(oldValue),
418 new Boolean(newValue));
419 }
420 }
421
422 public void set(String key, Color newValue)
423 {
424 Color oldValue = getColor(key);
425
426 if (!newValue.equals(oldValue)) {
427
428 int c = newValue.getRed()
429 | newValue.getGreen() << 8
430 | newValue.getBlue() << 16;
431 setProperty(key, c + "");
432 firePropertyChange(key, oldValue, newValue);
433 }
434 }
435
436 /***
437 * Saves a font.
438 */
439 public void set(String key, Font newValue)
440 {
441 Font oldValue = getFont(key);
442
443 if (!newValue.equals(oldValue)) {
444 setProperty(key, toString(newValue));
445 firePropertyChange(key, oldValue, newValue);
446 }
447 }
448
449 public void set(String key, int newValue)
450 {
451 int oldValue = getInt(key);
452 if (newValue != oldValue) {
453 setProperty(key, newValue + "");
454 firePropertyChange(key, new Integer(oldValue),
455 new Integer(newValue));
456 }
457 }
458
459 public void set(String key, int[] newValue)
460 {
461 int[] oldValue = getIntArray(key);
462 if (!Arrays.equals(newValue, oldValue)) {
463 setProperty(key, StringHelper.toString(newValue, ARRAY_SEPARATOR));
464 firePropertyChange(key, oldValue, newValue);
465 }
466 }
467
468 public void set(String key, KeyStroke newValue)
469 {
470 KeyStroke oldValue = getKeyStroke(key);
471
472 if (newValue != oldValue) {
473 setProperty(key, toString(newValue));
474 firePropertyChange(key, oldValue, newValue);
475 }
476 }
477
478 public void set(String key, long newValue)
479 {
480
481 long oldValue = getLong(key);
482 if (newValue != oldValue) {
483 setProperty(key, newValue + "");
484 firePropertyChange(key, new Long(oldValue),
485 new Long(newValue));
486 }
487 }
488
489 /***
490 * Sets a default property. If key already exists the value is not
491 * overwritten.
492 *
493 * <p>If a key already exists, the corresponding value is validated
494 * by <code>validator</code> and will replaced by <code>value</code>
495 * if the validation fails.
496 *
497 * @param key the key
498 * @param value the default value
499 * @param validator a validator that is invoked each time a value is set
500 */
501 public synchronized void setDefault(String key, String value,
502 Validator validator)
503 {
504 if (validator != null) {
505 validatorsByKey.put(namespace + key, validator);
506 }
507
508 if (getProperty(key, null) == null) {
509
510 setProperty(key, value, false);
511 }
512 else if (validator != null) {
513 try {
514
515 validator.validate(getProperty(key, ""));
516 }
517 catch (IllegalArgumentException e) {
518
519 logger.debug("invalid value: " + namespace + key + " = "
520 + getProperty(key, "") + " [" + value + "]", e);
521 setProperty(key, value, false);
522 }
523 }
524
525 }
526
527 /***
528 * Sets a default property. If key already exists the value is not
529 * overwritten.
530 *
531 * @param key the key
532 * @param value the default value
533 */
534 public void setDefault(String key, String value)
535 {
536 setDefault(key, value, null);
537 }
538
539 /***
540 * Ignores namespace.
541 */
542 public synchronized void removeProperty(String key)
543 {
544 props.remove(key);
545 changedFlag = true;
546 }
547
548 /***
549 * Renames a property, used for conversion of property file formats.
550 * Ignores namespace. Does not fire change event.
551 */
552 public synchronized void renameProperty(String oldKey, String newKey)
553 {
554 String value = props.getProperty(oldKey, null);
555 if (value != null) {
556 Object oldValue = props.remove(oldKey);
557 if (oldValue != null) {
558 props.setProperty(newKey, value);
559 changedFlag = true;
560 }
561 }
562 }
563
564 /***
565 * Returns a property.
566 */
567 protected synchronized String getProperty(String key, String defaultValue)
568 {
569 String s = props.getProperty(namespace + key, defaultValue);
570 return s;
571 }
572
573 /***
574 * Sets a property. <code>newValue</code> is validated if
575 * <code>validate</code> is set to true and a validator has been
576 * provided.
577 *
578 * @exception IllegalArgumentException if <code>newValue</code> is not valid
579 * @param key the key to set
580 * @param newValue the value to set
581 * @param validate if true, <code>newValue</code> will be validated
582 */
583 protected synchronized void setProperty(String key, String newValue,
584 boolean validate)
585 {
586 if (validate) {
587 Validator v = (Validator)validatorsByKey.get(namespace + key);
588 if (v != null) {
589 v.validate(newValue);
590 }
591 }
592
593 props.setProperty(namespace + key, newValue);
594 changedFlag = true;
595 }
596
597 /***
598 * Sets a property. <code>newValue</code> is validated if a validator has
599 * been provided.
600 *
601 * @param key the key to set
602 * @param newValue the value to set
603 * @see #setProperty(String, String, boolean)
604 */
605 protected void setProperty(String key, String newValue)
606 {
607 setProperty(key, newValue, true);
608 }
609
610 /***
611 * Determine if 2 objects are equal, or both point to null.
612 */
613 public static boolean areObjectsEqual(Object obj1, Object obj2)
614 {
615 return (obj1 != null) ? obj1.equals(obj2) : (obj1 == obj2);
616 }
617
618 /***
619 * Determine if 2 arrays are equal, or both point to null.
620 */
621 public static boolean areObjectsEqual(Object[] obj1, Object[] obj2)
622 {
623 return (obj1 != null) ? obj2 == null : Arrays.equals(obj1, obj2);
624 }
625
626 public static String toString(Font font)
627 {
628
629 StringBuffer sb = new StringBuffer();
630 sb.append(font.getName());
631 sb.append(ARRAY_SEPARATOR);
632 sb.append(font.getStyle());
633 sb.append(ARRAY_SEPARATOR);
634 sb.append(font.getSize());
635 return sb.toString();
636 }
637
638 public static String toString(KeyStroke keystroke)
639 {
640 if (keystroke == null) {
641 return "";
642 }
643
644 StringBuffer sb = new StringBuffer();
645 sb.append(keystroke.getKeyCode());
646 sb.append(ARRAY_SEPARATOR);
647 sb.append(keystroke.getModifiers());
648 sb.append(ARRAY_SEPARATOR);
649 sb.append(keystroke.getKeyChar());
650 return sb.toString();
651 }
652
653 }