001: /* Copyright (c) 2007, The HSQL Development Group
002: * All rights reserved.
003: *
004: * Redistribution and use in source and binary forms, with or without
005: * modification, are permitted provided that the following conditions are met:
006: *
007: * Redistributions of source code must retain the above copyright notice, this
008: * list of conditions and the following disclaimer.
009: *
010: * Redistributions in binary form must reproduce the above copyright notice,
011: * this list of conditions and the following disclaimer in the documentation
012: * and/or other materials provided with the distribution.
013: *
014: * Neither the name of the HSQL Development Group nor the names of its
015: * contributors may be used to endorse or promote products derived from this
016: * software without specific prior written permission.
017: *
018: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
019: * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
020: * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
021: * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
022: * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
023: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
024: * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
025: * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
026: * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
027: * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
028: * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029: */
030:
031: package org.hsqldb.util;
032:
033: import java.util.PropertyResourceBundle;
034: import java.util.Map;
035: import java.util.HashMap;
036: import java.util.Locale;
037: import java.util.ResourceBundle;
038: import java.util.MissingResourceException;
039: import java.util.Enumeration;
040: import java.util.regex.Pattern;
041: import java.util.regex.Matcher;
042: import java.io.InputStream;
043: import java.io.IOException;
044: import java.io.UnsupportedEncodingException;
045:
046: /**
047: * Just like PropertyResourceBundle, except keys mapped to nothing in the
048: * properties file will load the final String value from a text file.
049: *
050: * The use case is where one wants to use a ResourceBundle for Strings,
051: * but some of the Strings are long-- too long to maintain in a Java
052: * .properties file.
053: * By using this class, you can put each such long String in its own
054: * separate file, yet all keys mapped to (non-empty) values in the
055: * .properties file will behave just like regular PropertyResourceBundle
056: * properties.
057: * In this documentation, I call these values read in atomically from
058: * other files <i>referenced</i> values, because the values are not directly
059: * in the .properties file, but are "referenced" in the .properties file
060: * by virtue of the empty value for the key.
061: *
062: * You use this class in the same way as you would traditionally use
063: * ResourceBundle:
064: * <PRE>
065: * import org.hsqldb.util..RefCapablePropertyResourceBundle;
066: * ...
067: * RefCapablePropertyResourceBundle bundle =
068: * RefCapablePropertyResourceBundle.getBundle("subdir.xyz");
069: * System.out.println("Value for '1' = (" + bundle.getString("1") + ')');
070: * </PRE>
071: *
072: * Just like PropertyResourceBundle, the .properties file and the
073: * <i>referenced</i> files are read in from the classpath by a class loader,
074: * according to the normal ResourceBundle rules.
075: * To eliminate the need to prohibit the use of any strings in the .properties
076: * values, and to enforce consistency, you <b>must</b> use the following rules
077: * to when putting your referenced files into place.
078: * <P/>
079: * REFERENCED FILE DIRECTORY is a directory named with the base name of the
080: * properties file, and in the same parent directory. So, the referenced
081: * file directory <CODE>/a/b/c/greentea</CODE> is used to hold all reference
082: * files for properties files <CODE>/a/b/c/greentea_en_us.properties</CODE>,
083: * <CODE>/a/b/c/greentea_de.properties</CODE>,
084: * <CODE>/a/b/c/greentea.properties</CODE>, etc.
085: * (BTW, according to ResourceBundle rules, this resource should be looked
086: * up with name "a.b.c.greentea", not "/a/b/c..." or "a/b/c").
087: * REFERENCED FILES themselves all have the base name of the property key,
088: * with locale appendages exactly as the <i>referring</i> properties files
089: * has, plus the suffix <CODE>.text</CODE>.
090: * <P/>
091: * So, if we have the following line in
092: * <CODE>/a/b/c/greentea_de.properties</CODE>:
093: * <PRE>
094: * 1: eins
095: * </PRE>
096: * then you <b>must</b> have a reference text file
097: * <CODE>/a/b/c/greentea/1_de.properties</CODE>:
098: * <P/>
099: * In reference text files,
100: * sequences of "\r", "\n" and "\r\n" are all translated to the line
101: * delimiter for your platform (System property <CODE>line.separator</CODE>).
102: * If one of those sequences exists at the very end of the file, it will be
103: * eliminated (so, if you really want getString() to end with a line delimiter,
104: * end your file with two of them).
105: * (The file itself is never modified-- I'm talking about the value returned
106: * by <CODE>getString(String)</CODE>.
107: *
108: * To prevent throwing at runtime due to unset variables, use a wrapper class
109: * like SqltoolRB (use SqltoolRB.java as a template).
110: * To prevent throwing at runtime due to unset System Properties, or
111: * insufficient parameters passed to getString(String, String[]), set the
112: * behavior values appropriately.
113: *
114: * Just like all Properties files, referenced files must use ISO-8859-1
115: * encoding, with unicode escapes for characters outside of ISO-8859-1
116: * character set. But, unlike Properties files, \ does not need to be
117: * escaped for normal usage.
118: *
119: * The getString() methods with more than one parameter substitute for
120: * "positional" parameters of the form "%{1}".
121: * The getExpandedString() methods substitute for System Property names
122: * of the form "${1}".
123: * In both cases, you can interpose :+ and a string between the variable
124: * name and the closing }. This works just like the Bourne shell
125: * ${x:+y} feature. If "x" is set, then "y" is returned, and "y" may
126: * contain references to the original variable without the curly braces.
127: * In this file, I refer to the y text as the "conditional string".
128: * One example of each type:
129: * <PRE>
130: * Out val = (${condlSysProp:+Prop condlSysProp is set to $condlSysProp.})
131: * Out val = (%{2:+Pos Var #2 is set to %2.})
132: * OUTPUT if neither are set:
133: * Out val = ()
134: * Out val = ()
135: * OUTPUT if condlSysProp=alpha and condlPLvar=beta:
136: * Out val = (Prop condlSysProp is set to alpha.)
137: * Out val = (Pos Var #2 is set to beta.)
138: * </PRE>
139: * This feature has the following limitations.
140: * <UL>
141: * <LI>The conditional string may only contain the primary variable.
142: * <LI>Inner instances of the primary variable may not use curly braces,
143: * and therefore the variable name must end at a word boundary.
144: * </UL>
145: * The conditional string may span newlines, and it is often very useful
146: * to do so.
147: *
148: * @see java.util.PropertyResourceBundle
149: * @see java.util.ResourceBundle
150: * @author blaine.simpson@admc.com
151: */
152: public class RefCapablePropertyResourceBundle {
153: private PropertyResourceBundle wrappedBundle;
154: private String baseName;
155: private String language, country, variant;
156: static private Map allBundles = new HashMap();
157: public static String LS = System.getProperty("line.separator");
158: private Pattern sysPropVarPattern = Pattern
159: .compile("(?s)\\Q${\\E([^}]+?)(?:\\Q:+\\E([^}]+))?\\Q}");
160: private Pattern posPattern = Pattern
161: .compile("(?s)\\Q%{\\E(\\d)(?:\\Q:+\\E([^}]+))?\\Q}");
162: private ClassLoader loader; // Needed to load referenced files
163:
164: public static final int THROW_BEHAVIOR = 0;
165: public static final int EMPTYSTRING_BEHAVIOR = 1;
166: public static final int NOOP_BEHAVIOR = 2;
167:
168: public Enumeration getKeys() {
169: return wrappedBundle.getKeys();
170: }
171:
172: private RefCapablePropertyResourceBundle(String baseName,
173: PropertyResourceBundle wrappedBundle, ClassLoader loader) {
174: this .baseName = baseName;
175: this .wrappedBundle = wrappedBundle;
176: Locale locale = wrappedBundle.getLocale();
177: this .loader = loader;
178: language = locale.getLanguage();
179: country = locale.getCountry();
180: variant = locale.getVariant();
181: if (language.length() < 1)
182: language = null;
183: if (country.length() < 1)
184: country = null;
185: if (variant.length() < 1)
186: variant = null;
187: }
188:
189: /**
190: * Same as getString(), but expands System Variables specified in
191: * property values like ${sysvarname}.
192: */
193: public String getExpandedString(String key, int behavior) {
194: String s = getString(key);
195: Matcher matcher = sysPropVarPattern.matcher(s);
196: int previousEnd = 0;
197: StringBuffer sb = new StringBuffer();
198: String varName, varValue;
199: String condlVal; // Conditional : value
200: while (matcher.find()) {
201: varName = matcher.group(1);
202: condlVal = ((matcher.groupCount() > 1) ? matcher.group(2)
203: : null);
204: varValue = System.getProperty(varName);
205: if (condlVal != null) {
206: // Replace varValue (the value to be substituted), with
207: // the post-:+ portion of the expression.
208: varValue = ((varValue == null) ? "" : condlVal
209: .replaceAll("\\Q$" + varName + "\\E\\b",
210: RefCapablePropertyResourceBundle
211: .literalize(varValue)));
212: }
213: if (varValue == null)
214: switch (behavior) {
215: case THROW_BEHAVIOR:
216: throw new RuntimeException(
217: "No Sys Property set for variable '"
218: + varName + "' in property value ("
219: + s + ").");
220: case EMPTYSTRING_BEHAVIOR:
221: varValue = "";
222: case NOOP_BEHAVIOR:
223: break;
224: default:
225: throw new RuntimeException(
226: "Undefined value for behavior: " + behavior);
227: }
228: sb
229: .append(s.substring(previousEnd, matcher.start())
230: + ((varValue == null) ? matcher.group()
231: : varValue));
232: previousEnd = matcher.end();
233: }
234: return (previousEnd < 1) ? s : (sb.toString() + s
235: .substring(previousEnd));
236: }
237:
238: /**
239: * Replaces positional substitution patterns of the form %{\d} with
240: * corresponding element of the given subs array.
241: * Note that %{\d} numbers are 1-based, so we lok for subs[x-1].
242: */
243: public String posSubst(String s, String[] subs, int behavior) {
244: Matcher matcher = posPattern.matcher(s);
245: int previousEnd = 0;
246: StringBuffer sb = new StringBuffer();
247: String varValue;
248: int varIndex;
249: String condlVal; // Conditional : value
250: while (matcher.find()) {
251: varIndex = Integer.parseInt(matcher.group(1)) - 1;
252: condlVal = ((matcher.groupCount() > 1) ? matcher.group(2)
253: : null);
254: varValue = ((varIndex < subs.length) ? subs[varIndex]
255: : null);
256: if (condlVal != null) {
257: // Replace varValue (the value to be substituted), with
258: // the post-:+ portion of the expression.
259: varValue = ((varValue == null) ? "" : condlVal
260: .replaceAll("\\Q%" + (varIndex + 1) + "\\E\\b",
261: RefCapablePropertyResourceBundle
262: .literalize(varValue)));
263: }
264: // System.err.println("Behavior: " + behavior);
265: if (varValue == null)
266: switch (behavior) {
267: case THROW_BEHAVIOR:
268: throw new RuntimeException(
269: Integer.toString(subs.length)
270: + " positional values given, but property string "
271: + "contains (" + matcher.group()
272: + ").");
273: case EMPTYSTRING_BEHAVIOR:
274: varValue = "";
275: case NOOP_BEHAVIOR:
276: break;
277: default:
278: throw new RuntimeException(
279: "Undefined value for behavior: " + behavior);
280: }
281: sb
282: .append(s.substring(previousEnd, matcher.start())
283: + ((varValue == null) ? matcher.group()
284: : varValue));
285: previousEnd = matcher.end();
286: }
287: return (previousEnd < 1) ? s : (sb.toString() + s
288: .substring(previousEnd));
289: }
290:
291: public String getExpandedString(String key, String[] subs,
292: int missingPropertyBehavior, int missingPosValueBehavior) {
293: return posSubst(
294: getExpandedString(key, missingPropertyBehavior), subs,
295: missingPosValueBehavior);
296: }
297:
298: public String getString(String key, String[] subs, int behavior) {
299: return posSubst(getString(key), subs, behavior);
300: }
301:
302: /**
303: * Just identifies this RefCapablePropertyResourceBundle instance.
304: */
305: public String toString() {
306: return baseName + " for " + language + " / " + country + " / "
307: + variant;
308: }
309:
310: /**
311: * Returns value defined in this RefCapablePropertyResourceBundle's
312: * .properties file, unless that value is empty.
313: * If the value in the .properties file is empty, then this returns
314: * the entire contents of the referenced text file.
315: *
316: * @see ResourceBundle#get(String)
317: */
318: public String getString(String key) {
319: String value = wrappedBundle.getString(key);
320: if (value.length() > 0)
321: return value;
322: value = getStringFromFile(key);
323: // For conciseness and sanity, get rid of all \r's so that \n
324: // will definitively be our line breaks.
325: if (value.indexOf('\r') > -1)
326: value = value.replaceAll("\\r\\n", "\n").replaceAll("\\r",
327: "\n");
328: if (value.length() > 0
329: && value.charAt(value.length() - 1) == '\n')
330: value = value.substring(0, value.length() - 1);
331: if (!LS.equals("\n"))
332: value = value.replaceAll("\\n", LS);
333: return value;
334: }
335:
336: /**
337: * Use like java.util.ResourceBundle.getBundle(String).
338: *
339: * ClassLoader is required for our getBundles()s, since it is impossible
340: * to get the "caller's" ClassLoader without using JNI (i.e., with pure
341: * Java).
342: *
343: * @see ResourceBundle#getBundle(String)
344: */
345: public static RefCapablePropertyResourceBundle getBundle(
346: String baseName, ClassLoader loader) {
347: return getRef(baseName, ResourceBundle.getBundle(baseName,
348: Locale.getDefault(), loader), loader);
349: }
350:
351: /**
352: * Use exactly like java.util.ResourceBundle.get(String, Locale, ClassLoader).
353: *
354: * @see ResourceBundle#getBundle(String, Locale, ClassLoader)
355: */
356: public static RefCapablePropertyResourceBundle getBundle(
357: String baseName, Locale locale, ClassLoader loader) {
358: return getRef(baseName, ResourceBundle.getBundle(baseName,
359: locale, loader), loader);
360: }
361:
362: /**
363: * Return a ref to a new or existing RefCapablePropertyResourceBundle,
364: * or throw a MissingResourceException.
365: */
366: static private RefCapablePropertyResourceBundle getRef(
367: String baseName, ResourceBundle rb, ClassLoader loader) {
368: if (!(rb instanceof PropertyResourceBundle))
369: throw new MissingResourceException(
370: "Found a Resource Bundle, but it is a "
371: + rb.getClass().getName(),
372: PropertyResourceBundle.class.getName(), null);
373: if (allBundles.containsKey(rb))
374: return (RefCapablePropertyResourceBundle) allBundles
375: .get(rb);
376: RefCapablePropertyResourceBundle newPRAFP = new RefCapablePropertyResourceBundle(
377: baseName, (PropertyResourceBundle) rb, loader);
378: allBundles.put(rb, newPRAFP);
379: return newPRAFP;
380: }
381:
382: /**
383: * Recursive
384: */
385: private InputStream getMostSpecificStream(String key, String l,
386: String c, String v) {
387: String filePath = baseName.replace('.', '/') + '/' + key
388: + ((l == null) ? "" : ("_" + l))
389: + ((c == null) ? "" : ("_" + c))
390: + ((v == null) ? "" : ("_" + v)) + ".text";
391: // System.err.println("Seeking " + filePath);
392: InputStream is = loader.getResourceAsStream(filePath);
393: // N.b. If were using Class.getRes... instead of ClassLoader.getRes...
394: // we would need to previx the path with "/".
395: return (is == null && l != null) ? getMostSpecificStream(key,
396: ((c == null) ? null : l), ((v == null) ? null : c),
397: null) : is;
398: }
399:
400: private String getStringFromFile(String key) {
401: byte[] ba = null;
402: int bytesread = 0;
403: int retval;
404: InputStream inputStream = getMostSpecificStream(key, language,
405: country, variant);
406: if (inputStream == null)
407: throw new MissingResourceException(
408: "Key '"
409: + key
410: + "' is present in .properties file with no value, yet "
411: + "text file resource is missing",
412: RefCapablePropertyResourceBundle.class.getName(),
413: key);
414: try {
415: try {
416: ba = new byte[inputStream.available()];
417: } catch (RuntimeException re) {
418: throw new MissingResourceException(
419: "Resource is too big to read in '"
420: + key
421: + "' value in one "
422: + "gulp.\nPlease run the program with more RAM "
423: + "(try Java -Xm* switches).: " + re,
424: RefCapablePropertyResourceBundle.class
425: .getName(), key);
426: } catch (IOException ioe) {
427: throw new MissingResourceException(
428: "Failed to read in value for key '" + key
429: + "': " + ioe,
430: RefCapablePropertyResourceBundle.class
431: .getName(), key);
432: }
433: try {
434: while (bytesread < ba.length
435: && (retval = inputStream.read(ba, bytesread,
436: ba.length - bytesread)) > 0) {
437: bytesread += retval;
438: }
439: } catch (IOException ioe) {
440: throw new MissingResourceException(
441: "Failed to read in value for '" + key + "': "
442: + ioe,
443: RefCapablePropertyResourceBundle.class
444: .getName(), key);
445: }
446: } finally {
447: try {
448: inputStream.close();
449: } catch (IOException ioe) {
450: System.err.println("Failed to close input stream: "
451: + ioe);
452: }
453: }
454: if (bytesread != ba.length) {
455: throw new MissingResourceException(
456: "Didn't read all bytes. Read in " + bytesread
457: + " bytes out of " + ba.length
458: + " bytes for key '" + key + "'",
459: RefCapablePropertyResourceBundle.class.getName(),
460: key);
461: }
462: try {
463: return new String(ba, "ISO-8859-1");
464: } catch (UnsupportedEncodingException uee) {
465: throw new RuntimeException(uee);
466: } catch (RuntimeException re) {
467: throw new MissingResourceException("Value for key '" + key
468: + "' too big to convert to String. "
469: + "Please run the program with more RAM "
470: + "(try Java -Xm* switches).: " + re,
471: RefCapablePropertyResourceBundle.class.getName(),
472: key);
473: }
474: }
475:
476: /**
477: * Escape \ and $ characters in replacement strings so that nothing
478: * funny happens.
479: *
480: * Once we can use Java 1.5, wipe out this method and use
481: * java.util.regex.matcher.QuoteReplacement() instead.
482: */
483: public static String literalize(String s) {
484: if ((s.indexOf('\\') == -1) && (s.indexOf('$') == -1)) {
485: return s;
486: }
487: StringBuffer sb = new StringBuffer();
488: for (int i = 0; i < s.length(); i++) {
489: char c = s.charAt(i);
490: switch (c) {
491: case '\\':
492: sb.append('\\');
493: sb.append('\\');
494: break;
495: case '$':
496: sb.append('\\');
497: sb.append('$');
498: break;
499: default:
500: sb.append(c);
501: break;
502: }
503: }
504: return sb.toString();
505: }
506: }
|