001: /* ====================================================================
002: * The LateralNZ Software License, Version 1.0
003: *
004: * Copyright (c) 2003 LateralNZ. All rights reserved.
005: *
006: * Redistribution and use in source and binary forms, with or without
007: * modification, are permitted provided that the following conditions
008: * are met:
009: *
010: * 1. Redistributions of source code must retain the above copyright
011: * notice, this list of conditions and the following disclaimer.
012: *
013: * 2. Redistributions in binary form must reproduce the above copyright
014: * notice, this list of conditions and the following disclaimer in
015: * the documentation and/or other materials provided with the
016: * distribution.
017: *
018: * 3. The end-user documentation included with the redistribution,
019: * if any, must include the following acknowledgment:
020: * "This product includes software developed by
021: * LateralNZ (http://www.lateralnz.org/) and other third parties."
022: * Alternately, this acknowledgment may appear in the software itself,
023: * if and wherever such third-party acknowledgments normally appear.
024: *
025: * 4. The names "LateralNZ" must not be used to endorse or promote
026: * products derived from this software without prior written
027: * permission. For written permission, please
028: * contact oss@lateralnz.org.
029: *
030: * 5. Products derived from this software may not be called "Panther",
031: * or "Lateral" or "LateralNZ", nor may "PANTHER" or "LATERAL" or
032: * "LATERALNZ" appear in their name, without prior written
033: * permission of LateralNZ.
034: *
035: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
036: * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
037: * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
038: * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
039: * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
040: * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
041: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
042: * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
043: * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
044: * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
045: * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
046: * SUCH DAMAGE.
047: * ====================================================================
048: *
049: * This software consists of voluntary contributions made by many
050: * individuals on behalf of LateralNZ. For more
051: * information on Lateral, please see http://www.lateralnz.com/ or
052: * http://www.lateralnz.org
053: *
054: */
055: package org.lateralnz.common.util;
056:
057: import java.io.File;
058: import java.io.IOException;
059: import java.io.FilenameFilter;
060: import java.util.Arrays;
061: import java.util.Comparator;
062: import java.util.HashMap;
063: import java.util.Iterator;
064: import java.util.Locale;
065: import java.util.Map;
066: import java.util.MissingResourceException;
067: import java.util.regex.*;
068:
069: import org.apache.log4j.Logger;
070: import org.w3c.dom.Document;
071: import org.w3c.dom.Node;
072: import org.w3c.dom.NodeList;
073:
074: import org.lateralnz.common.util.StringUtils;
075: import org.lateralnz.common.util.XMLUtils;
076:
077: /**
078: * an XML-based resource bundle that works similar to a normal java.util.ResourceBundle,
079: * however this handles multiple variants.
080: * For example, the locale CA_fr_var1_var2_var3 would be parsed into:<br />
081: * CA_fr_var1_var2_var3<br />
082: * CA_fr_var1_var2<br />
083: * CA_fr_var1<br />
084: * CA_fr<br />
085: * CA<br />
086: * followed by the default resourcebundle.
087: * Which hopefully should explain the name: Multi Variant Resource Bundle
088: */
089: public class MVResourceBundle implements Constants {
090: private static final Logger log = Logger
091: .getLogger(MVResourceBundle.class.getName());
092: private static final String BRANCH = "branch";
093: private static final String CLASSNAME = MVResourceBundle.class
094: .getName();
095: private static final String DOT_XML = ".xml";
096: private static final String NAME = "name";
097: private static final String PATTERN_SUFFIX = "_?.*\\.xml";
098: private static final String PROPERTY = "property";
099: private static final String PROPERTIES_NODE = "properties";
100:
101: private static final Pattern PROPS_PATTERN = Pattern
102: .compile("(\\$\\{[^}]*\\})");
103:
104: private static HashMap resourceBundles = new HashMap();
105: static {
106: ObjRefUtils.getInstance().add(resourceBundles);
107: }
108:
109: private static final Comparator rbcomp = new Comparator() {
110: public int compare(Object o1, Object o2) {
111: String s1 = (String) o1;
112: String s2 = (String) o2;
113:
114: int tok1 = StringUtils.countOccurrences(s1, '_');
115: int tok2 = StringUtils.countOccurrences(s2, '_');
116:
117: if (tok1 == tok2) {
118: return 0;
119: } else if (tok1 < tok2) {
120: return -1;
121: } else {
122: return 1;
123: }
124: }
125:
126: public boolean equals(Object o1, Object o2) {
127: String s1 = (String) o1;
128: String s2 = (String) o2;
129: return s1.equals(s2);
130: }
131: };
132:
133: protected HashMap props = null;
134: protected Locale this locale = null;
135: private String dir;
136: private String name;
137: private String this name;
138: private HashMap bundles;
139:
140: private MVResourceBundle(HashMap bundles, String dir, String name,
141: String this name) throws MissingResourceException {
142: this .dir = dir;
143: this .name = name;
144: this .this name = this name;
145: // 'thisname' is the resource bundle name followed by the locale string
146: // therefore we need to remove the bundle name and underscore (e.g. "mymessages_")
147: // so we substring name.length()+1
148: int pos = name.length() + 1;
149: if (pos >= this name.length()) {
150: this .this locale = Locale.getDefault();
151: } else {
152: this .this locale = LocaleUtils.getLocale(this name
153: .substring(pos));
154: }
155: this .bundles = bundles;
156:
157: init();
158: }
159:
160: private void init() throws MissingResourceException {
161: try {
162: if (log.isInfoEnabled()) {
163: log.info("initing " + StringUtils.toDirectory(dir)
164: + this name + DOT_XML);
165: }
166: HashMap newprops = getParentProps(bundles, this name);
167:
168: String xml = StringUtils.readFromFile(StringUtils
169: .toDirectory(dir)
170: + this name + DOT_XML);
171: Document doc = XMLUtils.parse(XMLUtils.preprocess(xml));
172:
173: Node n = doc.getFirstChild();
174: NodeList nl = doc.getChildNodes();
175: NodeList nl2 = null;
176: for (int i = 0; i < nl.getLength(); i++) {
177: Node n2 = nl.item(i);
178: if (n2.getNodeName().equals(PROPERTIES_NODE)) {
179: nl2 = n2.getChildNodes();
180: break;
181: }
182: }
183: if (nl2 == null) {
184: throw new Exception("invalid property file");
185: }
186:
187: parse(EMPTY, newprops, nl2);
188:
189: this .props = newprops;
190: } catch (Exception e) {
191: e.printStackTrace();
192: throw new MissingResourceException(e.getMessage(),
193: CLASSNAME, "");
194: }
195:
196: }
197:
198: /**
199: * parse a list of nodes, putting the values of tag 'property' into the map
200: * as strings, and the children of 'branch' as a hashmap (recursive call back to
201: * this method)
202: */
203: private void parse(String branch, HashMap hm, NodeList nl)
204: throws Exception {
205: for (int i = 0; i < nl.getLength(); i++) {
206: Node n = nl.item(i);
207: String name = XMLUtils.getAttributeValue(n, NAME, EMPTY);
208: if (StringUtils.isEmpty(name)) {
209: continue;
210: } else if (n.getNodeName().equals(PROPERTY)) {
211: String val;
212: if (n.getFirstChild() != null) {
213: val = n.getFirstChild().getNodeValue();
214: } else {
215: val = n.getNodeValue();
216: }
217:
218: if (!StringUtils.isEmpty(val) && val.indexOf("${") >= 0) {
219: Matcher mat = PROPS_PATTERN.matcher(val);
220:
221: StringBuffer sb = new StringBuffer();
222: while (mat.find()) {
223: String match = mat.group(1).substring(2,
224: mat.group(1).length() - 1);
225: String prop = System.getProperty(match, EMPTY);
226: if (StringUtils.isEmpty(prop)) {
227: throw new Exception(
228: "missing system property " + match);
229: }
230: mat.appendReplacement(sb, prop);
231: }
232: mat.appendTail(sb);
233: val = sb.toString();
234: }
235: hm.put(branch + name, val);
236: } else if (n.getNodeName().equals(BRANCH)) {
237: parse(branch + name + FORWARD_SLASH, hm, n
238: .getChildNodes());
239: }
240: }
241: }
242:
243: /**
244: * get the default bundle
245: */
246: public static final MVResourceBundle getBundle(String dir,
247: String name) throws MissingResourceException {
248: return getBundle(dir, name, null);
249: }
250:
251: public static final MVResourceBundle getBundle(String dir,
252: String name, Locale locale) throws MissingResourceException {
253: return getBundle(dir, name, EMPTY, locale);
254: }
255:
256: /**
257: * get a bundle with the specified locale. This handles multiple variants. All variants
258: * of the resource bundle are loaded if they have not already been loaded.
259: */
260: public static final MVResourceBundle getBundle(String dir,
261: String name, String prevariant, Locale locale)
262: throws MissingResourceException {
263: if (log.isDebugEnabled()) {
264: log.debug("getBundle " + dir + ", " + name + ", "
265: + prevariant + ", " + locale);
266: }
267:
268: File f = new File(dir);
269: try {
270: dir = f.getCanonicalPath();
271: } catch (IOException ioe) {
272: log.error(ioe);
273: }
274:
275: String mainkey = dir + FORWARD_SLASH + name;
276: if (!resourceBundles.containsKey(mainkey)) {
277: reload(dir, name);
278: }
279:
280: HashMap hm = (HashMap) resourceBundles.get(mainkey);
281: if (hm != null) {
282: String key = name;
283: if (!StringUtils.isEmpty(prevariant)) {
284: key = key + UNDERSCORE + prevariant;
285:
286: if (locale != null) {
287: key = key + UNDERSCORE + locale.getLanguage()
288: + UNDERSCORE + locale.getCountry()
289: + UNDERSCORE + locale.getVariant();
290: }
291: }
292:
293: if (hm.containsKey(key)) {
294: return (MVResourceBundle) hm.get(key);
295: } else {
296: String[] split = key.split(UNDERSCORE);
297: for (int i = 0, j = split.length - 1; i < split.length; i++, j--) {
298: key = StringUtils
299: .fromArray(split, UNDERSCORE, 0, j);
300: if (hm.containsKey(key)) {
301: if (log.isDebugEnabled()) {
302: log.debug("found resources for " + key);
303: }
304: return (MVResourceBundle) hm.get(key);
305: }
306: }
307: if (hm.containsKey(name)) {
308: if (log.isDebugEnabled()) {
309: log
310: .debug("no localised bundle, returning default for "
311: + name);
312: }
313: return (MVResourceBundle) hm.get(name);
314: }
315: }
316: }
317: throw new MissingResourceException("no such bundle " + dir
318: + FORWARD_SLASH + name, CLASSNAME, EMPTY);
319: }
320:
321: /**
322: * load all the variants of a resource bundle
323: */
324: private static final void reload(String dir, String name)
325: throws MissingResourceException {
326: final String pat = name + PATTERN_SUFFIX;
327: FilenameFilter filter = new FilenameFilter() {
328: public boolean accept(File f, String fname) {
329: if (StringUtils.matches(fname, pat)) {
330: return true;
331: } else {
332: return false;
333: }
334: }
335: };
336:
337: File d = new File(dir);
338: if (d.exists() && d.isDirectory()) {
339: String[] list = d.list(filter);
340:
341: // sort the list of files so that we process in order
342: // (in this way we can pick up the parent values for any variant)
343: Arrays.sort(list, rbcomp);
344:
345: if (list != null && list.length > 0) {
346: String key = dir + FORWARD_SLASH + name;
347: HashMap bundles;
348: if (!resourceBundles.containsKey(key)) {
349: synchronized (resourceBundles) {
350: if (!resourceBundles.containsKey(key)) {
351: bundles = new HashMap();
352: resourceBundles.put(key, bundles);
353: }
354: }
355: }
356: bundles = (HashMap) resourceBundles.get(key);
357:
358: synchronized (bundles) {
359: for (int i = 0; i < list.length; i++) {
360: String bkey = list[i].substring(0, list[i]
361: .length() - 4);
362: if (!bundles.containsKey(bkey)) {
363: MVResourceBundle mvrb = new MVResourceBundle(
364: bundles, dir, name, bkey);
365: bundles.put(bkey, mvrb);
366: } else {
367: MVResourceBundle mvrb = (MVResourceBundle) bundles
368: .get(bkey);
369: mvrb.init();
370: }
371: }
372: }
373: }
374: } else {
375: throw new MissingResourceException(
376: "invalid resource bundle " + dir + FORWARD_SLASH
377: + name, CLASSNAME, EMPTY);
378: }
379: }
380:
381: private static final HashMap getParentProps(HashMap bundles,
382: String this name) {
383: if (StringUtils.countOccurrences(this name, '_') < 1) {
384: return new HashMap();
385: } else {
386: // work backwards through the name checking for existence of a resource bundle
387: // (i.e. first look for CA_fr_var1, then CA_fr, then CA, then the default
388: String[] split = this name.split(UNDERSCORE);
389: for (int i = 0, j = split.length - 1; i < split.length; i++, j--) {
390: String key = StringUtils.fromArray(split, UNDERSCORE,
391: 0, j);
392: if (bundles.containsKey(key)) {
393: MVResourceBundle mvrb = (MVResourceBundle) bundles
394: .get(key);
395: return (HashMap) mvrb.props.clone();
396: }
397: }
398: return new HashMap();
399: }
400: }
401:
402: /**
403: * reload property files (note: this reloads all variants)
404: */
405: public final void reload() throws Exception {
406: reload(dir, name);
407: }
408:
409: public final boolean contains(String name) {
410: return props.containsKey(name);
411: }
412:
413: public final String formatString(String name, Object[] args) {
414: String s = getString(name);
415: if (!StringUtils.isEmpty(s)) {
416: return StringUtils.format(s, args);
417: } else {
418: return s;
419: }
420: }
421:
422: public final Locale getLocale() {
423: return this locale;
424: }
425:
426: public final String getString(String name) {
427: return getString(name, true);
428: }
429:
430: public final String getString(String name, boolean throwerror) {
431: if (props.containsKey(name)) {
432: return (String) props.get(name);
433: } else if (throwerror) {
434: throw new MissingResourceException("no such resource '"
435: + name + "'", CLASSNAME, name);
436: } else {
437: return EMPTY;
438: }
439: }
440:
441: /**
442: * NOTE: not a particularly fast method. Be careful how this is used
443: */
444: public final Map getBranchAsMap(String branch) {
445: if (!branch.endsWith(FORWARD_SLASH)) {
446: branch = branch + FORWARD_SLASH;
447: }
448: int len = branch.length();
449: HashMap copy = (HashMap) props.clone();
450: Iterator iter = copy.keySet().iterator();
451: HashMap rtn = new HashMap();
452: while (iter.hasNext()) {
453: String key = (String) iter.next();
454: if (key.startsWith(branch)) {
455: rtn.put(key.substring(len), copy.get(key));
456: }
457: }
458: return rtn;
459: }
460:
461: }
|