View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    * 
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   * 
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
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; // for javadoc ref
28  import org.apache.jetspeed.security.SecurityException; // for javadoc ref
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   *     &nbsp;&nbsp;&lt;containingClass.name&gt;.&lt;staticInstanceField.name&gt;
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 = &quot;org.apache.jetspeed.exception.JetspeedExceptionMessages&quot;;
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(&quot;The user {0} does not exist.&quot;);
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             // search for this instance as a statically declared field in the containingClass to find out the name
285             // to use.
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                         // resolved: save the key
295                         key = containingClass.getName() + "." + fields[i].getName();
296                         resolved = true;
297 
298                         // Now derive the default resource bundle if not already done before
299                         synchronized (resourceNameMap)
300                         {
301                             if (getResourceName() == null)
302                             {
303                                 // Find resource bundle name by looking up the statically defined
304                                 // KEYED_MESSAGE_BUNDLE_FIELD_NAME String field in the containingClass.
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                                     // fallback to containingClass name as resource bundle name
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      *     &nbsp;&nbsp;&lt;containingClass.name&gt;.&lt;staticInstanceField.name&gt;
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                 // ignore: fallback to default message
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 }