001: /*
002: * PropertyDatabase.java
003: *
004: * Copyright (C) 2001,,2003 2002 Matt Albrecht
005: * groboclown@users.sourceforge.net
006: * http://groboutils.sourceforge.net
007: *
008: * Permission is hereby granted, free of charge, to any person obtaining a
009: * copy of this software and associated documentation files (the "Software"),
010: * to deal in the Software without restriction, including without limitation
011: * the rights to use, copy, modify, merge, publish, distribute, sublicense,
012: * and/or sell copies of the Software, and to permit persons to whom the
013: * Software is furnished to do so, subject to the following conditions:
014: *
015: * The above copyright notice and this permission notice shall be included in
016: * all copies or substantial portions of the Software.
017: *
018: * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
019: * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
020: * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
021: * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
022: * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
023: * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
024: * DEALINGS IN THE SOFTWARE.
025: */
026:
027: package net.sourceforge.groboutils.util.io.v1;
028:
029: import java.io.IOException;
030: import java.io.FileNotFoundException;
031: import java.io.File;
032: import java.io.PrintWriter;
033: import java.io.FileInputStream;
034: import java.io.FileOutputStream;
035:
036: import java.util.ResourceBundle;
037: import java.util.Locale;
038: import java.util.Properties;
039: import java.util.Enumeration;
040: import java.util.Hashtable;
041:
042: /**
043: * A database of property files. Internally, it uses a
044: * ResourceBundle, so that locale specific properties can be used.
045: * The format is for the application defined property files to be defined
046: * as "read-only", and for a user defined property file to be the
047: * readable/writeable properties (i.e. modifications or additions or
048: * removals of the default properties). User settings override the
049: * read-only settings. The user property file is not localized, and is
050: * stored at <tt>$home/.<i>app-name</i>/user.properties</tt> (or the filename
051: * may be specified).
052: * <P>
053: * By default, the properties are loaded from the Resource streams,
054: * although this can be changed.
055: * <P>
056: * Before using this class, you must initialize the user property file
057: * by either {@link #setApplicationName( String )} or
058: * {@link #setUserPropertyFile( String )}.
059: * <P>
060: * The stored data is only of type String, and multiple identical key
061: * entries are not possible - only the first one is allowed.
062: *
063: * @author Matt Albrecht <a href="mailto:groboclown@users.sourceforge.net">groboclown@users.sourceforge.net</a>
064: * @since January 7, 2001
065: * @version $Date: 2003/02/10 22:52:45 $
066: */
067: public class PropertyDatabase {
068: //---------------------------------------------------------------------
069: // Public Static Fields
070:
071: //---------------------------------------------------------------------
072: // Protected Static Fields
073:
074: //---------------------------------------------------------------------
075: // Private Static Fields
076:
077: private static final String USER_FILE_NAME = "user.properties";
078: private static final String USER_REMOVED = "<<REMOVED>>";
079:
080: //---------------------------------------------------------------------
081: // Public Fields
082:
083: //---------------------------------------------------------------------
084: // Protected Fields
085:
086: //---------------------------------------------------------------------
087: // Private Fields
088:
089: private Hashtable resourceValues = new Hashtable();
090: private Properties userValues = null;
091: private boolean doAutosave = false;
092: private File userProps = null;
093: private Locale locale = Locale.getDefault();
094: private PrintWriter trace = null;
095: private File appDirectory = null;
096:
097: //---------------------------------------------------------------------
098: // Constructors
099:
100: /**
101: * Default Constructor
102: */
103: public PropertyDatabase() {
104: // do nothing
105: }
106:
107: /**
108: * Specify the Locale to load the properties from.
109: */
110: public PropertyDatabase(Locale l) {
111: this .locale = l;
112: }
113:
114: //---------------------------------------------------------------------
115: // Public Methods
116:
117: /**
118: * Set the application name and thus the corresponding directory that will
119: * store the user properties. The user properties are located
120: * at <tt>$home/.<i>app-name</i>/user.properties</tt>. If the user property
121: * file is already set, then an IllegalStateException is thrown.
122: *
123: * @param name name of the application.
124: */
125: public void setApplicationName(String name) throws IOException {
126: if (this .userProps != null) {
127: throw new IllegalStateException(
128: "property file already in use");
129: }
130: if (name == null) {
131: throw new IllegalArgumentException("no null args");
132: }
133:
134: // find the home directory.
135: File home = new File(System.getProperty("user.home"));
136: if (home == null || !home.isDirectory() || !home.exists()) {
137: throw new FileNotFoundException(
138: "user.home property not valid");
139: }
140:
141: // for hidden unix home path
142: name = "." + name;
143: this .appDirectory = new File(home, name);
144: if (!this .appDirectory.exists()) {
145: // need to create the appDir directory
146: this .appDirectory.mkdir();
147: } else if (!this .appDirectory.isDirectory()) {
148: // error - app is not a directory
149: throw new FileNotFoundException("application directory "
150: + this .appDirectory.toString()
151: + " is not a directory");
152: }
153:
154: // get the user file
155: this .userProps = new File(this .appDirectory, USER_FILE_NAME);
156: // if the file doesn't exist, create it
157: this .userProps.createNewFile();
158:
159: loadUserProperties();
160: }
161:
162: /**
163: * Returns the directory where the user properties are stored for the
164: * current application, or <tt>null</tt> if there is no application set.
165: */
166: public File getApplicationDirectory() {
167: return this .appDirectory;
168: }
169:
170: /**
171: * Set the user property file name exactly. If the user property file
172: * is already set, then an IllegalStateException is thrown.
173: */
174: public void setUserPropertyFile(String name) throws IOException {
175: if (this .userProps != null) {
176: throw new IllegalStateException(
177: "property file already in use");
178: }
179: if (name == null) {
180: throw new IllegalArgumentException("no null args");
181: }
182: File f = new File(name);
183: if (f.isDirectory()) {
184: throw new IOException("user property file " + f
185: + " is a directory");
186: }
187: // if the file doesn't exist, create it
188: f.createNewFile();
189: this .userProps = f;
190:
191: loadUserProperties();
192: }
193:
194: /**
195: * Saves the current user properties.
196: */
197: public void saveUserProperties() throws IOException {
198: if (this .userValues == null || this .userProps == null) {
199: throw new IllegalStateException("database not initialized");
200: }
201: synchronized (this ) {
202: FileOutputStream fos = null;
203: try {
204: fos = new FileOutputStream(this .userProps);
205: this .userValues.store(fos, "User values");
206: } finally {
207: if (fos != null)
208: fos.close();
209: fos = null;
210: }
211: }
212: }
213:
214: /**
215: * Adds a resource bundle of the given name to the database, from the
216: * specified locale. Note that
217: * if a user property already exists with a given key, then the
218: * user property overrides the resource property.
219: */
220: public void addResourceBundle( String resourceName )
221: {
222: ResourceBundle rb = ResourceBundle.getBundle( resourceName,
223: this .locale );
224: if (rb == null)
225: {
226: return;
227: }
228:
229: Hashtable rv = this .resourceValues;
230: Enumeration enum = rb.getKeys();
231: String key, val;
232: synchronized( rv )
233: {
234: while (enum.hasMoreElements())
235: {
236: key = (String)enum.nextElement();
237: if (rv.contains( key ) && this .trace != null)
238: {
239: this .trace.println("Resource "+resourceName+
240: " contains a duplicate key '"+key+"'.");
241: }
242: val = rb.getString( key );
243: if (val == null)
244: {
245: if (this .trace != null)
246: {
247: this .trace.println("Resource "+resourceName+
248: " contains a null key '"+key+"'." );
249: }
250: if (rv.contains( key ))
251: {
252: rv.remove( key );
253: }
254: }
255: else
256: {
257: rv.put( key, val );
258: }
259: }
260: }
261: }
262:
263: /**
264: * Retrieves the auto-save setting.
265: */
266: public boolean isAutosaveOn() {
267: return this .doAutosave;
268: }
269:
270: /**
271: * Sets the current autosave setting.
272: */
273: public void setAutosaveOn(boolean yes) {
274: this .doAutosave = yes;
275: }
276:
277: /**
278: * Removes a value from the properties. If the property is defined
279: * by a resource, then the user list must specify that it is
280: * removed.
281: *
282: * @param key the key to remove
283: * @return the value the key was assigned to, or <tt>null</tt> if nothing
284: * was removed.
285: */
286: public String removeValue(String key) {
287: String val = null;
288: val = this .userValues.getProperty(key);
289: if (val != null) {
290: if (val.equals(USER_REMOVED)) {
291: return null;
292: }
293: if (this .resourceValues.contains(key)) {
294: // remove from the user list
295: this .userValues.setProperty(key, USER_REMOVED);
296: } else {
297: this .userValues.remove(key);
298: }
299: autoSave();
300: return val;
301: }
302: val = (String) this .resourceValues.get(key);
303: if (val != null) {
304: // we know the user values doesn't have this key
305: this .userValues.setProperty(key, USER_REMOVED);
306: autoSave();
307: return val;
308: }
309:
310: // no one had this key.
311: return null;
312: }
313:
314: /**
315: * Sets the given value to the user properties.
316: *
317: * @param key the key to assign the value to
318: * @param value the value to be assigned to the key
319: */
320: public void setValue(String key, String value) {
321: if (value == null) {
322: removeValue(key);
323: return;
324: }
325: this .userValues.setProperty(key, value);
326: autoSave();
327: }
328:
329: /**
330: * Retrieves the value associated with the given key.
331: *
332: * @param key the key to pull the value out of
333: */
334: public String getValue(String key) {
335: String val = this .userValues.getProperty(key);
336: if (val != null) {
337: if (val.equals(USER_REMOVED)) {
338: return null;
339: }
340: return val;
341: }
342: return (String) this .resourceValues.get(key);
343: }
344:
345: /**
346: * Resets the user property to the resource bundle's default value.
347: *
348: * @param key the key to be reset.
349: * @return the default value for the key.
350: */
351: public String setValueToDefault(String key) {
352: boolean needSave = this .userValues.contains(key);
353: this .userValues.remove(key);
354: if (needSave)
355: autoSave();
356: return (String) this .resourceValues.get(key);
357: }
358:
359: //---------------------------------------------------
360: // Convenience functions
361:
362: /**
363: * Convenience function to convert a property to an int value.
364: *
365: * @return the given key converted to an integer, or Integer.MIN_VALUE
366: * if there was a parse error.
367: */
368: public int getIntValue(String key) {
369: try {
370: return Integer.parseInt(getValue(key));
371: } catch (NumberFormatException nfe) {
372: return Integer.MIN_VALUE;
373: } catch (NullPointerException nfe) {
374: return Integer.MIN_VALUE;
375: }
376: }
377:
378: /**
379: * Convenience function to convert an int value to a String property.
380: */
381: public void setIntValue(String key, int value) {
382: setValue(key, Integer.toString(value));
383: }
384:
385: /**
386: * Convenience function to convert a property to a boolean value.
387: *
388: * @return the given key converted to a boolean, or false
389: * if there was a parse error.
390: */
391: public boolean getBooleanValue(String key) {
392: try {
393: return Boolean.getBoolean(getValue(key));
394: } catch (NullPointerException nfe) {
395: return false;
396: }
397: }
398:
399: /**
400: * Convenience function to convert a boolean value to a String property.
401: */
402: public void setBooleanValue(String key, boolean value) {
403: setValue(key, (value ? Boolean.TRUE.toString() : Boolean.FALSE
404: .toString()));
405: }
406:
407: /**
408: * Convenience function to convert a property to a byte value.
409: *
410: * @return the given key converted to a byte, or Byte.MIN_VALUE
411: * if there was a parse error.
412: */
413: public byte getByteValue(String key) {
414: try {
415: return Byte.parseByte(getValue(key));
416: } catch (NumberFormatException nfe) {
417: return Byte.MIN_VALUE;
418: } catch (NullPointerException nfe) {
419: return Byte.MIN_VALUE;
420: }
421: }
422:
423: /**
424: * Convenience function to convert a byte value to a String property.
425: */
426: public void setByteValue(String key, byte value) {
427: setValue(key, Byte.toString(value));
428: }
429:
430: /**
431: * Convenience function to convert a property to a char value.
432: *
433: * @return the given key converted to a char, or Character.MIN_VALUE
434: * if there was a parse error.
435: */
436: public char getCharValue(String key) {
437: String val = getValue(key);
438: if (val == null || val.length() <= 0)
439: return Character.MIN_VALUE;
440: return val.charAt(0);
441: }
442:
443: /**
444: * Convenience function to convert a char value to a String property.
445: */
446: public void setCharValue(String key, char value) {
447: setValue(key, "" + value);
448: }
449:
450: /**
451: * Convenience function to convert a property to a double value.
452: *
453: * @return the given key converted to a double, or Double.MIN_VALUE
454: * if there was a parse error.
455: */
456: public double getDoubleValue(String key) {
457: try {
458: return Double.parseDouble(getValue(key));
459: } catch (NumberFormatException nfe) {
460: return Double.MIN_VALUE;
461: } catch (NullPointerException nfe) {
462: return Double.MIN_VALUE;
463: }
464: }
465:
466: /**
467: * Convenience function to convert a byte value to a String property.
468: */
469: public void setDoubleValue(String key, double value) {
470: setValue(key, Double.toString(value));
471: }
472:
473: /**
474: * Convenience function to convert a property to a float value.
475: *
476: * @return the given key converted to a float, or Float.MIN_VALUE
477: * if there was a parse error.
478: */
479: public float getFloatValue(String key) {
480: try {
481: return Float.parseFloat(getValue(key));
482: } catch (NumberFormatException nfe) {
483: return Float.MIN_VALUE;
484: } catch (NullPointerException nfe) {
485: return Float.MIN_VALUE;
486: }
487: }
488:
489: /**
490: * Convenience function to convert a float value to a String property.
491: */
492: public void setFloatValue(String key, float value) {
493: setValue(key, Float.toString(value));
494: }
495:
496: /**
497: * Convenience function to convert a property to a long value.
498: *
499: * @return the given key converted to a long, or Long.MIN_VALUE
500: * if there was a parse error.
501: */
502: public long getLongValue(String key) {
503: try {
504: return Long.parseLong(getValue(key));
505: } catch (NumberFormatException nfe) {
506: return Long.MIN_VALUE;
507: } catch (NullPointerException nfe) {
508: return Long.MIN_VALUE;
509: }
510: }
511:
512: /**
513: * Convenience function to convert a long value to a String property.
514: */
515: public void setLongValue(String key, long value) {
516: setValue(key, Long.toString(value));
517: }
518:
519: /**
520: * Convenience function to convert a property to a short value.
521: *
522: * @return the given key converted to a short, or Short.MIN_VALUE
523: * if there was a parse error.
524: */
525: public short getShortValue(String key) {
526: try {
527: return Short.parseShort(getValue(key));
528: } catch (NumberFormatException nfe) {
529: return Short.MIN_VALUE;
530: } catch (NullPointerException nfe) {
531: return Short.MIN_VALUE;
532: }
533: }
534:
535: /**
536: * Convenience function to convert a short value to a String property.
537: */
538: public void setShortValue(String key, short value) {
539: setValue(key, Short.toString(value));
540: }
541:
542: //--------------------------------------------------
543: // Debug aid function
544:
545: /**
546: * Sets the trace stream. If you set this to non-null, then warnings,
547: * such as ResourceBundles containing duplicate keys, will be reported
548: * to the stream. Errors will still be thrown as exceptions. Autosave
549: * will send any exceptions to this trace.
550: */
551: public void setTrace(PrintWriter tracer) {
552: this .trace = tracer;
553: }
554:
555: //---------------------------------------------------------------------
556: // Protected Methods
557:
558: /**
559: *
560: */
561: protected void loadUserProperties() throws IOException {
562: Properties prop = new Properties();
563: FileInputStream fis = new FileInputStream(this .userProps);
564: prop.load(fis);
565: fis.close();
566:
567: // if no exception was thrown...
568: synchronized (this ) {
569: this .userValues = prop;
570: }
571: }
572:
573: /**
574: *
575: */
576: protected void autoSave() {
577: if (isAutosaveOn()) {
578: try {
579: // attempt to save
580: saveUserProperties();
581: } catch (IOException ioe) {
582: if (this .trace != null) {
583: ioe.printStackTrace(this .trace);
584: }
585: }
586: }
587: }
588:
589: //---------------------------------------------------------------------
590: // Private Methods
591:
592: }
|