1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.jetspeed.i18n;
18
19 import java.io.Serializable;
20 import java.lang.reflect.Field;
21 import java.lang.reflect.Modifier;
22 import java.text.MessageFormat;
23 import java.util.HashMap;
24 import java.util.Locale;
25 import java.util.ResourceBundle;
26
27 import org.apache.jetspeed.exception.JetspeedException;
28 import org.apache.jetspeed.security.SecurityException;
29
30 /***
31 * KeyedMessage provides an automatically derived i18n message key based on its static instance definition and can be
32 * used as comparable constant too.
33 * <h3>Purpose</h3>
34 * <p>
35 * With a KeyedMessage a named constant message (format) can be statically defined which automatically translate
36 * themselves for a specific locale using an automatically derived ResourceBundle or even a specified one.
37 * </p>
38 * <h3>Key derivation</h3>
39 * <p>
40 * Because KeyedMessages are created with a default message (format), even if no ResourceBundle or its key is defined or
41 * can't be found, message translation is still possible.
42 * </p>
43 * <p>
44 * A KeyedMessage automatically derives the ResourceBundle lookup key from its (statically defined) instance field name
45 * using the following format: <br/><br/><code>
46 * <containingClass.name>.<staticInstanceField.name>
47 * </code>
48 * <br/>
49 * </p>
50 * <p>
51 * The containingClass is derived at construction time by analyzing the StackTraceElements of a thrown exception. This
52 * <em><b>requires</b></em> the instance to be defined as a public static field!
53 * </p>
54 * <p>
55 * At first access, the key is resolved by inspecting the derived containingClass for the <em>declared</em> field
56 * defining this instance.
57 * </p>
58 * <p>
59 * If the KeyedMessage instance <em><b>wasn't</b></em> defined as public static field, the key can't be resolved and
60 * message translation using a ResourceBundle won't be possible. Translation using the default message will still work
61 * though. Furthermore, this instance can't be used as comparable named constant as the {@link #equals(Object)}method
62 * will always return false in this case.
63 * </p>
64 * <h3>Default ResourceBundle name derivation</h3>
65 * <p>
66 * When the key of a KeyedMessage is resolved, the default ResourceBundle name for message translation is retrieved from
67 * the defined public static String field named {@link #KEYED_MESSAGE_BUNDLE_FIELD_NAME "KEYED_MESSAGE_BUNDLE"}defined
68 * in its containingClass or one of its superClasses or interfaces.
69 * </p>
70 * <p>
71 * If this field cannot be found, the fully qualified name of the containingClass is used.
72 * </p>
73 * <p>
74 * ResourceBundle names are cached in a Map for each containingClass and only derived for the first KeyedMessage defined
75 * in a containingClass.
76 * </p>
77 * <p>
78 * <em>Again: only <b>resolved</b> instances can use a ResourceBundle for message translation.</em>
79 * </p>
80 * <h3>Default Locale lookup</h3>
81 * <p>
82 * When a message is translated without a specified Locale, {@link CurrentLocale#get()}is used to determine the default
83 * Locale for the current Thread.
84 * </p>
85 * <p>
86 * In Jetspeed, the <code>LocalizationValve</code> initializes the {@link CurrentLocale} on each request.
87 * KeyedMessages accessed within the context of an Jetspeed request therefore will always be translated using the
88 * current user Locale with the {@link #getMessage()}or {@link #toString()}methods.
89 * </p>
90 * <h3>Default ResourceBundle lookup</h3>
91 * <p>
92 * If a message translation is done using the default ResourceBundle name the ResourceBundle is retrieved using the
93 * ClassLoader of the containingClass. This means the bundle(s) must be provided in the same context as from where the
94 * containingClass is loaded. Usually (and preferably), this will be from the shared classpath of the webserver.
95 * </p>
96 * <h3>MessageFormat parameters</h3>
97 * <p>
98 * MessageFormat patterns can also be used for a KeyedMessage.<br/>
99 * With the {@link #create(Object[])}method a specialized copy of a KeyedMessage instance can be created containing the
100 * arguments to be used during message translation.
101 * </p>
102 * <p>
103 * This new copy remains {@link equals(Object)}to its source and can still be used for named constant comparison.
104 * </p>
105 * <p>
106 * For simplified usage, three {@link #create(Object)},{@link #create(Object, Object)}and
107 * {@link #create(Object, Object, Object)}methods are provided which delegate to {@link #create(Object[])}with their
108 * argument(s) transformed into an Object array.
109 * </p>
110 * <h3>Extending KeyedMessage</h3>
111 * <p>
112 * An statically defined KeyedMessage can be used as a "simple" named constant. <br/>If additional metadata is required
113 * like some kind of status, level or type indication, the KeyedMessage class can easily be extended by providing a
114 * specialized version of the {@link #create(KeyedMessage, Object[])}copy factory.
115 * </p>
116 * <h3>Usage</h3>
117 * <p>
118 * KeyedMessage has been used to replace the hardcoded {@link SecurityException} String constants. <br/>The
119 * ResourceBundle name used is defined by {@link JetspeedException#KEYED_MESSAGE_BUNDLE} which is the superClass of
120 * {@link SecurityException}.<br/>
121 * <p>
122 * <em>For a different ResourceBundle to be used for SecurityException messages a KEYED_MESSAGE_BUNDLE field can be defined
123 * in {@link SecurityException} too, overriding the one in {@link JetspeedException}.</em>
124 * </p>
125 * <p>
126 * Example:
127 * </p>
128 * <pre>
129 * public class JetspeedException extends Exception {
130 * public static final String KEYED_MESSAGE_BUNDLE = "org.apache.jetspeed.exception.JetspeedExceptionMessages";
131 * ...
132 *
133 * public String getMessage() {
134 * if ( keyedMessage != null ) {
135 * return keyedMessage.getMessage(); // translated using current Locale and default ResourceBundle
136 * }
137 * return super.getMessage();
138 * }
139 * }
140 *
141 * public class SecurityException extends JetspeedException {
142 * public static final KeyedMessage USER_DOES_NOT_EXIST = new KeyedMessage("The user {0} does not exist.");
143 * ...
144 * }
145 *
146 * // resource file: org.apache.jetspeed.exception.JetspeedExceptionMessages_nl.properties
147 * org.apache.jetspeed.security.SecurityException.USER_DOES_NOT_EXIST = De gebruiker {0} bestaat niet.
148 * ...
149 *
150 * public class UserManagerImpl implements UserManager {
151 * public User getUser(String username) throws SecurityException {
152 * ...
153 * if (null == userPrincipal) {
154 * throw new SecurityException(SecurityException.USER_DOES_NOT_EXIST.create(username));
155 * }
156 * ...
157 * }
158 * ...
159 * }
160 *
161 * // example get User
162 * try {
163 * User user = userManager.getUser(userName);
164 * } catch (SecurityException sex) {
165 * if ( SecurityException.USER_DOES_NOT_EXISTS.equals(sex.getKeyedMessage()) {
166 * // handle USER_DOES_NOT_EXISTS error
167 * }
168 * }
169 * </pre>
170 *
171 * @author <a href="mailto:ate@douma.nu">Ate Douma</a>
172 * @version $Id: KeyedMessage.java 516448 2007-03-09 16:25:47Z ate $
173 */
174 public class KeyedMessage implements Serializable
175 {
176 /***
177 * Static String Field name searched for in the class defining a KeyedMessage containing the default resource bundle
178 * to use for translation. <br/><em>Note: this Field is looked up using definingClass.getField thus it may also be
179 * defined in a superclass or interface of the definingClass.</em>
180 */
181 public static final String KEYED_MESSAGE_BUNDLE_FIELD_NAME = "KEYED_MESSAGE_BUNDLE";
182
183 /***
184 * Key value for an unresolved KeyMessage.
185 */
186 private static final String UNRESOLVED_KEY = KeyedMessage.class.getName() + ".<unresolved>";
187
188 /***
189 * Map caching default resource bundle names keyed on containingClass
190 */
191 private static final HashMap resourceNameMap = new HashMap();
192
193 /***
194 * Default message used when key couldn't be looked up in the default or a specified resource bundle
195 */
196 private String message;
197
198 /***
199 * Dynamically derived key based on the definingClass name, postfixed with the static field name of this instance
200 * </br>
201 *
202 * @see #getKey()
203 */
204 private String key;
205
206 /***
207 * Optional message format arguments which can only be set using a derived KeyedMessage using the
208 * {@link #create(Object[])}method(s).
209 */
210 private Object[] arguments;
211
212 /***
213 * The class in which this instance is defined as a static Field.
214 */
215 private Class containingClass;
216
217 /***
218 * Indicates if this instance could be {@link #resolve() resolved}.
219 */
220 private boolean resolved;
221
222 /***
223 * Constructs a derived KeyedMessage from another KeyedMessage to provide additional message format arguments.
224 *
225 * @see #create(Object[])
226 * @param source the KeyedMessage to derive this instance from
227 * @param arguments this instance specific message format arguments
228 */
229 protected KeyedMessage(KeyedMessage source, Object[] arguments)
230 {
231 this.key = source.getKey();
232 this.message = source.message;
233 this.resolved = source.resolved;
234 this.containingClass = source.containingClass;
235 this.arguments = arguments;
236 }
237
238 /***
239 * Constructs a new KeyedMessage which will dynamically derive its own {@link #getKey()}.
240 *
241 * @param message the default message used when the {@link #getKey()}could not be found in the default or a
242 * specified resource bundle.
243 */
244 public KeyedMessage(String message)
245 {
246 try
247 {
248 throw new Exception();
249 }
250 catch (Exception e)
251 {
252 StackTraceElement[] elements = e.getStackTrace();
253 if (elements.length >= 2)
254 {
255 String containingClassName = elements[1].getClassName();
256 try
257 {
258 containingClass = Thread.currentThread().getContextClassLoader().loadClass(containingClassName);
259 }
260 catch (ClassNotFoundException e1)
261 {
262 key = UNRESOLVED_KEY;
263 }
264 }
265 }
266 this.message = message;
267 }
268
269 private String getResourceName()
270 {
271 synchronized (resourceNameMap)
272 {
273 return (String) resourceNameMap.get(containingClass);
274 }
275 }
276
277 /***
278 * @see KeyedMessage
279 */
280 private void resolve()
281 {
282 if (key == null)
283 {
284
285
286 Field[] fields = containingClass.getDeclaredFields();
287 for (int i = 0; i < fields.length; i++)
288 {
289 try
290 {
291 if (fields[i].getType() == this.getClass() && Modifier.isStatic(fields[i].getModifiers())
292 && fields[i].get(null) == this)
293 {
294
295 key = containingClass.getName() + "." + fields[i].getName();
296 resolved = true;
297
298
299 synchronized (resourceNameMap)
300 {
301 if (getResourceName() == null)
302 {
303
304
305 String resourceName = null;
306 try
307 {
308 Field field = containingClass.getField(KEYED_MESSAGE_BUNDLE_FIELD_NAME);
309 if (field != null && field.getType() == String.class
310 && Modifier.isStatic(field.getModifiers()))
311 {
312 resourceName = (String) field.get(null);
313 }
314 }
315 catch (Exception e)
316 {
317 }
318 if (resourceName == null)
319 {
320
321 resourceName = containingClass.getName();
322 }
323 resourceNameMap.put(containingClass, resourceName);
324 }
325 }
326
327 break;
328 }
329 }
330 catch (Exception e)
331 {
332 }
333 }
334 if (key == null)
335 {
336 key = UNRESOLVED_KEY;
337 }
338 }
339 }
340
341 /***
342 * Formats a message using MessageFormat if arguments are defined, otherwise simply returns the argument.
343 *
344 * @param message the message format
345 * @return formatted message
346 */
347 private String format(String message)
348 {
349 if (arguments != null && arguments.length > 0)
350 {
351 return new MessageFormat(message).format(arguments);
352 }
353 else
354 {
355 return message;
356 }
357 }
358
359 /***
360 * Extendable KeyedMessage factory
361 *
362 * @param source the source to copy from
363 * @param arguments the optional message format arguments
364 * @return copied instance with new arguments set
365 */
366 protected KeyedMessage create(KeyedMessage source, Object[] arguments)
367 {
368 return new KeyedMessage(this, arguments);
369 }
370
371 /***
372 * Creates a derived KeyedMessage from this instance to provide additional message format arguments. <br/>The new
373 * instance will be {@link #equals(Object)}to this instance with only different arguments. <br/><br/>Note: the
374 * argument objects should be lightweight types and preferably Serializable instances
375 *
376 * @param arguments The derived instance specific message format arguments
377 * @return derived KeyedMessage {@link #equals(Object) equal}to this with its own message format arguments
378 */
379 public KeyedMessage create(Object[] arguments)
380 {
381 return new KeyedMessage(this, arguments);
382 }
383
384 /***
385 * Simplied version of {@link #create(Object[])}with only one argument
386 *
387 * @param single message format argument
388 * @see #create(Object[])
389 * @return derived KeyedMessage {@link #equals(Object) equal}to this with its own message format argument
390 */
391 public KeyedMessage create(Object o)
392 {
393 return create(new Object[] { o });
394 }
395
396 /***
397 * Simplied version of {@link #create(Object[])}with only two arguments
398 *
399 * @param single message format argument
400 * @see #create(Object[])
401 * @return derived KeyedMessage {@link #equals(Object) equal}to this with its own message format arguments
402 */
403 public KeyedMessage create(Object o1, Object o2)
404 {
405 return create(new Object[] { o1, o2 });
406 }
407
408 /***
409 * Simplied version of {@link #create(Object[])}with only three arguments
410 *
411 * @param single message format argument
412 * @see #create(Object[])
413 * @return derived KeyedMessage {@link #equals(Object) equal}to this with its own message format arguments
414 */
415 public KeyedMessage create(Object o1, Object o2, Object o3)
416 {
417 return create(new Object[] { o1, o2, o3 });
418 }
419
420 /***
421 * Dynamically derived key based on the definingClass name, postfixed with the static field name of this instance.
422 * <br/><br/>Format: <br/><code>
423 * <containingClass.name>.<staticInstanceField.name>
424 * </code>
425 * <br/><br/>If this instance couldn't be resolved, generic value UNRESOLVED_KEY will have been set.
426 *
427 * @return derived key
428 */
429 public final String getKey()
430 {
431 resolve();
432 return key;
433 }
434
435 /***
436 * Loads and returns a Locale specific default ResourceBundle for this instance. <br/>If this instance couldn't be
437 * {@link #resolve() resolved}or the bundle couldn't be loadednull will be returned. <br/>The ResourceBundle will
438 * be loaded using the {@link #containingClass}its ClassLoader.
439 *
440 * @param locale the Locale to lookup the locale specific default ResourceBundle
441 * @return a Locale specific default ResourceBundle
442 */
443 public ResourceBundle getBundle(Locale locale)
444 {
445 resolve();
446 if (resolved)
447 {
448 try
449 {
450 return ResourceBundle.getBundle(getResourceName(), locale, containingClass.getClassLoader());
451 }
452 catch (RuntimeException e)
453 {
454 }
455
456 }
457 return null;
458 }
459
460 /***
461 * Loads and returns the default ResourceBundle for this instance using the
462 * {@link CurrentLocale#get() current Locale}.
463 *
464 * @see #getBundle(Locale)
465 * @see CurrentLocale
466 * @return the default ResourceBundle for the current Locale
467 */
468 public ResourceBundle getBundle()
469 {
470 return getBundle(CurrentLocale.get());
471 }
472
473 /***
474 * @return formatted message using the default ResourceBundle using the {@link CurrentLocale current Locale}.
475 * @see #getBundle()
476 */
477 public String getMessage()
478 {
479 return getMessage(getBundle());
480 }
481
482 /***
483 * @param bundle a specific ResourceBundle defining this instance {@link #getKey() key}
484 * @return formatted message using a specific ResourceBundle.
485 */
486 public String getMessage(ResourceBundle bundle)
487 {
488 resolve();
489 String message = this.message;
490 if (resolved && bundle != null)
491 {
492 try
493 {
494 message = bundle.getString(key);
495 }
496 catch (RuntimeException e)
497 {
498
499 }
500 }
501 return format(message);
502 }
503
504 /***
505 * @param locale a specific Locale
506 * @return formatted message using the default ResourceBundle using a specific Locale.
507 */
508 public String getMessage(Locale locale)
509 {
510 return getMessage(getBundle(locale));
511 }
512
513 /***
514 * @return the arguments defined for this {@link #create(Object[]) derived}instance
515 * @see #create(Object[])
516 */
517 public Object[] getArguments()
518 {
519 return arguments;
520 }
521
522 /***
523 * @param index argument number
524 * @return an argument defined for this {@link #create(Object[]) derived}instance
525 */
526 public Object getArgument(int index)
527 {
528 return arguments[index];
529 }
530
531 /***
532 * @return formatted message using the default ResourceBundle using the {@link CurrentLocale current Locale}.
533 * @see #getMessage()
534 */
535 public String toString()
536 {
537 return getMessage();
538 }
539
540 /***
541 * @param otherObject KeyedMessage instance to compare with
542 * @return true only if otherObject is a KeyedMessage {@link create(Object[]) derived}from this instance (or visa
543 * versa) and (thus both are) {@link #resolve() resolved}.
544 * @see #create(Object[])
545 * @see #resolve()
546 */
547 public boolean equals(Object otherObject)
548 {
549 if (otherObject != null && otherObject instanceof KeyedMessage)
550 {
551 resolve();
552 return (resolved && key.equals(((KeyedMessage) otherObject).getKey()));
553 }
554 return false;
555 }
556 }