001: /*
002: * Copyright (c) 2002-2006 by OpenSymphony
003: * All rights reserved.
004: */
005: package com.opensymphony.xwork.util;
006:
007: import com.opensymphony.xwork.ActionContext;
008: import com.opensymphony.xwork.ActionInvocation;
009: import com.opensymphony.xwork.ModelDriven;
010:
011: import ognl.OgnlRuntime;
012:
013: import org.apache.commons.logging.Log;
014: import org.apache.commons.logging.LogFactory;
015:
016: import java.beans.PropertyDescriptor;
017: import java.lang.reflect.Field;
018: import java.lang.reflect.Method;
019: import java.lang.reflect.InvocationTargetException;
020: import java.text.MessageFormat;
021: import java.util.*;
022:
023: /**
024: * Provides support for localization in XWork.
025: *
026: * <!-- START SNIPPET: searchorder -->
027: * Resource bundles are searched in the following order:<p/>
028: * <p/>
029: * <ol>
030: * <li>ActionClass.properties</li>
031: * <li>BaseClass.properties (all the way to Object.properties)</li>
032: * <li>Interface.properties (every interface and sub-interface)</li>
033: * <li>ModelDriven's model (if implements ModelDriven), for the model object repeat from 1</li>
034: * <li>package.properties (of the directory where class is located and every parent directory all the way to the root directory)</li>
035: * <li>search up the i18n message key hierarchy itself</li>
036: * <li>global resource properties (webwork.custom.i18n.resources) defined in webwork.properties</li>
037: * </ol>
038: * <p/>
039: * <!-- END SNIPPET: searchorder -->
040: *
041: * <!-- START SNIPPET: packagenote -->
042: * To clarify #5, while traversing the package hierarchy, WW will look for a file package.properties:<p/>
043: * com/<br/>
044: * acme/<br/>
045: * package.properties<br/>
046: * actions/<br/>
047: * package.properties<br/>
048: * FooAction.java<br/>
049: * FooAction.properties<br/>
050: * <p/>
051: * If FooAction.properties does not exist, com/acme/action/package.properties will be searched for, if
052: * not found com/acme/package.properties, if not found com/package.properties, etc.
053: * <p/>
054: * <!-- END SNIPPET: packagenote -->
055: *
056: * <!-- START SNIPPET: globalresource -->
057: * A global resource bundle could be specified through the 'webwork.custom.i18n.resources' property in
058: * webwork.properties. The locale can be siwtched by 'webwork.locale' in the webwork.properties as well.
059: * <p/>
060: * <!-- END SNIPPET: globalresource -->
061: *
062: * <!-- START SNIPPET: strutscomparison -->
063: * Struts users should be familiar with the application.properties resource bundle, where you can put all the messages
064: * in the application that are going to be translated. WebWork, though, splits the resource bundles per action or model
065: * class, and you may end up with duplicated messages in those resource bundles. A quick fix for that is to create a
066: * file called ActionSupport.properties in com/opensymphony/xwork and put it on your classpath. This will only work well
067: * if all your actions subclass ActionSupport.
068: * <p/>
069: * <!-- END SNIPPET: strutscomparison -->
070: *
071: * @author Jason Carreira
072: * @author Mark Woon
073: * @author Rainer Hermanns
074: * @author tm_jee
075: *
076: * @author $Date: 2007-06-30 18:15:16 +0200 (Sat, 30 Jun 2007) $ $Id: LocalizedTextUtil.java 1536 2007-06-30 16:15:16Z tm_jee $
077: */
078: public class LocalizedTextUtil {
079:
080: private static final Log _log = LogFactory
081: .getLog(LocalizedTextUtil.class);
082:
083: private static List DEFAULT_RESOURCE_BUNDLES = null;
084: private static final Log LOG = LogFactory
085: .getLog(LocalizedTextUtil.class);
086: private static boolean reloadBundles = false;
087: private static final Collection misses = new HashSet();
088: private static final Map messageFormats = new HashMap();
089:
090: static {
091: clearDefaultResourceBundles();
092: }
093:
094: /**
095: * Clears the internal list of resource bundles.
096: */
097: public static void clearDefaultResourceBundles() {
098: if (DEFAULT_RESOURCE_BUNDLES != null) {
099: DEFAULT_RESOURCE_BUNDLES.clear();
100: }
101: DEFAULT_RESOURCE_BUNDLES = Collections
102: .synchronizedList(new ArrayList());
103: DEFAULT_RESOURCE_BUNDLES
104: .add("com/opensymphony/xwork/xwork-messages");
105: }
106:
107: /**
108: * Should resorce bundles be reloaded.
109: * <p/>
110: * In WW see <code>webwork.i18n.reload</code> property.
111: * @param reloadBundles reload bundles?
112: */
113: public static void setReloadBundles(boolean reloadBundles) {
114: LocalizedTextUtil.reloadBundles = reloadBundles;
115: }
116:
117: /**
118: * Add's the bundle to the internal list of default bundles.
119: * <p/>
120: * If the bundle already exists in the list it will be readded.
121: *
122: * @param resourceBundleName the name of the bundle to add.
123: */
124: public static void addDefaultResourceBundle(
125: String resourceBundleName) {
126: //make sure this doesn't get added more than once
127: DEFAULT_RESOURCE_BUNDLES.remove(resourceBundleName);
128: DEFAULT_RESOURCE_BUNDLES.add(0, resourceBundleName);
129:
130: if (LOG.isDebugEnabled()) {
131: LOG.debug("Added default resource bundle '"
132: + resourceBundleName
133: + "' to default resource bundles = "
134: + DEFAULT_RESOURCE_BUNDLES);
135: }
136: }
137:
138: /**
139: * Builds a {@link java.util.Locale} from a String of the form en_US_foo into a Locale
140: * with language "en", country "US" and variant "foo". This will parse the output of
141: * {@link java.util.Locale#toString()}.
142: *
143: * @param localeStr The locale String to parse.
144: * @param defaultLocale The locale to use if localeStr is <tt>null</tt>.
145: * @return requested Locale
146: */
147: public static Locale localeFromString(String localeStr,
148: Locale defaultLocale) {
149: if ((localeStr == null) || (localeStr.trim().length() == 0)
150: || (localeStr.equals("_"))) {
151: if (defaultLocale != null) {
152: return defaultLocale;
153: }
154: return Locale.getDefault();
155: }
156:
157: int index = localeStr.indexOf('_');
158: if (index < 0) {
159: return new Locale(localeStr);
160: }
161:
162: String language = localeStr.substring(0, index);
163: if (index == localeStr.length()) {
164: return new Locale(language);
165: }
166:
167: localeStr = localeStr.substring(index + 1);
168: index = localeStr.indexOf('_');
169: if (index < 0) {
170: return new Locale(language, localeStr);
171: }
172:
173: String country = localeStr.substring(0, index);
174: if (index == localeStr.length()) {
175: return new Locale(language, country);
176: }
177:
178: localeStr = localeStr.substring(index + 1);
179: return new Locale(language, country, localeStr);
180: }
181:
182: /**
183: * Returns a localized message for the specified key, aTextName. Neither the key nor the
184: * message is evaluated.
185: *
186: * @param aTextName the message key
187: * @param locale the locale the message should be for
188: * @return a localized message based on the specified key, or null if no localized message can be found for it
189: */
190: public static String findDefaultText(String aTextName, Locale locale) {
191: List localList = DEFAULT_RESOURCE_BUNDLES; // it isn't sync'd, but this is so rare, let's do it anyway
192:
193: for (Iterator iterator = localList.iterator(); iterator
194: .hasNext();) {
195: String bundleName = (String) iterator.next();
196:
197: ResourceBundle bundle = findResourceBundle(bundleName,
198: locale);
199: if (bundle != null) {
200: reloadBundles();
201: try {
202: return bundle.getString(aTextName);
203: } catch (MissingResourceException e) {
204: // ignore and try others
205: }
206: }
207: }
208:
209: return null;
210: }
211:
212: /**
213: * Returns a localized message for the specified key, aTextName, substituting variables from the
214: * array of params into the message. Neither the key nor the message is evaluated.
215: *
216: * @param aTextName the message key
217: * @param locale the locale the message should be for
218: * @param params an array of objects to be substituted into the message text
219: * @return A formatted message based on the specified key, or null if no localized message can be found for it
220: */
221: public static String findDefaultText(String aTextName,
222: Locale locale, Object[] params) {
223: String defaultText = findDefaultText(aTextName, locale);
224: if (defaultText != null) {
225: MessageFormat mf = buildMessageFormat(defaultText, locale);
226: return mf.format(params);
227: }
228: return null;
229: }
230:
231: /**
232: * Finds the given resorce bundle by it's name.
233: * <p/>
234: * Will use <code>Thread.currentThread().getContextClassLoader()</code> as the classloader.
235: *
236: * @param aBundleName the name of the bundle (usually it's FQN classname).
237: * @param locale the locale.
238: * @return the bundle, <tt>null</tt> if not found.
239: */
240: public static ResourceBundle findResourceBundle(String aBundleName,
241: Locale locale) {
242: synchronized (misses) {
243: try {
244: if (!misses.contains(aBundleName)) {
245: return ResourceBundle.getBundle(aBundleName,
246: locale, Thread.currentThread()
247: .getContextClassLoader());
248: }
249: } catch (MissingResourceException ex) {
250: misses.add(aBundleName);
251: }
252: }
253:
254: return null;
255: }
256:
257: /**
258: * Calls {@link #findText(Class aClass, String aTextName, Locale locale, String defaultMessage, Object[] args)}
259: * with aTextName as the default message.
260: *
261: * @see #findText(Class aClass, String aTextName, Locale locale, String defaultMessage, Object[] args)
262: */
263: public static String findText(Class aClass, String aTextName,
264: Locale locale) {
265: return findText(aClass, aTextName, locale, aTextName,
266: new Object[0]);
267: }
268:
269: /**
270: * Finds a localized text message for the given key, aTextName. Both the key and the message
271: * itself is evaluated as required. The following algorithm is used to find the requested
272: * message:
273: * <p/>
274: * <ol>
275: * <li>Look for message in aClass' class hierarchy.
276: * <ol>
277: * <li>Look for the message in a resource bundle for aClass</li>
278: * <li>If not found, look for the message in a resource bundle for any implemented interface</li>
279: * <li>If not found, traverse up the Class' hierarchy and repeat from the first sub-step</li>
280: * </ol></li>
281: * <li>If not found and aClass is a {@link ModelDriven} Action, then look for message in
282: * the model's class hierarchy (repeat sub-steps listed above).</li>
283: * <li>If not found, look for message in child property. This is determined by evaluating
284: * the message key as an OGNL expression. For example, if the key is
285: * <i>user.address.state</i>, then it will attempt to see if "user" can be resolved into an
286: * object. If so, repeat the entire process fromthe beginning with the object's class as
287: * aClass and "address.state" as the message key.</li>
288: * <li>If not found, look for the message in aClass' package hierarchy.</li>
289: * <li>If still not found, look for the message in the default resource bundles.</li>
290: * <li>Return defaultMessage</li>
291: * </ol>
292: * <p/>
293: * When looking for the message, if the key indexes a collection (e.g. user.phone[0]) and a
294: * message for that specific key cannot be found, the general form will also be looked up
295: * (i.e. user.phone[*]).
296: * <p/>
297: * If a message is found, it will also be interpolated. Anything within <code>${...}</code>
298: * will be treated as an OGNL expression and evaluated as such.
299: *
300: * @param aClass the class whose name to use as the start point for the search
301: * @param aTextName the key to find the text message for
302: * @param locale the locale the message should be for
303: * @param defaultMessage the message to be returned if no text message can be found in any
304: * resource bundle
305: * @return the localized text, or null if none can be found and no defaultMessage is provided
306: */
307: public static String findText(Class aClass, String aTextName,
308: Locale locale, String defaultMessage, Object[] args) {
309: OgnlValueStack valueStack = ActionContext.getContext()
310: .getValueStack();
311: return findText(aClass, aTextName, locale, defaultMessage,
312: args, valueStack);
313:
314: }
315:
316: /**
317: * <b>This method call will log a warning message (in debug level) if message is not found</b>
318: *
319: * Finds a localized text message for the given key, aTextName. Both the key and the message
320: * itself is evaluated as required. The following algorithm is used to find the requested
321: * message:
322: * <p/>
323: * <ol>
324: * <li>Look for message in aClass' class hierarchy.
325: * <ol>
326: * <li>Look for the message in a resource bundle for aClass</li>
327: * <li>If not found, look for the message in a resource bundle for any implemented interface</li>
328: * <li>If not found, traverse up the Class' hierarchy and repeat from the first sub-step</li>
329: * </ol></li>
330: * <li>If not found and aClass is a {@link ModelDriven} Action, then look for message in
331: * the model's class hierarchy (repeat sub-steps listed above).</li>
332: * <li>If not found, look for message in child property. This is determined by evaluating
333: * the message key as an OGNL expression. For example, if the key is
334: * <i>user.address.state</i>, then it will attempt to see if "user" can be resolved into an
335: * object. If so, repeat the entire process fromthe beginning with the object's class as
336: * aClass and "address.state" as the message key.</li>
337: * <li>If not found, look for the message in aClass' package hierarchy.</li>
338: * <li>If still not found, look for the message in the default resource bundles.</li>
339: * <li>Return defaultMessage</li>
340: * </ol>
341: * <p/>
342: * When looking for the message, if the key indexes a collection (e.g. user.phone[0]) and a
343: * message for that specific key cannot be found, the general form will also be looked up
344: * (i.e. user.phone[*]).
345: * <p/>
346: * If a message is found, it will also be interpolated. Anything within <code>${...}</code>
347: * will be treated as an OGNL expression and evaluated as such.
348: * <p/>
349: * If a message is <b>not</b> found a WARN log will be logged.
350: *
351: * @param aClass the class whose name to use as the start point for the search
352: * @param aTextName the key to find the text message for
353: * @param locale the locale the message should be for
354: * @param defaultMessage the message to be returned if no text message can be found in any
355: * resource bundle
356: * @param valueStack the value stack to use to evaluate expressions instead of the
357: * one in the ActionContext ThreadLocal
358: * @return the localized text, or null if none can be found and no defaultMessage is provided
359: */
360: public static String findText(Class aClass, String aTextName,
361: Locale locale, String defaultMessage, Object[] args,
362: OgnlValueStack valueStack) {
363: return findText(aClass, aTextName, locale, defaultMessage,
364: args, valueStack, true);
365: }
366:
367: /**
368: * Finds a localized text message for the given key, aTextName. Both the key and the message
369: * itself is evaluated as required. The following algorithm is used to find the requested
370: * message:
371: * <p/>
372: * <ol>
373: * <li>Look for message in aClass' class hierarchy.
374: * <ol>
375: * <li>Look for the message in a resource bundle for aClass</li>
376: * <li>If not found, look for the message in a resource bundle for any implemented interface</li>
377: * <li>If not found, traverse up the Class' hierarchy and repeat from the first sub-step</li>
378: * </ol></li>
379: * <li>If not found and aClass is a {@link ModelDriven} Action, then look for message in
380: * the model's class hierarchy (repeat sub-steps listed above).</li>
381: * <li>If not found, look for message in child property. This is determined by evaluating
382: * the message key as an OGNL expression. For example, if the key is
383: * <i>user.address.state</i>, then it will attempt to see if "user" can be resolved into an
384: * object. If so, repeat the entire process fromthe beginning with the object's class as
385: * aClass and "address.state" as the message key.</li>
386: * <li>If not found, look for the message in aClass' package hierarchy.</li>
387: * <li>If still not found, look for the message in the default resource bundles.</li>
388: * <li>Return defaultMessage</li>
389: * </ol>
390: * <p/>
391: * When looking for the message, if the key indexes a collection (e.g. user.phone[0]) and a
392: * message for that specific key cannot be found, the general form will also be looked up
393: * (i.e. user.phone[*]).
394: * <p/>
395: * If a message is found, it will also be interpolated. Anything within <code>${...}</code>
396: * will be treated as an OGNL expression and evaluated as such.
397: * <p/>
398: * If a message is <b>not</b> found a WARN log will be logged.
399: *
400: * @param aClass the class whose name to use as the start point for the search
401: * @param aTextName the key to find the text message for
402: * @param locale the locale the message should be for
403: * @param defaultMessage the message to be returned if no text message can be found in any
404: * resource bundle
405: * @param valueStack the value stack to use to evaluate expressions instead of the
406: * one in the ActionContext ThreadLocal
407: * @param warnIfNoMessageFound log warning message (in debug level) if message is not found.
408: * @return the localized text, or null if none can be found and no defaultMessage is provided
409: */
410: public static String findText(Class aClass, String aTextName,
411: Locale locale, String defaultMessage, Object[] args,
412: OgnlValueStack valueStack, boolean warnIfNoMessageFound) {
413: String indexedTextName = null;
414: if (aTextName == null) {
415: LOG.warn("Trying to find text with null key!");
416: aTextName = "";
417: }
418: // calculate indexedTextName (collection[*]) if applicable
419: if (aTextName.indexOf("[") != -1) {
420: int i = -1;
421:
422: indexedTextName = aTextName;
423:
424: while ((i = indexedTextName.indexOf("[", i + 1)) != -1) {
425: int j = indexedTextName.indexOf("]", i);
426: String a = indexedTextName.substring(0, i);
427: String b = indexedTextName.substring(j);
428: indexedTextName = a + "[*" + b;
429: }
430: }
431:
432: // search up class hierarchy
433: String msg = findMessage(aClass, aTextName, indexedTextName,
434: locale, args, null, valueStack);
435:
436: if (msg != null) {
437: return msg;
438: }
439:
440: if (ModelDriven.class.isAssignableFrom(aClass)) {
441: ActionContext context = ActionContext.getContext();
442: // search up model's class hierarchy
443: ActionInvocation actionInvocation = context
444: .getActionInvocation();
445:
446: // ActionInvocation may be null if we're being run from a Sitemesh filter, so we won't get model texts if this is null
447: if (actionInvocation != null) {
448: Object action = actionInvocation.getAction();
449: Object model = ((ModelDriven) action).getModel();
450: if (model != null) {
451: msg = findMessage(model.getClass(), aTextName,
452: indexedTextName, locale, args, null,
453: valueStack);
454: if (msg != null) {
455: return msg;
456: }
457: }
458: }
459: }
460:
461: // nothing still? alright, search the package hierarchy now
462: for (Class clazz = aClass; (clazz != null)
463: && !clazz.equals(Object.class); clazz = clazz
464: .getSuperclass()) {
465:
466: String basePackageName = clazz.getName();
467: while (basePackageName.lastIndexOf('.') != -1) {
468: basePackageName = basePackageName.substring(0,
469: basePackageName.lastIndexOf('.'));
470: String packageName = basePackageName + ".package";
471: msg = getMessage(packageName, locale, aTextName,
472: valueStack, args);
473:
474: if (msg != null) {
475: return msg;
476: }
477:
478: if (indexedTextName != null) {
479: msg = getMessage(packageName, locale,
480: indexedTextName, valueStack, args);
481:
482: if (msg != null) {
483: return msg;
484: }
485: }
486: }
487: }
488:
489: // see if it's a child property
490: int idx = aTextName.indexOf(".");
491:
492: if (idx != -1) {
493: String newKey = null;
494: String prop = null;
495:
496: if (aTextName
497: .startsWith(XWorkConverter.CONVERSION_ERROR_PROPERTY_PREFIX)) {
498: idx = aTextName.indexOf(".",
499: XWorkConverter.CONVERSION_ERROR_PROPERTY_PREFIX
500: .length());
501:
502: if (idx != -1) {
503: prop = aTextName
504: .substring(
505: XWorkConverter.CONVERSION_ERROR_PROPERTY_PREFIX
506: .length(), idx);
507: newKey = XWorkConverter.CONVERSION_ERROR_PROPERTY_PREFIX
508: + aTextName.substring(idx + 1);
509: }
510: } else {
511: prop = aTextName.substring(0, idx);
512: newKey = aTextName.substring(idx + 1);
513: }
514:
515: if (prop != null) {
516: Object obj = valueStack.findValue(prop);
517: try {
518: Object realTarget = OgnlUtil.getRealTarget(prop,
519: valueStack.getContext(), valueStack
520: .getRoot());
521:
522: if (realTarget != null) {
523: PropertyDescriptor propertyDescriptor = OgnlRuntime
524: .getPropertyDescriptor(realTarget
525: .getClass(), prop);
526: if (propertyDescriptor != null) {
527: Class clazz = propertyDescriptor
528: .getPropertyType();
529: if (clazz != null) {
530: if (obj != null)
531: valueStack.push(obj);
532: msg = findText(clazz, newKey, locale,
533: null, args);
534: if (obj != null)
535: valueStack.pop();
536:
537: if (msg != null) {
538: return msg;
539: }
540: }
541: }
542: }
543: } catch (Exception e) {
544: _log.debug("unable to find property " + prop, e);
545: }
546: }
547: }
548:
549: // get default
550: GetDefaultMessageReturnArg result = null;
551: if (indexedTextName == null) {
552: result = getDefaultMessage(aTextName, locale, valueStack,
553: args, defaultMessage);
554: } else {
555: result = getDefaultMessage(aTextName, locale, valueStack,
556: args, null);
557: if (result.message != null) {
558: return result.message;
559: }
560: result = getDefaultMessage(indexedTextName, locale,
561: valueStack, args, defaultMessage);
562: }
563:
564: // could we find the text, if not log a warn
565: if (warnIfNoMessageFound && unableToFindTextForKey(result)) {
566: String warn = "Unable to find text for key '" + aTextName
567: + "' ";
568: if (indexedTextName != null) {
569: warn += " or indexed key '" + indexedTextName + "' ";
570: }
571: warn += "in class '" + aClass.getName() + "' and locale '"
572: + locale + "'";
573: LOG.debug(warn);
574: }
575:
576: return result != null ? result.message : null;
577: }
578:
579: /**
580: * Determines if we found the text in the bundles.
581: *
582: * @param result the result so far
583: * @return <tt>true</tt> if we could <b>not</b> find the text, <tt>false</tt> if the text was found (=success).
584: */
585: private static boolean unableToFindTextForKey(
586: GetDefaultMessageReturnArg result) {
587: if (result == null || result.message == null) {
588: return true;
589: }
590:
591: // did we find it in the bundle, then no problem?
592: if (result.foundInBundle) {
593: return false;
594: }
595:
596: // not found in bundle
597: return true;
598: }
599:
600: /**
601: * Finds a localized text message for the given key, aTextName, in the specified resource bundle
602: * with aTextName as the default message.
603: * <p/>
604: * If a message is found, it will also be interpolated. Anything within <code>${...}</code>
605: * will be treated as an OGNL expression and evaluated as such.
606: *
607: * @see #findText(java.util.ResourceBundle, String, java.util.Locale, String, Object[])
608: */
609: public static String findText(ResourceBundle bundle,
610: String aTextName, Locale locale) {
611: return findText(bundle, aTextName, locale, aTextName,
612: new Object[0]);
613: }
614:
615: /**
616: * Finds a localized text message for the given key, aTextName, in the specified resource
617: * bundle.
618: * <p/>
619: * If a message is found, it will also be interpolated. Anything within <code>${...}</code>
620: * will be treated as an OGNL expression and evaluated as such.
621: * <p/>
622: * If a message is <b>not</b> found a WARN log will be logged.
623: *
624: * @param bundle the bundle
625: * @param aTextName the key
626: * @param locale the locale
627: * @param defaultMessage the default message to use if no message was found in the bundle
628: * @param args arguments for the message formatter.
629: */
630: public static String findText(ResourceBundle bundle,
631: String aTextName, Locale locale, String defaultMessage,
632: Object[] args) {
633: OgnlValueStack valueStack = ActionContext.getContext()
634: .getValueStack();
635: return findText(bundle, aTextName, locale, defaultMessage,
636: args, valueStack);
637: }
638:
639: /**
640: * <b>This method will log a warning (in debug level) if no message is found.</b>
641: *
642: * Finds a localized text message for the given key, aTextName, in the specified resource
643: * bundle.
644: * <p/>
645: * If a message is found, it will also be interpolated. Anything within <code>${...}</code>
646: * will be treated as an OGNL expression and evaluated as such.
647: * <p/>
648: * If a message is <b>not</b> found a WARN log will be logged.
649: *
650: * @param bundle the bundle
651: * @param aTextName the key
652: * @param locale the locale
653: * @param defaultMessage the default message to use if no message was found in the bundle
654: * @param args arguments for the message formatter.
655: * @param valueStack the OGNL value stack.
656: */
657: public static String findText(ResourceBundle bundle,
658: String aTextName, Locale locale, String defaultMessage,
659: Object[] args, OgnlValueStack valueStack) {
660: return findText(bundle, aTextName, locale, defaultMessage,
661: args, valueStack, true);
662: }
663:
664: /**
665: * Finds a localized text message for the given key, aTextName, in the specified resource
666: * bundle.
667: * <p/>
668: * If a message is found, it will also be interpolated. Anything within <code>${...}</code>
669: * will be treated as an OGNL expression and evaluated as such.
670: * <p/>
671: * If a message is <b>not</b> found a WARN log will be logged.
672: *
673: * @param bundle the bundle
674: * @param aTextName the key
675: * @param locale the locale
676: * @param defaultMessage the default message to use if no message was found in the bundle
677: * @param args arguments for the message formatter.
678: * @param valueStack the OGNL value stack.
679: */
680: public static String findText(ResourceBundle bundle,
681: String aTextName, Locale locale, String defaultMessage,
682: Object[] args, OgnlValueStack valueStack,
683: boolean warnIfNoMessageFound) {
684: try {
685: reloadBundles();
686:
687: String message = TextParseUtil.translateVariables(bundle
688: .getString(aTextName), valueStack);
689: MessageFormat mf = buildMessageFormat(message, locale);
690:
691: return mf.format(args);
692: } catch (MissingResourceException ex) {
693: // ignore
694: }
695:
696: GetDefaultMessageReturnArg result = getDefaultMessage(
697: aTextName, locale, valueStack, args, defaultMessage);
698: if (warnIfNoMessageFound && unableToFindTextForKey(result)) {
699: LOG.warn("Unable to find text for key '" + aTextName
700: + "' in ResourceBundles for locale '" + locale
701: + "'");
702: }
703: return result == null ? null : result.message;
704: }
705:
706: /**
707: * Gets the default message.
708: */
709: private static GetDefaultMessageReturnArg getDefaultMessage(
710: String key, Locale locale, OgnlValueStack valueStack,
711: Object[] args, String defaultMessage) {
712: GetDefaultMessageReturnArg result = null;
713: boolean found = true;
714:
715: if (key != null) {
716: String message = findDefaultText(key, locale);
717:
718: if (message == null) {
719: message = defaultMessage;
720: found = false; // not found in bundles
721: }
722:
723: // defaultMessage may be null
724: if (message != null) {
725: MessageFormat mf = buildMessageFormat(TextParseUtil
726: .translateVariables(message, valueStack),
727: locale);
728:
729: String msg = mf.format(args);
730: result = new GetDefaultMessageReturnArg(msg, found);
731: }
732: }
733:
734: return result;
735: }
736:
737: /**
738: * Gets the message from the named resource bundle.
739: */
740: private static String getMessage(String bundleName, Locale locale,
741: String key, OgnlValueStack valueStack, Object[] args) {
742: ResourceBundle bundle = findResourceBundle(bundleName, locale);
743: if (bundle == null) {
744: return null;
745: }
746:
747: reloadBundles();
748:
749: try {
750: String message = TextParseUtil.translateVariables(bundle
751: .getString(key), valueStack);
752: MessageFormat mf = buildMessageFormat(message, locale);
753: return mf.format(args);
754: } catch (MissingResourceException e) {
755: return null;
756: }
757: }
758:
759: private static MessageFormat buildMessageFormat(String pattern,
760: Locale locale) {
761: MessageFormatKey key = new MessageFormatKey(pattern, locale);
762: MessageFormat format = (MessageFormat) messageFormats.get(key);
763: if (format == null) {
764: format = new MessageFormat(pattern);
765: format.setLocale(locale);
766: format.applyPattern(pattern);
767: messageFormats.put(key, format);
768: }
769:
770: return format;
771: }
772:
773: /**
774: * Traverse up class hierarchy looking for message. Looks at class, then implemented interface,
775: * before going up hierarchy.
776: */
777: private static String findMessage(Class clazz, String key,
778: String indexedKey, Locale locale, Object[] args,
779: Set checked, OgnlValueStack valueStack) {
780: if (checked == null) {
781: checked = new TreeSet();
782: } else if (checked.contains(clazz.getName())) {
783: return null;
784: }
785:
786: // look in properties of this class
787: String msg = getMessage(clazz.getName(), locale, key,
788: valueStack, args);
789:
790: if (msg != null) {
791: return msg;
792: }
793:
794: if (indexedKey != null) {
795: msg = getMessage(clazz.getName(), locale, indexedKey,
796: valueStack, args);
797:
798: if (msg != null) {
799: return msg;
800: }
801: }
802:
803: // look in properties of implemented interfaces
804: Class[] interfaces = clazz.getInterfaces();
805:
806: for (int x = 0; x < interfaces.length; x++) {
807: msg = getMessage(interfaces[x].getName(), locale, key,
808: valueStack, args);
809:
810: if (msg != null) {
811: return msg;
812: }
813:
814: if (indexedKey != null) {
815: msg = getMessage(interfaces[x].getName(), locale,
816: indexedKey, valueStack, args);
817:
818: if (msg != null) {
819: return msg;
820: }
821: }
822: }
823:
824: // traverse up hierarchy
825: if (clazz.isInterface()) {
826: interfaces = clazz.getInterfaces();
827:
828: for (int x = 0; x < interfaces.length; x++) {
829: msg = findMessage(interfaces[x], key, indexedKey,
830: locale, args, checked, valueStack);
831:
832: if (msg != null) {
833: return msg;
834: }
835: }
836: } else {
837: if (!clazz.equals(Object.class) && !clazz.isPrimitive()) {
838: return findMessage(clazz.getSuperclass(), key,
839: indexedKey, locale, args, checked, valueStack);
840: }
841: }
842:
843: return null;
844: }
845:
846: private static void reloadBundles() {
847: if (reloadBundles) {
848: try {
849: clearMap(ResourceBundle.class, null, "cacheList");
850:
851: // now, for the true and utter hack, if we're running in tomcat, clear
852: // it's class loader resource cache as well.
853: clearTomcatCache();
854: } catch (Exception e) {
855: LOG.error("Could not reload resource bundles", e);
856: }
857: }
858: }
859:
860: private static void clearTomcatCache() {
861: ClassLoader loader = Thread.currentThread()
862: .getContextClassLoader();
863: // no need for compilation here.
864: Class cl = loader.getClass();
865:
866: try {
867: if ("org.apache.catalina.loader.WebappClassLoader"
868: .equals(cl.getName())) {
869: clearMap(cl, loader, "resourceEntries");
870: } else {
871: if (LOG.isDebugEnabled()) {
872: LOG.debug("class loader " + cl.getName()
873: + " is not tomcat loader.");
874: }
875: }
876: } catch (Exception e) {
877: LOG.warn("couldn't clear tomcat cache", e);
878: }
879: }
880:
881: private static void clearMap(Class cl, Object obj, String name)
882: throws NoSuchFieldException, IllegalAccessException,
883: NoSuchMethodException, InvocationTargetException {
884: Field field = cl.getDeclaredField(name);
885: field.setAccessible(true);
886:
887: Object cache = field.get(obj);
888:
889: synchronized (cache) {
890: Class ccl = cache.getClass();
891: Method clearMethod = ccl.getMethod("clear", new Class[0]);
892: clearMethod.invoke(cache, new Class[0]);
893: }
894:
895: }
896:
897: /**
898: * Clears all the internal lists.
899: */
900: public static void reset() {
901: clearDefaultResourceBundles();
902:
903: synchronized (misses) {
904: misses.clear();
905: }
906:
907: synchronized (messageFormats) {
908: messageFormats.clear();
909: }
910: }
911:
912: static class MessageFormatKey {
913: String pattern;
914: Locale locale;
915:
916: MessageFormatKey(String pattern, Locale locale) {
917: this .pattern = pattern;
918: this .locale = locale;
919: }
920:
921: public boolean equals(Object o) {
922: if (this == o)
923: return true;
924: if (!(o instanceof MessageFormatKey))
925: return false;
926:
927: final MessageFormatKey messageFormatKey = (MessageFormatKey) o;
928:
929: if (locale != null ? !locale
930: .equals(messageFormatKey.locale)
931: : messageFormatKey.locale != null)
932: return false;
933: if (pattern != null ? !pattern
934: .equals(messageFormatKey.pattern)
935: : messageFormatKey.pattern != null)
936: return false;
937:
938: return true;
939: }
940:
941: public int hashCode() {
942: int result;
943: result = (pattern != null ? pattern.hashCode() : 0);
944: result = 29 * result
945: + (locale != null ? locale.hashCode() : 0);
946: return result;
947: }
948: }
949:
950: static class GetDefaultMessageReturnArg {
951: String message;
952: boolean foundInBundle;
953:
954: public GetDefaultMessageReturnArg(String message,
955: boolean foundInBundle) {
956: this.message = message;
957: this.foundInBundle = foundInBundle;
958: }
959: }
960: }
|