1 /*
2 * Copyright (c) 2002-2006 by OpenSymphony
3 * All rights reserved.
4 */
5 package com.opensymphony.xwork2.util;
6
7 import java.beans.PropertyDescriptor;
8 import java.lang.reflect.Field;
9 import java.lang.reflect.InvocationTargetException;
10 import java.lang.reflect.Method;
11 import java.text.MessageFormat;
12 import java.util.ArrayList;
13 import java.util.Collections;
14 import java.util.HashMap;
15 import java.util.Iterator;
16 import java.util.List;
17 import java.util.Locale;
18 import java.util.Map;
19 import java.util.MissingResourceException;
20 import java.util.ResourceBundle;
21 import java.util.Set;
22 import java.util.TreeSet;
23
24 import com.opensymphony.xwork2.ActionContext;
25 import com.opensymphony.xwork2.ActionInvocation;
26 import com.opensymphony.xwork2.ModelDriven;
27 import com.opensymphony.xwork2.conversion.impl.XWorkConverter;
28 import com.opensymphony.xwork2.util.logging.Logger;
29 import com.opensymphony.xwork2.util.logging.LoggerFactory;
30 import com.opensymphony.xwork2.util.reflection.ReflectionProviderFactory;
31
32
33 /**
34 * Provides support for localization in XWork.
35 *
36 * <!-- START SNIPPET: searchorder -->
37 * Resource bundles are searched in the following order:<p/>
38 * <p/>
39 * <ol>
40 * <li>ActionClass.properties</li>
41 * <li>Interface.properties (every interface and sub-interface)</li>
42 * <li>BaseClass.properties (all the way to Object.properties)</li>
43 * <li>ModelDriven's model (if implements ModelDriven), for the model object repeat from 1</li>
44 * <li>package.properties (of the directory where class is located and every parent directory all the way to the root directory)</li>
45 * <li>search up the i18n message key hierarchy itself</li>
46 * <li>global resource properties</li>
47 * </ol>
48 * <p/>
49 * <!-- END SNIPPET: searchorder -->
50 *
51 * <!-- START SNIPPET: packagenote -->
52 * To clarify #5, while traversing the package hierarchy, Struts 2 will look for a file package.properties:<p/>
53 * com/<br/>
54 * acme/<br/>
55 * package.properties<br/>
56 * actions/<br/>
57 * package.properties<br/>
58 * FooAction.java<br/>
59 * FooAction.properties<br/>
60 * <p/>
61 * If FooAction.properties does not exist, com/acme/action/package.properties will be searched for, if
62 * not found com/acme/package.properties, if not found com/package.properties, etc.
63 * <p/>
64 * <!-- END SNIPPET: packagenote -->
65 *
66 * <!-- START SNIPPET: globalresource -->
67 * A global resource bundle could be specified programatically, as well as the locale.
68 * <p/>
69 * <!-- END SNIPPET: globalresource -->
70 *
71 * @author Jason Carreira
72 * @author Mark Woon
73 * @author Rainer Hermanns
74 * @author tm_jee
75 *
76 * @version $Date: 2007-11-16 21:10:07 +0100 (Fri, 16 Nov 2007) $ $Id: LocalizedTextUtil.java 1667 2007-11-16 20:10:07Z mrdon $
77 */
78 public class LocalizedTextUtil {
79
80 private static List DEFAULT_RESOURCE_BUNDLES = null;
81 private static final Logger LOG = LoggerFactory.getLogger(LocalizedTextUtil.class);
82 private static boolean reloadBundles = false;
83 private static final Map<String, String> misses = new HashMap<String, String>();
84 private static final Map messageFormats = new HashMap();
85
86 static {
87 clearDefaultResourceBundles();
88 }
89
90
91 /**
92 * Clears the internal list of resource bundles.
93 */
94 public static void clearDefaultResourceBundles() {
95 if (DEFAULT_RESOURCE_BUNDLES != null) {
96 DEFAULT_RESOURCE_BUNDLES.clear();
97 }
98 DEFAULT_RESOURCE_BUNDLES = Collections.synchronizedList(new ArrayList());
99 DEFAULT_RESOURCE_BUNDLES.add("com/opensymphony/xwork2/xwork-messages");
100 }
101
102 /**
103 * Should resorce bundles be reloaded.
104 * @param reloadBundles reload bundles?
105 */
106 public static void setReloadBundles(boolean reloadBundles) {
107 LocalizedTextUtil.reloadBundles = reloadBundles;
108 }
109
110 /**
111 * Add's the bundle to the internal list of default bundles.
112 * <p/>
113 * If the bundle already exists in the list it will be readded.
114 *
115 * @param resourceBundleName the name of the bundle to add.
116 */
117 public static void addDefaultResourceBundle(String resourceBundleName) {
118 //make sure this doesn't get added more than once
119 DEFAULT_RESOURCE_BUNDLES.remove(resourceBundleName);
120 DEFAULT_RESOURCE_BUNDLES.add(0, resourceBundleName);
121
122 if (LOG.isDebugEnabled()) {
123 LOG.debug("Added default resource bundle '" + resourceBundleName + "' to default resource bundles = " + DEFAULT_RESOURCE_BUNDLES);
124 }
125 }
126
127 /**
128 * Builds a {@link java.util.Locale} from a String of the form en_US_foo into a Locale
129 * with language "en", country "US" and variant "foo". This will parse the output of
130 * {@link java.util.Locale#toString()}.
131 *
132 * @param localeStr The locale String to parse.
133 * @param defaultLocale The locale to use if localeStr is <tt>null</tt>.
134 * @return requested Locale
135 */
136 public static Locale localeFromString(String localeStr, Locale defaultLocale) {
137 if ((localeStr == null) || (localeStr.trim().length() == 0) || (localeStr.equals("_"))) {
138 if ( defaultLocale != null) {
139 return defaultLocale;
140 }
141 return Locale.getDefault();
142 }
143
144 int index = localeStr.indexOf('_');
145 if (index < 0) {
146 return new Locale(localeStr);
147 }
148
149 String language = localeStr.substring(0, index);
150 if (index == localeStr.length()) {
151 return new Locale(language);
152 }
153
154 localeStr = localeStr.substring(index + 1);
155 index = localeStr.indexOf('_');
156 if (index < 0) {
157 return new Locale(language, localeStr);
158 }
159
160 String country = localeStr.substring(0, index);
161 if (index == localeStr.length()) {
162 return new Locale(language, country);
163 }
164
165 localeStr = localeStr.substring(index + 1);
166 return new Locale(language, country, localeStr);
167 }
168
169 /**
170 * Returns a localized message for the specified key, aTextName. Neither the key nor the
171 * message is evaluated.
172 *
173 * @param aTextName the message key
174 * @param locale the locale the message should be for
175 * @return a localized message based on the specified key, or null if no localized message can be found for it
176 */
177 public static String findDefaultText(String aTextName, Locale locale) {
178 List localList = DEFAULT_RESOURCE_BUNDLES; // it isn't sync'd, but this is so rare, let's do it anyway
179
180 for (Iterator iterator = localList.iterator(); iterator.hasNext();) {
181 String bundleName = (String) iterator.next();
182
183 ResourceBundle bundle = findResourceBundle(bundleName, locale);
184 if (bundle != null) {
185 reloadBundles();
186 try {
187 return bundle.getString(aTextName);
188 } catch (MissingResourceException e) {
189 // ignore and try others
190 }
191 }
192 }
193
194 return null;
195 }
196
197 /**
198 * Returns a localized message for the specified key, aTextName, substituting variables from the
199 * array of params into the message. Neither the key nor the message is evaluated.
200 *
201 * @param aTextName the message key
202 * @param locale the locale the message should be for
203 * @param params an array of objects to be substituted into the message text
204 * @return A formatted message based on the specified key, or null if no localized message can be found for it
205 */
206 public static String findDefaultText(String aTextName, Locale locale, Object[] params) {
207 String defaultText = findDefaultText(aTextName, locale);
208 if (defaultText != null) {
209 MessageFormat mf = buildMessageFormat(defaultText, locale);
210 return mf.format(params);
211 }
212 return null;
213 }
214
215 /**
216 * Finds the given resorce bundle by it's name.
217 * <p/>
218 * Will use <code>Thread.currentThread().getContextClassLoader()</code> as the classloader.
219 *
220 * @param aBundleName the name of the bundle (usually it's FQN classname).
221 * @param locale the locale.
222 * @return the bundle, <tt>null</tt> if not found.
223 */
224 public static ResourceBundle findResourceBundle(String aBundleName, Locale locale) {
225 synchronized (misses) {
226 String key = createMissesKey(aBundleName, locale);
227 try {
228 if (!misses.containsKey(key)) {
229 return ResourceBundle.getBundle(aBundleName, locale, Thread.currentThread().getContextClassLoader());
230 }
231 } catch (MissingResourceException ex) {
232 misses.put(key, aBundleName);
233 }
234 }
235
236 return null;
237 }
238
239 /**
240 * Creates a key to used for lookup/storing in the bundle misses cache.
241 *
242 * @param aBundleName the name of the bundle (usually it's FQN classname).
243 * @param locale the locale.
244 * @return the key to use for lookup/storing in the bundle misses cache.
245 */
246 private static String createMissesKey(String aBundleName, Locale locale) {
247 return aBundleName + "_" + locale.toString();
248 }
249
250 /**
251 * Calls {@link #findText(Class aClass, String aTextName, Locale locale, String defaultMessage, Object[] args)}
252 * with aTextName as the default message.
253 *
254 * @see #findText(Class aClass, String aTextName, Locale locale, String defaultMessage, Object[] args)
255 */
256 public static String findText(Class aClass, String aTextName, Locale locale) {
257 return findText(aClass, aTextName, locale, aTextName, new Object[0]);
258 }
259
260 /**
261 * Finds a localized text message for the given key, aTextName. Both the key and the message
262 * itself is evaluated as required. The following algorithm is used to find the requested
263 * message:
264 * <p/>
265 * <ol>
266 * <li>Look for message in aClass' class hierarchy.
267 * <ol>
268 * <li>Look for the message in a resource bundle for aClass</li>
269 * <li>If not found, look for the message in a resource bundle for any implemented interface</li>
270 * <li>If not found, traverse up the Class' hierarchy and repeat from the first sub-step</li>
271 * </ol></li>
272 * <li>If not found and aClass is a {@link ModelDriven} Action, then look for message in
273 * the model's class hierarchy (repeat sub-steps listed above).</li>
274 * <li>If not found, look for message in child property. This is determined by evaluating
275 * the message key as an OGNL expression. For example, if the key is
276 * <i>user.address.state</i>, then it will attempt to see if "user" can be resolved into an
277 * object. If so, repeat the entire process fromthe beginning with the object's class as
278 * aClass and "address.state" as the message key.</li>
279 * <li>If not found, look for the message in aClass' package hierarchy.</li>
280 * <li>If still not found, look for the message in the default resource bundles.</li>
281 * <li>Return defaultMessage</li>
282 * </ol>
283 * <p/>
284 * When looking for the message, if the key indexes a collection (e.g. user.phone[0]) and a
285 * message for that specific key cannot be found, the general form will also be looked up
286 * (i.e. user.phone[*]).
287 * <p/>
288 * If a message is found, it will also be interpolated. Anything within <code>${...}</code>
289 * will be treated as an OGNL expression and evaluated as such.
290 *
291 * @param aClass the class whose name to use as the start point for the search
292 * @param aTextName the key to find the text message for
293 * @param locale the locale the message should be for
294 * @param defaultMessage the message to be returned if no text message can be found in any
295 * resource bundle
296 * @return the localized text, or null if none can be found and no defaultMessage is provided
297 */
298 public static String findText(Class aClass, String aTextName, Locale locale, String defaultMessage, Object[] args) {
299 ValueStack valueStack = ActionContext.getContext().getValueStack();
300 return findText(aClass, aTextName, locale, defaultMessage, args, valueStack);
301
302 }
303
304 /**
305 * Finds a localized text message for the given key, aTextName. Both the key and the message
306 * itself is evaluated as required. The following algorithm is used to find the requested
307 * message:
308 * <p/>
309 * <ol>
310 * <li>Look for message in aClass' class hierarchy.
311 * <ol>
312 * <li>Look for the message in a resource bundle for aClass</li>
313 * <li>If not found, look for the message in a resource bundle for any implemented interface</li>
314 * <li>If not found, traverse up the Class' hierarchy and repeat from the first sub-step</li>
315 * </ol></li>
316 * <li>If not found and aClass is a {@link ModelDriven} Action, then look for message in
317 * the model's class hierarchy (repeat sub-steps listed above).</li>
318 * <li>If not found, look for message in child property. This is determined by evaluating
319 * the message key as an OGNL expression. For example, if the key is
320 * <i>user.address.state</i>, then it will attempt to see if "user" can be resolved into an
321 * object. If so, repeat the entire process fromthe beginning with the object's class as
322 * aClass and "address.state" as the message key.</li>
323 * <li>If not found, look for the message in aClass' package hierarchy.</li>
324 * <li>If still not found, look for the message in the default resource bundles.</li>
325 * <li>Return defaultMessage</li>
326 * </ol>
327 * <p/>
328 * When looking for the message, if the key indexes a collection (e.g. user.phone[0]) and a
329 * message for that specific key cannot be found, the general form will also be looked up
330 * (i.e. user.phone[*]).
331 * <p/>
332 * If a message is found, it will also be interpolated. Anything within <code>${...}</code>
333 * will be treated as an OGNL expression and evaluated as such.
334 * <p/>
335 * If a message is <b>not</b> found a WARN log will be logged.
336 *
337 * @param aClass the class whose name to use as the start point for the search
338 * @param aTextName the key to find the text message for
339 * @param locale the locale the message should be for
340 * @param defaultMessage the message to be returned if no text message can be found in any
341 * resource bundle
342 * @param valueStack the value stack to use to evaluate expressions instead of the
343 * one in the ActionContext ThreadLocal
344 * @return the localized text, or null if none can be found and no defaultMessage is provided
345 */
346 public static String findText(Class aClass, String aTextName, Locale locale, String defaultMessage, Object[] args, ValueStack valueStack) {
347 String indexedTextName = null;
348 if (aTextName == null) {
349 LOG.warn("Trying to find text with null key!");
350 aTextName = "";
351 }
352 // calculate indexedTextName (collection[*]) if applicable
353 if (aTextName.indexOf("[") != -1) {
354 int i = -1;
355
356 indexedTextName = aTextName;
357
358 while ((i = indexedTextName.indexOf("[", i + 1)) != -1) {
359 int j = indexedTextName.indexOf("]", i);
360 String a = indexedTextName.substring(0, i);
361 String b = indexedTextName.substring(j);
362 indexedTextName = a + "[*" + b;
363 }
364 }
365
366 // search up class hierarchy
367 String msg = findMessage(aClass, aTextName, indexedTextName, locale, args, null, valueStack);
368
369 if (msg != null) {
370 return msg;
371 }
372
373 if (ModelDriven.class.isAssignableFrom(aClass)) {
374 ActionContext context = ActionContext.getContext();
375 // search up model's class hierarchy
376 ActionInvocation actionInvocation = context.getActionInvocation();
377
378 // ActionInvocation may be null if we're being run from a Sitemesh filter, so we won't get model texts if this is null
379 if (actionInvocation != null) {
380 Object action = actionInvocation.getAction();
381 if (action instanceof ModelDriven) {
382 Object model = ((ModelDriven) action).getModel();
383 if (model != null) {
384 msg = findMessage(model.getClass(), aTextName, indexedTextName, locale, args, null, valueStack);
385 if (msg != null) {
386 return msg;
387 }
388 }
389 }
390 }
391 }
392
393 // nothing still? alright, search the package hierarchy now
394 for (Class clazz = aClass;
395 (clazz != null) && !clazz.equals(Object.class);
396 clazz = clazz.getSuperclass()) {
397
398 String basePackageName = clazz.getName();
399 while (basePackageName.lastIndexOf('.') != -1) {
400 basePackageName = basePackageName.substring(0, basePackageName.lastIndexOf('.'));
401 String packageName = basePackageName + ".package";
402 msg = getMessage(packageName, locale, aTextName, valueStack, args);
403
404 if (msg != null) {
405 return msg;
406 }
407
408 if (indexedTextName != null) {
409 msg = getMessage(packageName, locale, indexedTextName, valueStack, args);
410
411 if (msg != null) {
412 return msg;
413 }
414 }
415 }
416 }
417
418 // see if it's a child property
419 int idx = aTextName.indexOf(".");
420
421 if (idx != -1) {
422 String newKey = null;
423 String prop = null;
424
425 if (aTextName.startsWith(XWorkConverter.CONVERSION_ERROR_PROPERTY_PREFIX)) {
426 idx = aTextName.indexOf(".", XWorkConverter.CONVERSION_ERROR_PROPERTY_PREFIX.length());
427
428 if (idx != -1) {
429 prop = aTextName.substring(XWorkConverter.CONVERSION_ERROR_PROPERTY_PREFIX.length(), idx);
430 newKey = XWorkConverter.CONVERSION_ERROR_PROPERTY_PREFIX + aTextName.substring(idx + 1);
431 }
432 } else {
433 prop = aTextName.substring(0, idx);
434 newKey = aTextName.substring(idx + 1);
435 }
436
437 if (prop != null) {
438 Object obj = valueStack.findValue(prop);
439 try {
440 Object actionObj = ReflectionProviderFactory.getInstance().getRealTarget(prop, valueStack.getContext(), valueStack.getRoot());
441 if (actionObj != null) {
442 PropertyDescriptor propertyDescriptor = ReflectionProviderFactory.getInstance().getPropertyDescriptor(actionObj.getClass(), prop);
443
444 if (propertyDescriptor != null) {
445 Class clazz=propertyDescriptor.getPropertyType();
446
447 if (clazz != null) {
448 if (obj != null)
449 valueStack.push(obj);
450 msg = findText(clazz, newKey, locale, null, args);
451 if (obj != null)
452 valueStack.pop();
453
454 if (msg != null) {
455 return msg;
456 }
457 }
458 }
459 }
460 }
461 catch(Exception e) {
462 LOG.debug("unable to find property "+prop, e);
463 }
464 }
465 }
466
467 // get default
468 GetDefaultMessageReturnArg result = null;
469 if (indexedTextName == null) {
470 result = getDefaultMessage(aTextName, locale, valueStack, args, defaultMessage);
471 } else {
472 result = getDefaultMessage(aTextName, locale, valueStack, args, null);
473 if (result != null && result.message != null) {
474 return result.message;
475 }
476 result = getDefaultMessage(indexedTextName, locale, valueStack, args, defaultMessage);
477 }
478
479 // could we find the text, if not log a warn
480 if (unableToFindTextForKey(result)) {
481 String warn = "Unable to find text for key '" + aTextName + "' ";
482 if (indexedTextName != null) {
483 warn += " or indexed key '" + indexedTextName + "' ";
484 }
485 warn += "in class '" + aClass.getName() + "' and locale '" + locale + "'";
486 LOG.debug(warn);
487 }
488
489 return result != null ? result.message : null;
490 }
491
492 /**
493 * Determines if we found the text in the bundles.
494 *
495 * @param result the result so far
496 * @return <tt>true</tt> if we could <b>not</b> find the text, <tt>false</tt> if the text was found (=success).
497 */
498 private static boolean unableToFindTextForKey(GetDefaultMessageReturnArg result) {
499 if (result == null || result.message == null) {
500 return true;
501 }
502
503 // did we find it in the bundle, then no problem?
504 if (result.foundInBundle) {
505 return false;
506 }
507
508 // not found in bundle
509 return true;
510 }
511
512 /**
513 * Finds a localized text message for the given key, aTextName, in the specified resource bundle
514 * with aTextName as the default message.
515 * <p/>
516 * If a message is found, it will also be interpolated. Anything within <code>${...}</code>
517 * will be treated as an OGNL expression and evaluated as such.
518 *
519 * @see #findText(java.util.ResourceBundle, String, java.util.Locale, String, Object[])
520 */
521 public static String findText(ResourceBundle bundle, String aTextName, Locale locale) {
522 return findText(bundle, aTextName, locale, aTextName, new Object[0]);
523 }
524
525 /**
526 * Finds a localized text message for the given key, aTextName, in the specified resource
527 * bundle.
528 * <p/>
529 * If a message is found, it will also be interpolated. Anything within <code>${...}</code>
530 * will be treated as an OGNL expression and evaluated as such.
531 * <p/>
532 * If a message is <b>not</b> found a WARN log will be logged.
533 *
534 * @param bundle the bundle
535 * @param aTextName the key
536 * @param locale the locale
537 * @param defaultMessage the default message to use if no message was found in the bundle
538 * @param args arguments for the message formatter.
539 */
540 public static String findText(ResourceBundle bundle, String aTextName, Locale locale, String defaultMessage, Object[] args) {
541 ValueStack valueStack = ActionContext.getContext().getValueStack();
542 return findText(bundle, aTextName, locale, defaultMessage, args, valueStack);
543 }
544
545 /**
546 * Finds a localized text message for the given key, aTextName, in the specified resource
547 * bundle.
548 * <p/>
549 * If a message is found, it will also be interpolated. Anything within <code>${...}</code>
550 * will be treated as an OGNL expression and evaluated as such.
551 * <p/>
552 * If a message is <b>not</b> found a WARN log will be logged.
553 *
554 * @param bundle the bundle
555 * @param aTextName the key
556 * @param locale the locale
557 * @param defaultMessage the default message to use if no message was found in the bundle
558 * @param args arguments for the message formatter.
559 * @param valueStack the OGNL value stack.
560 */
561 public static String findText(ResourceBundle bundle, String aTextName, Locale locale, String defaultMessage, Object[] args, ValueStack valueStack) {
562 try {
563 reloadBundles();
564
565 String message = TextParseUtil.translateVariables(bundle.getString(aTextName), valueStack);
566 MessageFormat mf = buildMessageFormat(message, locale);
567
568 return mf.format(args);
569 } catch (MissingResourceException ex) {
570 // ignore
571 }
572
573 GetDefaultMessageReturnArg result = getDefaultMessage(aTextName, locale, valueStack, args, defaultMessage);
574 if (unableToFindTextForKey(result)) {
575 LOG.warn("Unable to find text for key '" + aTextName + "' in ResourceBundles for locale '" + locale + "'");
576 }
577 return result.message;
578 }
579
580 /**
581 * Gets the default message.
582 */
583 private static GetDefaultMessageReturnArg getDefaultMessage(String key, Locale locale, ValueStack valueStack, Object[] args, String defaultMessage) {
584 GetDefaultMessageReturnArg result = null;
585 boolean found = true;
586
587 if (key != null) {
588 String message = findDefaultText(key, locale);
589
590 if (message == null) {
591 message = defaultMessage;
592 found = false; // not found in bundles
593 }
594
595 // defaultMessage may be null
596 if (message != null) {
597 MessageFormat mf = buildMessageFormat(TextParseUtil.translateVariables(message, valueStack), locale);
598
599 String msg = mf.format(args);
600 result = new GetDefaultMessageReturnArg(msg, found);
601 }
602 }
603
604 return result;
605 }
606
607 /**
608 * Gets the message from the named resource bundle.
609 */
610 private static String getMessage(String bundleName, Locale locale, String key, ValueStack valueStack, Object[] args) {
611 ResourceBundle bundle = findResourceBundle(bundleName, locale);
612 if (bundle == null) {
613 return null;
614 }
615
616 reloadBundles();
617
618 try {
619 String message = TextParseUtil.translateVariables(bundle.getString(key), valueStack);
620 MessageFormat mf = buildMessageFormat(message, locale);
621 return mf.format(args);
622 } catch (MissingResourceException e) {
623 return null;
624 }
625 }
626
627 private static MessageFormat buildMessageFormat(String pattern, Locale locale) {
628 MessageFormatKey key = new MessageFormatKey(pattern, locale);
629 MessageFormat format = null;
630 synchronized(messageFormats) {
631 format = (MessageFormat) messageFormats.get(key);
632 if (format == null) {
633 format = new MessageFormat(pattern);
634 format.setLocale(locale);
635 format.applyPattern(pattern);
636 messageFormats.put(key, format);
637 }
638 }
639
640 return format;
641 }
642
643 /**
644 * Traverse up class hierarchy looking for message. Looks at class, then implemented interface,
645 * before going up hierarchy.
646 */
647 private static String findMessage(Class clazz, String key, String indexedKey, Locale locale, Object[] args, Set checked, ValueStack valueStack) {
648 if (checked == null) {
649 checked = new TreeSet();
650 } else if (checked.contains(clazz.getName())) {
651 return null;
652 }
653
654 // look in properties of this class
655 String msg = getMessage(clazz.getName(), locale, key, valueStack, args);
656
657 if (msg != null) {
658 return msg;
659 }
660
661 if (indexedKey != null) {
662 msg = getMessage(clazz.getName(), locale, indexedKey, valueStack, args);
663
664 if (msg != null) {
665 return msg;
666 }
667 }
668
669 // look in properties of implemented interfaces
670 Class[] interfaces = clazz.getInterfaces();
671
672 for (int x = 0; x < interfaces.length; x++) {
673 msg = getMessage(interfaces[x].getName(), locale, key, valueStack, args);
674
675 if (msg != null) {
676 return msg;
677 }
678
679 if (indexedKey != null) {
680 msg = getMessage(interfaces[x].getName(), locale, indexedKey, valueStack, args);
681
682 if (msg != null) {
683 return msg;
684 }
685 }
686 }
687
688 // traverse up hierarchy
689 if (clazz.isInterface()) {
690 interfaces = clazz.getInterfaces();
691
692 for (int x = 0; x < interfaces.length; x++) {
693 msg = findMessage(interfaces[x], key, indexedKey, locale, args, checked, valueStack);
694
695 if (msg != null) {
696 return msg;
697 }
698 }
699 } else {
700 if (!clazz.equals(Object.class) && !clazz.isPrimitive()) {
701 return findMessage(clazz.getSuperclass(), key, indexedKey, locale, args, checked, valueStack);
702 }
703 }
704
705 return null;
706 }
707
708 private static void reloadBundles() {
709 if (reloadBundles) {
710 try {
711 clearMap(ResourceBundle.class, null, "cacheList");
712
713 // now, for the true and utter hack, if we're running in tomcat, clear
714 // it's class loader resource cache as well.
715 clearTomcatCache();
716 }
717 catch (Exception e) {
718 LOG.error("Could not reload resource bundles", e);
719 }
720 }
721 }
722
723
724 private static void clearTomcatCache() {
725 ClassLoader loader = Thread.currentThread().getContextClassLoader();
726 // no need for compilation here.
727 Class cl = loader.getClass();
728
729 try {
730 if ("org.apache.catalina.loader.WebappClassLoader".equals(cl.getName())) {
731 clearMap(cl, loader, "resourceEntries");
732 } else {
733 if (LOG.isDebugEnabled()) {
734 LOG.debug("class loader " + cl.getName() + " is not tomcat loader.");
735 }
736 }
737 }
738 catch (Exception e) {
739 LOG.warn("couldn't clear tomcat cache", e);
740 }
741 }
742
743
744 private static void clearMap(Class cl, Object obj, String name)
745 throws NoSuchFieldException, IllegalAccessException, NoSuchMethodException,
746 InvocationTargetException {
747 Field field = cl.getDeclaredField(name);
748 field.setAccessible(true);
749
750 Object cache = field.get(obj);
751
752 synchronized (cache) {
753 Class ccl = cache.getClass();
754 Method clearMethod = ccl.getMethod("clear");
755 clearMethod.invoke(cache);
756 }
757
758 }
759
760 /**
761 * Clears all the internal lists.
762 */
763 public static void reset() {
764 clearDefaultResourceBundles();
765
766 synchronized (misses) {
767 misses.clear();
768 }
769
770 synchronized (messageFormats) {
771 messageFormats.clear();
772 }
773 }
774
775 static class MessageFormatKey {
776 String pattern;
777 Locale locale;
778
779 MessageFormatKey(String pattern, Locale locale) {
780 this.pattern = pattern;
781 this.locale = locale;
782 }
783
784 public boolean equals(Object o) {
785 if (this == o) return true;
786 if (!(o instanceof MessageFormatKey)) return false;
787
788 final MessageFormatKey messageFormatKey = (MessageFormatKey) o;
789
790 if (locale != null ? !locale.equals(messageFormatKey.locale) : messageFormatKey.locale != null)
791 return false;
792 if (pattern != null ? !pattern.equals(messageFormatKey.pattern) : messageFormatKey.pattern != null)
793 return false;
794
795 return true;
796 }
797
798 public int hashCode() {
799 int result;
800 result = (pattern != null ? pattern.hashCode() : 0);
801 result = 29 * result + (locale != null ? locale.hashCode() : 0);
802 return result;
803 }
804 }
805
806 static class GetDefaultMessageReturnArg {
807 String message;
808 boolean foundInBundle;
809
810 public GetDefaultMessageReturnArg(String message, boolean foundInBundle) {
811 this.message = message;
812 this.foundInBundle = foundInBundle;
813 }
814 }
815 }