View Javadoc

1   /*
2    *  XNap - A P2P framework and client.
3    *
4    *  See the file AUTHORS for copyright information.
5    *
6    *  This program is free software; you can redistribute it and/or modify
7    *  it under the terms of the GNU General Public License as published by
8    *  the Free Software Foundation.
9    *
10   *  This program is distributed in the hope that it will be useful,
11   *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12   *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   *  GNU General Public License for more details.
14   *
15   *  You should have received a copy of the GNU General Public License
16   *  along with this program; if not, write to the Free Software
17   *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
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      //--- Constant(s) ---
49  
50      public static final String ARRAY_SEPARATOR = ";";
51  
52      //--- Data field(s) ---
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      //--- Constructor(s) ---
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     //--- Method(s) ---
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 		//  	if (key.startsWith(namespace)) {
264 		//  	    key = key.substring(namespace.length() + 1);
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 		// this would be way easier but there seems to be no serialize
343 		// method that can create the format recognized by getKeyStroke()
344 		//return KeyStroke.getKeyStroke(key);
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 			// encode color
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 		// we can use the != operator since KeyStroke objects are unique
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 			// property not set, use default value
510 			setProperty(key, value, false);
511 		}
512 		else if (validator != null) {
513 			try {
514 				// validate already set value
515 				validator.validate(getProperty(key, ""));
516 			}
517 			catch (IllegalArgumentException e) {
518 				// set value is not valid, use default
519 				logger.debug("invalid value: " + namespace + key + " = "
520 							 + getProperty(key, "") + " [" + value + "]", e);
521 				setProperty(key, value, false);
522 			}
523 		}
524 		// else: value is already set and needs no validation
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 		// encode font
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 		// encode keystroke
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 }