001: package net.suberic.util;
002:
003: import java.util.*;
004: import java.io.*;
005:
006: /**
007: * VariableBundle is a combination of a Properties object, a ResourceBundle
008: * object, and (optionally) a second Properties object to act as the 'parent'
009: * properties. This allows both for a single point of reference for
010: * variables, as well as the ability to do hierarchical lookups with the
011: * parent (see getProperty() for an example).
012: *
013: * The order of lookup is as follows: Local properties are checked first,
014: * then parent properties, and then finally (if the value is not found in
015: * any properties) the ResourceBundle is checked.
016: */
017:
018: public class VariableBundle extends Object {
019: private Properties properties;
020: private Properties writableProperties;
021: private Properties temporaryProperties = new Properties();
022: private ResourceBundle resources;
023: private VariableBundle parentProperties;
024: private File mSaveFile;
025: private Vector removeList = new Vector();
026: private Hashtable VCListeners = new Hashtable();
027: private Hashtable VCGlobListeners = new Hashtable();
028:
029: public VariableBundle(InputStream propertiesFile,
030: String resourceFile, VariableBundle newParentProperties) {
031: configure(propertiesFile, resourceFile, newParentProperties);
032: }
033:
034: public VariableBundle(File propertiesFile,
035: VariableBundle newParentProperties)
036: throws java.io.FileNotFoundException {
037: FileInputStream fis = new FileInputStream(propertiesFile);
038: configure(fis, null, newParentProperties);
039: try {
040: fis.close();
041: } catch (java.io.IOException ioe) {
042: }
043: mSaveFile = propertiesFile;
044:
045: }
046:
047: public VariableBundle(InputStream propertiesFile,
048: String resourceFile) {
049: this (propertiesFile, resourceFile, null);
050: }
051:
052: public VariableBundle(InputStream propertiesFile,
053: VariableBundle newParentProperties) {
054: this (propertiesFile, null, newParentProperties);
055: }
056:
057: public VariableBundle(Properties editableProperties,
058: VariableBundle newParentProperties) {
059: writableProperties = editableProperties;
060: parentProperties = newParentProperties;
061: properties = new Properties();
062: resources = null;
063: }
064:
065: /**
066: * Configures the VariableBundle.
067: */
068: protected void configure(InputStream propertiesFile,
069: String resourceFile, VariableBundle newParentProperties) {
070:
071: writableProperties = new Properties();
072:
073: if (resourceFile != null)
074: try {
075: resources = ResourceBundle.getBundle(resourceFile,
076: Locale.getDefault());
077: } catch (MissingResourceException mre) {
078: System.err.println("Error loading resource "
079: + mre.getClassName() + mre.getKey()
080: + ": trying default locale.");
081: try {
082: resources = ResourceBundle.getBundle(resourceFile,
083: Locale.US);
084: } catch (MissingResourceException mreTwo) {
085: System.err
086: .println("Unable to load default (US) resource bundle; exiting.");
087: System.exit(1);
088: }
089: }
090: else
091: resources = null;
092:
093: properties = new Properties();
094:
095: if (propertiesFile != null)
096: try {
097: properties.load(propertiesFile);
098: } catch (java.io.IOException ioe) {
099: System.err.println(ioe.getMessage() + ": "
100: + propertiesFile);
101: }
102:
103: List includeStreams = getPropertyAsList(
104: "VariableBundle.include", "");
105: if (includeStreams != null && includeStreams.size() > 0) {
106: for (int i = 0; i < includeStreams.size(); i++) {
107: String current = (String) includeStreams.get(i);
108: try {
109: if (current != null && !current.equals("")) {
110: java.net.URL url = this .getClass().getResource(
111: current);
112:
113: java.io.InputStream is = url.openStream();
114:
115: properties.load(is);
116: }
117: } catch (java.io.IOException ioe) {
118: System.err.println("error including file "
119: + current + ": " + ioe.getMessage());
120: ioe.printStackTrace();
121: }
122: }
123: }
124:
125: parentProperties = newParentProperties;
126:
127: }
128:
129: public String getProperty(String key, String defaultValue) {
130: String returnValue;
131:
132: returnValue = temporaryProperties.getProperty(key, "");
133: if (returnValue == "") {
134: returnValue = writableProperties.getProperty(key, "");
135: if (returnValue == "") {
136: returnValue = properties.getProperty(key, "");
137: if (returnValue == "") {
138: returnValue = getParentProperty(key, "");
139: if (returnValue == "") {
140: if (resources != null)
141: try {
142: returnValue = resources.getString(key);
143: } catch (MissingResourceException mre) {
144: returnValue = defaultValue;
145: }
146: else
147: returnValue = defaultValue;
148: }
149: }
150: }
151: }
152: return returnValue;
153: }
154:
155: public String getProperty(String key)
156: throws MissingResourceException {
157: String returnValue;
158:
159: returnValue = getProperty(key, "");
160: if (returnValue == "") {
161: throw new MissingResourceException(key, "", key);
162: }
163: return returnValue;
164: }
165:
166: private String getParentProperty(String key, String defaultValue) {
167: if (parentProperties == null) {
168: return defaultValue;
169: } else {
170: return parentProperties.getProperty(key, defaultValue);
171: }
172: }
173:
174: public ResourceBundle getResources() {
175: return resources;
176: }
177:
178: public void setResourceBundle(ResourceBundle newResources) {
179: resources = newResources;
180: }
181:
182: public Properties getProperties() {
183: return properties;
184: }
185:
186: public void setProperties(Properties newProperties) {
187: properties = newProperties;
188: }
189:
190: public VariableBundle getParentProperties() {
191: return parentProperties;
192: }
193:
194: public Properties getWritableProperties() {
195: return writableProperties;
196: }
197:
198: public void setProperty(String propertyName, String propertyValue) {
199: temporaryProperties.remove(propertyName);
200: writableProperties.setProperty(propertyName, propertyValue);
201: if (propertyValue == null || propertyValue.equalsIgnoreCase("")) {
202: removeProperty(propertyName);
203: } else {
204: unRemoveProperty(propertyName);
205: }
206: fireValueChanged(propertyName);
207: }
208:
209: /**
210: * sets a property as temporary (so it won't be saved).
211: */
212: public void setProperty(String propertyName, String propertyValue,
213: boolean temporary) {
214: if (temporary) {
215: temporaryProperties
216: .setProperty(propertyName, propertyValue);
217: fireValueChanged(propertyName);
218: } else {
219: setProperty(propertyName, propertyValue);
220: }
221: }
222:
223: /**
224: * Returns a property which has multiple values separated by a ':' (colon)
225: * as a java.util.Enumeration.
226: */
227:
228: public Enumeration getPropertyAsEnumeration(String propertyName,
229: String defaultValue) {
230: StringTokenizer tokens = new StringTokenizer(getProperty(
231: propertyName, defaultValue), ":");
232: return tokens;
233: }
234:
235: /**
236: * Converts a value which has multiple values separated by a ':' (colon)
237: * to a java.util.Vector.
238: */
239: public static Vector convertToVector(String value) {
240: Vector returnValue = new Vector();
241: StringTokenizer tokens = new StringTokenizer(value, ":");
242: while (tokens.hasMoreElements())
243: returnValue.add(tokens.nextElement());
244: return returnValue;
245: }
246:
247: /**
248: * Converts the given property value to a Vector using the convertToVector
249: * call.
250: */
251: public Vector getPropertyAsVector(String propertyName,
252: String defaultValue) {
253: return convertToVector(getProperty(propertyName, defaultValue));
254: }
255:
256: /**
257: * Converts a value which has multiple values separated by a ':' (colon)
258: * to a java.util.List.
259: */
260: public static List convertToList(String value) {
261: List returnValue = new ArrayList();
262: StringTokenizer tokens = new StringTokenizer(value, ":");
263: while (tokens.hasMoreElements())
264: returnValue.add(tokens.nextElement());
265: return returnValue;
266: }
267:
268: /**
269: * Converts the given property value to a List using the convertToList
270: * call.
271: */
272: public List getPropertyAsList(String propertyName,
273: String defaultValue) {
274: return convertToList(getProperty(propertyName, defaultValue));
275: }
276:
277: /**
278: * Converts a List of Strings to a colon-delimited String.
279: */
280: public static String convertToString(List pValue) {
281: if (pValue == null || pValue.size() == 0)
282: return "";
283: else {
284: StringBuffer returnBuffer = new StringBuffer();
285: Iterator it = pValue.iterator();
286: while (it.hasNext()) {
287: returnBuffer.append((String) it.next());
288: if (it.hasNext()) {
289: returnBuffer.append(":");
290: }
291: }
292:
293: return returnBuffer.toString();
294: }
295: }
296:
297: /**
298: * Saves the current properties in the VariableBundle to a file. Note
299: * that this only saves the writableProperties of this particular
300: * VariableBundle--underlying defaults are not written.
301: */
302: public void saveProperties() {
303: if (mSaveFile != null) {
304: saveProperties(mSaveFile);
305: }
306: }
307:
308: /**
309: * Saves the current properties in the VariableBundle to a file. Note
310: * that this only saves the writableProperties of this particular
311: * VariableBundle--underlying defaults are not written.
312: */
313: public void saveProperties(File pSaveFile) {
314: if (pSaveFile == null)
315: return;
316:
317: synchronized (this ) {
318: if (writableProperties.size() > 0) {
319: File outputFile;
320: String currentLine, key;
321: int equalsLoc;
322:
323: try {
324: if (!pSaveFile.exists())
325: pSaveFile.createNewFile();
326:
327: outputFile = pSaveFile.createTempFile(pSaveFile
328: .getName(), ".tmp", pSaveFile
329: .getParentFile());
330:
331: BufferedReader readSaveFile = new BufferedReader(
332: new FileReader(pSaveFile));
333: BufferedWriter writeSaveFile = new BufferedWriter(
334: new FileWriter(outputFile));
335: currentLine = readSaveFile.readLine();
336: while (currentLine != null) {
337: equalsLoc = currentLine.indexOf('=');
338: if (equalsLoc != -1) {
339: String rawKey = currentLine.substring(0,
340: equalsLoc);
341: key = unEscapeString(rawKey);
342:
343: if (!propertyIsRemoved(key)) {
344: if (writableProperties.getProperty(key,
345: "").equals("")) {
346:
347: writeSaveFile.write(currentLine);
348: writeSaveFile.newLine();
349:
350: } else {
351: writeSaveFile
352: .write(rawKey
353: + "="
354: + escapeWhiteSpace(writableProperties
355: .getProperty(
356: key,
357: "")));
358: writeSaveFile.newLine();
359: properties.setProperty(key,
360: writableProperties
361: .getProperty(key,
362: ""));
363: writableProperties.remove(key);
364: }
365: removeProperty(key);
366: }
367:
368: } else {
369: writeSaveFile.write(currentLine);
370: writeSaveFile.newLine();
371: }
372: currentLine = readSaveFile.readLine();
373: }
374:
375: // write out the rest of the writableProperties
376:
377: Enumeration propsLeft = writableProperties.keys();
378: while (propsLeft.hasMoreElements()) {
379: String nextKey = (String) propsLeft
380: .nextElement();
381: String nextKeyEscaped = escapeWhiteSpace(nextKey);
382: String nextValueEscaped = escapeWhiteSpace(writableProperties
383: .getProperty(nextKey, ""));
384: writeSaveFile.write(nextKeyEscaped + "="
385: + nextValueEscaped);
386: writeSaveFile.newLine();
387:
388: properties.setProperty(nextKey,
389: writableProperties.getProperty(nextKey,
390: ""));
391: writableProperties.remove(nextKey);
392: }
393:
394: clearRemoveList();
395:
396: readSaveFile.close();
397: writeSaveFile.flush();
398: writeSaveFile.close();
399:
400: // if you don't delete the .old file first, then the
401: // rename fails under Windows.
402: String oldSaveName = pSaveFile.getAbsolutePath()
403: + ".old";
404: File oldSave = new File(oldSaveName);
405: if (oldSave.exists())
406: oldSave.delete();
407:
408: String fileName = new String(pSaveFile
409: .getAbsolutePath());
410: pSaveFile.renameTo(oldSave);
411: outputFile.renameTo(new File(fileName));
412:
413: } catch (Exception e) {
414: System.out.println(getProperty(
415: "VariableBundle.saveError",
416: "Error saving properties file: "
417: + pSaveFile.getName() + ": "
418: + e.getMessage()));
419: e.printStackTrace(System.err);
420: }
421: }
422: }
423: }
424:
425: /*
426: * Converts encoded \uxxxx to unicode chars
427: * and changes special saved chars to their original forms
428: *
429: * ripped directly from java.util.Properties; hope they don't mind.
430: */
431: private String loadConvert(String theString) {
432: char aChar;
433: int len = theString.length();
434: StringBuffer outBuffer = new StringBuffer(len);
435:
436: for (int x = 0; x < len;) {
437: aChar = theString.charAt(x++);
438: if (aChar == '\\') {
439: aChar = theString.charAt(x++);
440: if (aChar == 'u') {
441: // Read the xxxx
442: int value = 0;
443: for (int i = 0; i < 4; i++) {
444: aChar = theString.charAt(x++);
445: switch (aChar) {
446: case '0':
447: case '1':
448: case '2':
449: case '3':
450: case '4':
451: case '5':
452: case '6':
453: case '7':
454: case '8':
455: case '9':
456: value = (value << 4) + aChar - '0';
457: break;
458: case 'a':
459: case 'b':
460: case 'c':
461: case 'd':
462: case 'e':
463: case 'f':
464: value = (value << 4) + 10 + aChar - 'a';
465: break;
466: case 'A':
467: case 'B':
468: case 'C':
469: case 'D':
470: case 'E':
471: case 'F':
472: value = (value << 4) + 10 + aChar - 'A';
473: break;
474: default:
475: throw new IllegalArgumentException(
476: "Malformed \\uxxxx encoding.");
477: }
478: }
479: outBuffer.append((char) value);
480: } else {
481: if (aChar == 't')
482: aChar = '\t';
483: else if (aChar == 'r')
484: aChar = '\r';
485: else if (aChar == 'n')
486: aChar = '\n';
487: else if (aChar == 'f')
488: aChar = '\f';
489: outBuffer.append(aChar);
490: }
491: } else
492: outBuffer.append(aChar);
493: }
494: return outBuffer.toString();
495: }
496:
497: /*
498: * Converts unicodes to encoded \uxxxx
499: * and writes out any of the characters in specialSaveChars
500: * with a preceding slash
501: *
502: * ripped directly from java.util.Properties; hope they don't mind.
503: */
504: private String saveConvert(String theString, boolean escapeSpace) {
505: int len = theString.length();
506: StringBuffer outBuffer = new StringBuffer(len * 2);
507:
508: for (int x = 0; x < len; x++) {
509: char aChar = theString.charAt(x);
510: switch (aChar) {
511: case ' ':
512: if (x == 0 || escapeSpace)
513: outBuffer.append('\\');
514:
515: outBuffer.append(' ');
516: break;
517: case '\\':
518: outBuffer.append('\\');
519: outBuffer.append('\\');
520: break;
521: case '\t':
522: outBuffer.append('\\');
523: outBuffer.append('t');
524: break;
525: case '\n':
526: outBuffer.append('\\');
527: outBuffer.append('n');
528: break;
529: case '\r':
530: outBuffer.append('\\');
531: outBuffer.append('r');
532: break;
533: case '\f':
534: outBuffer.append('\\');
535: outBuffer.append('f');
536: break;
537: default:
538: if ((aChar < 0x0020) || (aChar > 0x007e)) {
539: outBuffer.append('\\');
540: outBuffer.append('u');
541: outBuffer.append(toHex((aChar >> 12) & 0xF));
542: outBuffer.append(toHex((aChar >> 8) & 0xF));
543: outBuffer.append(toHex((aChar >> 4) & 0xF));
544: outBuffer.append(toHex(aChar & 0xF));
545: } else {
546: if (specialSaveChars.indexOf(aChar) != -1)
547: outBuffer.append('\\');
548: outBuffer.append(aChar);
549: }
550: }
551: }
552: return outBuffer.toString();
553: }
554:
555: /**
556: * Escapes whitespace in a string by putting a '\' in front of each
557: * whitespace character.
558: */
559: public String escapeWhiteSpace(String sourceString) {
560: /*
561: char[] origString = sourceString.toCharArray();
562: StringBuffer returnString = new StringBuffer();
563: for (int i = 0; i < origString.length; i++) {
564: char currentChar = origString[i];
565: if (Character.isWhitespace(currentChar) || '\\' == currentChar)
566: returnString.append('\\');
567:
568: returnString.append(currentChar);
569: }
570:
571: return returnString.toString();
572: */
573: return saveConvert(sourceString, true);
574: }
575:
576: /**
577: * resolves a whitespace-escaped string.
578: */
579: public String unEscapeString(String sourceString) {
580: return loadConvert(sourceString);
581: }
582:
583: /**
584: * Clears the removeList. This should generally be called after
585: * you do a writeProperties();
586: */
587: public void clearRemoveList() {
588: removeList.clear();
589: }
590:
591: /**
592: * This removes the property from the currently VariableBundle. This
593: * is different than setting the value to "" (or null) in that, if the
594: * property is removed, it is removed from the source property file.
595: */
596: public void removeProperty(String remProp) {
597: if (!propertyIsRemoved(remProp))
598: removeList.add(remProp);
599: }
600:
601: /**
602: * Removes a property from the removeList. Only necessary if a property
603: * had been removed since the last save, and now has been set to a new
604: * value. It's probably a good idea, though, to call this method any
605: * time a property has its value set.
606: */
607: public void unRemoveProperty(String unRemProp) {
608: for (int i = removeList.size() - 1; i >= 0; i--) {
609: if (((String) removeList.elementAt(i)).equals(unRemProp))
610: removeList.removeElementAt(i);
611: }
612: }
613:
614: /**
615: * Returns true if the property is in the removeList for this
616: * VariableBundle.
617: */
618: public boolean propertyIsRemoved(String prop) {
619: if (removeList.size() < 1)
620: return false;
621:
622: for (int i = 0; i < removeList.size(); i++) {
623: if (((String) removeList.elementAt(i)).equals(prop))
624: return true;
625: }
626:
627: return false;
628: }
629:
630: /**
631: * This notifies all registered listeners for changedValue that its
632: * value has changed.
633: */
634: public void fireValueChanged(String changedValue) {
635: // only notify each listener once.
636: Set notified = new HashSet();
637:
638: Vector listeners = (Vector) VCListeners.get(changedValue);
639: if (listeners != null && listeners.size() > 0) {
640: for (int i = 0; i < listeners.size(); i++) {
641: ((ValueChangeListener) listeners.elementAt(i))
642: .valueChanged(changedValue);
643: notified.add(listeners.elementAt(i));
644: }
645: }
646:
647: // now add the glob listeners.
648:
649: Enumeration keys = VCGlobListeners.keys();
650: while (keys.hasMoreElements()) {
651: String currentPattern = (String) keys.nextElement();
652: if (changedValue.startsWith(currentPattern)) {
653: Vector globListeners = (Vector) VCGlobListeners
654: .get(currentPattern);
655: if (globListeners != null && globListeners.size() > 0) {
656: for (int i = 0; i < globListeners.size(); i++) {
657: ValueChangeListener currentListener = ((ValueChangeListener) globListeners
658: .elementAt(i));
659: if (!notified.contains(currentListener)) {
660: currentListener.valueChanged(changedValue);
661: notified.add(currentListener);
662: }
663: }
664: }
665: }
666: }
667:
668: }
669:
670: /**
671: * This adds the ValueChangeListener to listen for changes in the
672: * given property.
673: */
674: public void addValueChangeListener(ValueChangeListener vcl,
675: String property) {
676: if (property.endsWith("*")) {
677: String startProperty = property.substring(0, property
678: .length() - 1);
679: Vector listeners = (Vector) VCGlobListeners
680: .get(startProperty);
681: if (listeners == null) {
682: listeners = new Vector();
683: listeners.add(vcl);
684: VCGlobListeners.put(startProperty, listeners);
685: } else {
686: if (!listeners.contains(vcl))
687: listeners.add(vcl);
688: }
689:
690: } else {
691: Vector listeners = (Vector) VCListeners.get(property);
692: if (listeners == null) {
693: listeners = new Vector();
694: listeners.add(vcl);
695: VCListeners.put(property, listeners);
696: } else {
697: if (!listeners.contains(vcl))
698: listeners.add(vcl);
699: }
700: }
701: }
702:
703: /**
704: * This removes the given ValueChangeListener for all the values that
705: * it's listening to.
706: */
707: public void removeValueChangeListener(ValueChangeListener vcl) {
708: Enumeration keys = VCListeners.keys();
709: Vector currentListenerList;
710: while (keys.hasMoreElements()) {
711: currentListenerList = (Vector) VCListeners.get(keys
712: .nextElement());
713: while (currentListenerList != null
714: && currentListenerList.contains(vcl))
715: currentListenerList.remove(vcl);
716: }
717:
718: keys = VCGlobListeners.keys();
719: while (keys.hasMoreElements()) {
720: currentListenerList = (Vector) VCGlobListeners.get(keys
721: .nextElement());
722: while (currentListenerList != null
723: && currentListenerList.contains(vcl))
724: currentListenerList.remove(vcl);
725: }
726: }
727:
728: /**
729: * This removes the given ValueChangeListener from listening on the
730: * given property.
731: */
732: public void removeValueChangeListener(ValueChangeListener vcl,
733: String property) {
734: Vector currentListenerList;
735: currentListenerList = (Vector) VCListeners.get(property);
736: while (currentListenerList != null
737: && currentListenerList.contains(vcl))
738: currentListenerList.remove(vcl);
739:
740: currentListenerList = (Vector) VCGlobListeners.get(property);
741: while (currentListenerList != null
742: && currentListenerList.contains(vcl))
743: currentListenerList.remove(vcl);
744: }
745:
746: /**
747: * Returns all of the ValueChangeListeners registered.
748: */
749: public Map getAllListeners() {
750: HashMap returnValue = new HashMap(VCListeners);
751: returnValue.putAll(VCGlobListeners);
752: return returnValue;
753: }
754:
755: /**
756: * Convert a nibble to a hex character
757: * @param nibble the nibble to convert.
758: */
759: private static char toHex(int nibble) {
760: return hexDigit[(nibble & 0xF)];
761: }
762:
763: /** A table of hex digits */
764: private static final char[] hexDigit = { '0', '1', '2', '3', '4',
765: '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
766:
767: private static final String keyValueSeparators = "=: \t\r\n\f";
768:
769: private static final String strictKeyValueSeparators = "=:";
770:
771: private static final String specialSaveChars = "=: \t\r\n\f#!";
772:
773: private static final String whiteSpaceChars = " \t\r\n\f";
774:
775: /**
776: * Returns the current saveFile.
777: */
778: public File getSaveFile() {
779: return mSaveFile;
780: }
781:
782: /**
783: * Sets the save file.
784: */
785: public void setSaveFile(File newFile) {
786: mSaveFile = newFile;
787: }
788: }
|