001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.jetspeed.i18n;
018:
019: import java.io.Serializable;
020: import java.lang.reflect.Field;
021: import java.lang.reflect.Modifier;
022: import java.text.MessageFormat;
023: import java.util.HashMap;
024: import java.util.Locale;
025: import java.util.ResourceBundle;
026:
027: import org.apache.jetspeed.exception.JetspeedException; // for javadoc ref
028: import org.apache.jetspeed.security.SecurityException; // for javadoc ref
029:
030: /**
031: * KeyedMessage provides an automatically derived i18n message key based on its static instance definition and can be
032: * used as comparable constant too.
033: * <h3>Purpose</h3>
034: * <p>
035: * With a KeyedMessage a named constant message (format) can be statically defined which automatically translate
036: * themselves for a specific locale using an automatically derived ResourceBundle or even a specified one.
037: * </p>
038: * <h3>Key derivation</h3>
039: * <p>
040: * Because KeyedMessages are created with a default message (format), even if no ResourceBundle or its key is defined or
041: * can't be found, message translation is still possible.
042: * </p>
043: * <p>
044: * A KeyedMessage automatically derives the ResourceBundle lookup key from its (statically defined) instance field name
045: * using the following format: <br/><br/><code>
046: * <containingClass.name>.<staticInstanceField.name>
047: * </code>
048: * <br/>
049: * </p>
050: * <p>
051: * The containingClass is derived at construction time by analyzing the StackTraceElements of a thrown exception. This
052: * <em><b>requires</b></em> the instance to be defined as a public static field!
053: * </p>
054: * <p>
055: * At first access, the key is resolved by inspecting the derived containingClass for the <em>declared</em> field
056: * defining this instance.
057: * </p>
058: * <p>
059: * If the KeyedMessage instance <em><b>wasn't</b></em> defined as public static field, the key can't be resolved and
060: * message translation using a ResourceBundle won't be possible. Translation using the default message will still work
061: * though. Furthermore, this instance can't be used as comparable named constant as the {@link #equals(Object)}method
062: * will always return false in this case.
063: * </p>
064: * <h3>Default ResourceBundle name derivation</h3>
065: * <p>
066: * When the key of a KeyedMessage is resolved, the default ResourceBundle name for message translation is retrieved from
067: * the defined public static String field named {@link #KEYED_MESSAGE_BUNDLE_FIELD_NAME "KEYED_MESSAGE_BUNDLE"}defined
068: * in its containingClass or one of its superClasses or interfaces.
069: * </p>
070: * <p>
071: * If this field cannot be found, the fully qualified name of the containingClass is used.
072: * </p>
073: * <p>
074: * ResourceBundle names are cached in a Map for each containingClass and only derived for the first KeyedMessage defined
075: * in a containingClass.
076: * </p>
077: * <p>
078: * <em>Again: only <b>resolved</b> instances can use a ResourceBundle for message translation.</em>
079: * </p>
080: * <h3>Default Locale lookup</h3>
081: * <p>
082: * When a message is translated without a specified Locale, {@link CurrentLocale#get()}is used to determine the default
083: * Locale for the current Thread.
084: * </p>
085: * <p>
086: * In Jetspeed, the <code>LocalizationValve</code> initializes the {@link CurrentLocale} on each request.
087: * KeyedMessages accessed within the context of an Jetspeed request therefore will always be translated using the
088: * current user Locale with the {@link #getMessage()}or {@link #toString()}methods.
089: * </p>
090: * <h3>Default ResourceBundle lookup</h3>
091: * <p>
092: * If a message translation is done using the default ResourceBundle name the ResourceBundle is retrieved using the
093: * ClassLoader of the containingClass. This means the bundle(s) must be provided in the same context as from where the
094: * containingClass is loaded. Usually (and preferably), this will be from the shared classpath of the webserver.
095: * </p>
096: * <h3>MessageFormat parameters</h3>
097: * <p>
098: * MessageFormat patterns can also be used for a KeyedMessage.<br/>
099: * 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: * Static String Field name searched for in the class defining a KeyedMessage containing the default resource bundle
177: * to use for translation. <br/><em>Note: this Field is looked up using definingClass.getField thus it may also be
178: * defined in a superclass or interface of the definingClass.</em>
179: */
180: public static final String KEYED_MESSAGE_BUNDLE_FIELD_NAME = "KEYED_MESSAGE_BUNDLE";
181:
182: /**
183: * Key value for an unresolved KeyMessage.
184: */
185: private static final String UNRESOLVED_KEY = KeyedMessage.class
186: .getName()
187: + ".<unresolved>";
188:
189: /**
190: * Map caching default resource bundle names keyed on containingClass
191: */
192: private static final HashMap resourceNameMap = new HashMap();
193:
194: /**
195: * Default message used when key couldn't be looked up in the default or a specified resource bundle
196: */
197: private String message;
198:
199: /**
200: * Dynamically derived key based on the definingClass name, postfixed with the static field name of this instance
201: * </br>
202: *
203: * @see #getKey()
204: */
205: private String key;
206:
207: /**
208: * Optional message format arguments which can only be set using a derived KeyedMessage using the
209: * {@link #create(Object[])}method(s).
210: */
211: private Object[] arguments;
212:
213: /**
214: * The class in which this instance is defined as a static Field.
215: */
216: private Class containingClass;
217:
218: /**
219: * Indicates if this instance could be {@link #resolve() resolved}.
220: */
221: private boolean resolved;
222:
223: /**
224: * Constructs a derived KeyedMessage from another KeyedMessage to provide additional message format arguments.
225: *
226: * @see #create(Object[])
227: * @param source the KeyedMessage to derive this instance from
228: * @param arguments this instance specific message format arguments
229: */
230: protected KeyedMessage(KeyedMessage source, Object[] arguments) {
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: try {
246: throw new Exception();
247: } catch (Exception e) {
248: StackTraceElement[] elements = e.getStackTrace();
249: if (elements.length >= 2) {
250: String containingClassName = elements[1].getClassName();
251: try {
252: containingClass = Thread.currentThread()
253: .getContextClassLoader().loadClass(
254: containingClassName);
255: } catch (ClassNotFoundException e1) {
256: key = UNRESOLVED_KEY;
257: }
258: }
259: }
260: this .message = message;
261: }
262:
263: private String getResourceName() {
264: synchronized (resourceNameMap) {
265: return (String) resourceNameMap.get(containingClass);
266: }
267: }
268:
269: /**
270: * @see KeyedMessage
271: */
272: private void resolve() {
273: if (key == null) {
274: // search for this instance as a statically declared field in the containingClass to find out the name
275: // to use.
276: Field[] fields = containingClass.getDeclaredFields();
277: for (int i = 0; i < fields.length; i++) {
278: try {
279: if (fields[i].getType() == this .getClass()
280: && Modifier.isStatic(fields[i]
281: .getModifiers())
282: && fields[i].get(null) == this ) {
283: // resolved: save the key
284: key = containingClass.getName() + "."
285: + fields[i].getName();
286: resolved = true;
287:
288: // Now derive the default resource bundle if not already done before
289: synchronized (resourceNameMap) {
290: if (getResourceName() == null) {
291: // Find resource bundle name by looking up the statically defined
292: // KEYED_MESSAGE_BUNDLE_FIELD_NAME String field in the containingClass.
293: String resourceName = null;
294: try {
295: Field field = containingClass
296: .getField(KEYED_MESSAGE_BUNDLE_FIELD_NAME);
297: if (field != null
298: && field.getType() == String.class
299: && Modifier.isStatic(field
300: .getModifiers())) {
301: resourceName = (String) field
302: .get(null);
303: }
304: } catch (Exception e) {
305: }
306: if (resourceName == null) {
307: // fallback to containingClass name as resource bundle name
308: resourceName = containingClass
309: .getName();
310: }
311: resourceNameMap.put(containingClass,
312: resourceName);
313: }
314: }
315:
316: break;
317: }
318: } catch (Exception e) {
319: }
320: }
321: if (key == null) {
322: key = UNRESOLVED_KEY;
323: }
324: }
325: }
326:
327: /**
328: * Formats a message using MessageFormat if arguments are defined, otherwise simply returns the argument.
329: *
330: * @param message the message format
331: * @return formatted message
332: */
333: private String format(String message) {
334: if (arguments != null && arguments.length > 0) {
335: return new MessageFormat(message).format(arguments);
336: } else {
337: return message;
338: }
339: }
340:
341: /**
342: * Extendable KeyedMessage factory
343: *
344: * @param source the source to copy from
345: * @param arguments the optional message format arguments
346: * @return copied instance with new arguments set
347: */
348: protected KeyedMessage create(KeyedMessage source,
349: Object[] arguments) {
350: return new KeyedMessage(this , arguments);
351: }
352:
353: /**
354: * Creates a derived KeyedMessage from this instance to provide additional message format arguments. <br/>The new
355: * instance will be {@link #equals(Object)}to this instance with only different arguments. <br/><br/>Note: the
356: * argument objects should be lightweight types and preferably Serializable instances
357: *
358: * @param arguments The derived instance specific message format arguments
359: * @return derived KeyedMessage {@link #equals(Object) equal}to this with its own message format arguments
360: */
361: public KeyedMessage create(Object[] arguments) {
362: return new KeyedMessage(this , arguments);
363: }
364:
365: /**
366: * Simplied version of {@link #create(Object[])}with only one argument
367: *
368: * @param single message format argument
369: * @see #create(Object[])
370: * @return derived KeyedMessage {@link #equals(Object) equal}to this with its own message format argument
371: */
372: public KeyedMessage create(Object o) {
373: return create(new Object[] { o });
374: }
375:
376: /**
377: * Simplied version of {@link #create(Object[])}with only two arguments
378: *
379: * @param single message format argument
380: * @see #create(Object[])
381: * @return derived KeyedMessage {@link #equals(Object) equal}to this with its own message format arguments
382: */
383: public KeyedMessage create(Object o1, Object o2) {
384: return create(new Object[] { o1, o2 });
385: }
386:
387: /**
388: * Simplied version of {@link #create(Object[])}with only three arguments
389: *
390: * @param single message format argument
391: * @see #create(Object[])
392: * @return derived KeyedMessage {@link #equals(Object) equal}to this with its own message format arguments
393: */
394: public KeyedMessage create(Object o1, Object o2, Object o3) {
395: return create(new Object[] { o1, o2, o3 });
396: }
397:
398: /**
399: * Dynamically derived key based on the definingClass name, postfixed with the static field name of this instance.
400: * <br/><br/>Format: <br/><code>
401: * <containingClass.name>.<staticInstanceField.name>
402: * </code>
403: * <br/><br/>If this instance couldn't be resolved, generic value UNRESOLVED_KEY will have been set.
404: *
405: * @return derived key
406: */
407: public final String getKey() {
408: resolve();
409: return key;
410: }
411:
412: /**
413: * Loads and returns a Locale specific default ResourceBundle for this instance. <br/>If this instance couldn't be
414: * {@link #resolve() resolved}or the bundle couldn't be loadednull will be returned. <br/>The ResourceBundle will
415: * be loaded using the {@link #containingClass}its ClassLoader.
416: *
417: * @param locale the Locale to lookup the locale specific default ResourceBundle
418: * @return a Locale specific default ResourceBundle
419: */
420: public ResourceBundle getBundle(Locale locale) {
421: resolve();
422: if (resolved) {
423: try {
424: return ResourceBundle.getBundle(getResourceName(),
425: locale, containingClass.getClassLoader());
426: } catch (RuntimeException e) {
427: }
428:
429: }
430: return null;
431: }
432:
433: /**
434: * Loads and returns the default ResourceBundle for this instance using the
435: * {@link CurrentLocale#get() current Locale}.
436: *
437: * @see #getBundle(Locale)
438: * @see CurrentLocale
439: * @return the default ResourceBundle for the current Locale
440: */
441: public ResourceBundle getBundle() {
442: return getBundle(CurrentLocale.get());
443: }
444:
445: /**
446: * @return formatted message using the default ResourceBundle using the {@link CurrentLocale current Locale}.
447: * @see #getBundle()
448: */
449: public String getMessage() {
450: return getMessage(getBundle());
451: }
452:
453: /**
454: * @param bundle a specific ResourceBundle defining this instance {@link #getKey() key}
455: * @return formatted message using a specific ResourceBundle.
456: */
457: public String getMessage(ResourceBundle bundle) {
458: resolve();
459: String message = this .message;
460: if (resolved && bundle != null) {
461: try {
462: message = bundle.getString(key);
463: } catch (RuntimeException e) {
464: // ignore: fallback to default message
465: }
466: }
467: return format(message);
468: }
469:
470: /**
471: * @param locale a specific Locale
472: * @return formatted message using the default ResourceBundle using a specific Locale.
473: */
474: public String getMessage(Locale locale) {
475: return getMessage(getBundle(locale));
476: }
477:
478: /**
479: * @return the arguments defined for this {@link #create(Object[]) derived}instance
480: * @see #create(Object[])
481: */
482: public Object[] getArguments() {
483: return arguments;
484: }
485:
486: /**
487: * @param index argument number
488: * @return an argument defined for this {@link #create(Object[]) derived}instance
489: */
490: public Object getArgument(int index) {
491: return arguments[index];
492: }
493:
494: /**
495: * @return formatted message using the default ResourceBundle using the {@link CurrentLocale current Locale}.
496: * @see #getMessage()
497: */
498: public String toString() {
499: return getMessage();
500: }
501:
502: /**
503: * @param otherObject KeyedMessage instance to compare with
504: * @return true only if otherObject is a KeyedMessage {@link create(Object[]) derived}from this instance (or visa
505: * versa) and (thus both are) {@link #resolve() resolved}.
506: * @see #create(Object[])
507: * @see #resolve()
508: */
509: public boolean equals(Object otherObject) {
510: if (otherObject != null && otherObject instanceof KeyedMessage) {
511: resolve();
512: return (resolved && key.equals(((KeyedMessage) otherObject)
513: .getKey()));
514: }
515: return false;
516: }
517: }
|