001: /*
002: * Copyright 2002-2007 the original author or authors.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016:
017: package org.springframework.context.support;
018:
019: import java.text.MessageFormat;
020: import java.util.ArrayList;
021: import java.util.HashMap;
022: import java.util.List;
023: import java.util.Locale;
024: import java.util.Map;
025:
026: import org.apache.commons.logging.Log;
027: import org.apache.commons.logging.LogFactory;
028:
029: import org.springframework.context.HierarchicalMessageSource;
030: import org.springframework.context.MessageSource;
031: import org.springframework.context.MessageSourceResolvable;
032: import org.springframework.context.NoSuchMessageException;
033: import org.springframework.util.ObjectUtils;
034:
035: /**
036: * Abstract implementation of the {@link HierarchicalMessageSource} interface,
037: * implementing common handling of message variants, making it easy
038: * to implement a specific strategy for a concrete MessageSource.
039: *
040: * <p>Subclasses must implement the abstract {@link #resolveCode}
041: * method. For efficient resolution of messages without arguments, the
042: * {@link #resolveCodeWithoutArguments} method should be overridden
043: * as well, resolving messages without a MessageFormat being involved.
044: *
045: * <p><b>Note:</b> By default, message texts are only parsed through
046: * MessageFormat if arguments have been passed in for the message. In case
047: * of no arguments, message texts will be returned as-is. As a consequence,
048: * you should only use MessageFormat escaping for messages with actual
049: * arguments, and keep all other messages unescaped. If you prefer to
050: * escape all messages, set the "alwaysUseMessageFormat" flag to "true".
051: *
052: * <p>Supports not only MessageSourceResolvables as primary messages
053: * but also resolution of message arguments that are in turn
054: * MessageSourceResolvables themselves.
055: *
056: * <p>This class does not implement caching of messages per code, thus
057: * subclasses can dynamically change messages over time. Subclasses are
058: * encouraged to cache their messages in a modification-aware fashion,
059: * allowing for hot deployment of updated messages.
060: *
061: * @author Juergen Hoeller
062: * @author Rod Johnson
063: * @see #resolveCode(String, java.util.Locale)
064: * @see #resolveCodeWithoutArguments(String, java.util.Locale)
065: * @see #setAlwaysUseMessageFormat
066: * @see java.text.MessageFormat
067: */
068: public abstract class AbstractMessageSource implements
069: HierarchicalMessageSource {
070:
071: /** Logger available to subclasses */
072: protected final Log logger = LogFactory.getLog(getClass());
073:
074: private MessageSource parentMessageSource;
075:
076: private boolean useCodeAsDefaultMessage = false;
077:
078: private boolean alwaysUseMessageFormat = false;
079:
080: /**
081: * Cache to hold already generated MessageFormats per message.
082: * Used for passed-in default messages. MessageFormats for resolved
083: * codes are cached on a specific basis in subclasses.
084: */
085: private final Map cachedMessageFormats = new HashMap();
086:
087: public void setParentMessageSource(MessageSource parent) {
088: this .parentMessageSource = parent;
089: }
090:
091: public MessageSource getParentMessageSource() {
092: return this .parentMessageSource;
093: }
094:
095: /**
096: * Set whether to use the message code as default message instead of
097: * throwing a NoSuchMessageException. Useful for development and debugging.
098: * Default is "false".
099: * <p>Note: In case of a MessageSourceResolvable with multiple codes
100: * (like a FieldError) and a MessageSource that has a parent MessageSource,
101: * do <i>not</i> activate "useCodeAsDefaultMessage" in the <i>parent</i>:
102: * Else, you'll get the first code returned as message by the parent,
103: * without attempts to check further codes.
104: * <p>To be able to work with "useCodeAsDefaultMessage" turned on in the parent,
105: * AbstractMessageSource and AbstractApplicationContext contain special checks
106: * to delegate to the internal <code>getMessageInternal</code> method if available.
107: * In general, it is recommended to just use "useCodeAsDefaultMessage" during
108: * development and not rely on it in production in the first place, though.
109: * @see #getMessage(String, Object[], Locale)
110: * @see #getMessageInternal
111: * @see org.springframework.validation.FieldError
112: */
113: public void setUseCodeAsDefaultMessage(
114: boolean useCodeAsDefaultMessage) {
115: this .useCodeAsDefaultMessage = useCodeAsDefaultMessage;
116: }
117:
118: /**
119: * Return whether to use the message code as default message instead of
120: * throwing a NoSuchMessageException. Useful for development and debugging.
121: * Default is "false".
122: * <p>Alternatively, consider overriding the <code>getDefaultMessage</code>
123: * method to return a custom fallback message for an unresolvable code.
124: * @see #getDefaultMessage(String)
125: */
126: protected boolean isUseCodeAsDefaultMessage() {
127: return this .useCodeAsDefaultMessage;
128: }
129:
130: /**
131: * Set whether to always apply the MessageFormat rules, parsing even
132: * messages without arguments.
133: * <p>Default is "false": Messages without arguments are by default
134: * returned as-is, without parsing them through MessageFormat.
135: * Set this to "true" to enforce MessageFormat for all messages,
136: * expecting all message texts to be written with MessageFormat escaping.
137: * <p>For example, MessageFormat expects a single quote to be escaped
138: * as "''". If your message texts are all written with such escaping,
139: * even when not defining argument placeholders, you need to set this
140: * flag to "true". Else, only message texts with actual arguments
141: * are supposed to be written with MessageFormat escaping.
142: * @see java.text.MessageFormat
143: */
144: public void setAlwaysUseMessageFormat(boolean alwaysUseMessageFormat) {
145: this .alwaysUseMessageFormat = alwaysUseMessageFormat;
146: }
147:
148: /**
149: * Return whether to always apply the MessageFormat rules, parsing even
150: * messages without arguments.
151: */
152: protected boolean isAlwaysUseMessageFormat() {
153: return this .alwaysUseMessageFormat;
154: }
155:
156: public final String getMessage(String code, Object[] args,
157: String defaultMessage, Locale locale) {
158: String msg = getMessageInternal(code, args, locale);
159: if (msg != null) {
160: return msg;
161: }
162: if (defaultMessage == null) {
163: String fallback = getDefaultMessage(code);
164: if (fallback != null) {
165: return fallback;
166: }
167: }
168: return renderDefaultMessage(defaultMessage, args, locale);
169: }
170:
171: public final String getMessage(String code, Object[] args,
172: Locale locale) throws NoSuchMessageException {
173: String msg = getMessageInternal(code, args, locale);
174: if (msg != null) {
175: return msg;
176: }
177: String fallback = getDefaultMessage(code);
178: if (fallback != null) {
179: return fallback;
180: }
181: throw new NoSuchMessageException(code, locale);
182: }
183:
184: public final String getMessage(MessageSourceResolvable resolvable,
185: Locale locale) throws NoSuchMessageException {
186:
187: String[] codes = resolvable.getCodes();
188: if (codes == null) {
189: codes = new String[0];
190: }
191: for (int i = 0; i < codes.length; i++) {
192: String msg = getMessageInternal(codes[i], resolvable
193: .getArguments(), locale);
194: if (msg != null) {
195: return msg;
196: }
197: }
198: if (resolvable.getDefaultMessage() != null) {
199: return renderDefaultMessage(resolvable.getDefaultMessage(),
200: resolvable.getArguments(), locale);
201: }
202: if (codes.length > 0) {
203: String fallback = getDefaultMessage(codes[0]);
204: if (fallback != null) {
205: return fallback;
206: }
207: }
208: throw new NoSuchMessageException(
209: codes.length > 0 ? codes[codes.length - 1] : null,
210: locale);
211: }
212:
213: /**
214: * Resolve the given code and arguments as message in the given Locale,
215: * returning null if not found. Does <i>not</i> fall back to the code
216: * as default message. Invoked by getMessage methods.
217: * @param code the code to lookup up, such as 'calculator.noRateSet'
218: * @param args array of arguments that will be filled in for params
219: * within the message
220: * @param locale the Locale in which to do the lookup
221: * @return the resolved message, or <code>null</code> if not found
222: * @see #getMessage(String, Object[], String, Locale)
223: * @see #getMessage(String, Object[], Locale)
224: * @see #getMessage(MessageSourceResolvable, Locale)
225: * @see #setUseCodeAsDefaultMessage
226: */
227: protected String getMessageInternal(String code, Object[] args,
228: Locale locale) {
229: if (code == null) {
230: return null;
231: }
232: if (locale == null) {
233: locale = Locale.getDefault();
234: }
235: Object[] argsToUse = args;
236:
237: if (!isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args)) {
238: // Optimized resolution: no arguments to apply,
239: // therefore no MessageFormat needs to be involved.
240: // Note that the default implementation still uses MessageFormat;
241: // this can be overridden in specific subclasses.
242: String message = resolveCodeWithoutArguments(code, locale);
243: if (message != null) {
244: return message;
245: }
246: }
247:
248: else {
249: // Resolve arguments eagerly, for the case where the message
250: // is defined in a parent MessageSource but resolvable arguments
251: // are defined in the child MessageSource.
252: argsToUse = resolveArguments(args, locale);
253:
254: MessageFormat messageFormat = resolveCode(code, locale);
255: if (messageFormat != null) {
256: synchronized (messageFormat) {
257: return messageFormat.format(argsToUse);
258: }
259: }
260: }
261:
262: // Not found -> check parent, if any.
263: return getMessageFromParent(code, argsToUse, locale);
264: }
265:
266: /**
267: * Try to retrieve the given message from the parent MessageSource, if any.
268: * @param code the code to lookup up, such as 'calculator.noRateSet'
269: * @param args array of arguments that will be filled in for params
270: * within the message
271: * @param locale the Locale in which to do the lookup
272: * @return the resolved message, or <code>null</code> if not found
273: * @see #getParentMessageSource()
274: */
275: protected String getMessageFromParent(String code, Object[] args,
276: Locale locale) {
277: MessageSource parent = getParentMessageSource();
278: if (parent != null) {
279: if (parent instanceof AbstractMessageSource) {
280: // Call internal method to avoid getting the default code back
281: // in case of "useCodeAsDefaultMessage" being activated.
282: return ((AbstractMessageSource) parent)
283: .getMessageInternal(code, args, locale);
284: } else {
285: // Check parent MessageSource, returning null if not found there.
286: return parent.getMessage(code, args, null, locale);
287: }
288: }
289: // Not found in parent either.
290: return null;
291: }
292:
293: /**
294: * Return a fallback default message for the given code, if any.
295: * <p>Default is to return the code itself if "useCodeAsDefaultMessage"
296: * is activated, or return no fallback else. In case of no fallback,
297: * the caller will usually receive a NoSuchMessageException from
298: * <code>getMessage</code>.
299: * @param code the message code that we couldn't resolve
300: * and that we didn't receive an explicit default message for
301: * @return the default message to use, or <code>null</code> if none
302: * @see #setUseCodeAsDefaultMessage
303: */
304: protected String getDefaultMessage(String code) {
305: if (isUseCodeAsDefaultMessage()) {
306: return code;
307: }
308: return null;
309: }
310:
311: /**
312: * Render the given default message String. The default message is
313: * passed in as specified by the caller and can be rendered into
314: * a fully formatted default message shown to the user.
315: * <p>Default implementation passes the String to <code>formatMessage</code>,
316: * resolving any argument placeholders found in them. Subclasses may override
317: * this method to plug in custom processing of default messages.
318: * @param defaultMessage the passed-in default message String
319: * @param args array of arguments that will be filled in for params within
320: * the message, or <code>null</code> if none.
321: * @param locale the Locale used for formatting
322: * @return the rendered default message (with resolved arguments)
323: * @see #formatMessage(String, Object[], java.util.Locale)
324: */
325: protected String renderDefaultMessage(String defaultMessage,
326: Object[] args, Locale locale) {
327: return formatMessage(defaultMessage, args, locale);
328: }
329:
330: /**
331: * Format the given message String, using cached MessageFormats.
332: * By default invoked for passed-in default messages, to resolve
333: * any argument placeholders found in them.
334: * @param msg the message to format
335: * @param args array of arguments that will be filled in for params within
336: * the message, or <code>null</code> if none.
337: * @param locale the Locale used for formatting
338: * @return the formatted message (with resolved arguments)
339: */
340: protected String formatMessage(String msg, Object[] args,
341: Locale locale) {
342: if (msg == null
343: || (!this .alwaysUseMessageFormat && (args == null || args.length == 0))) {
344: return msg;
345: }
346: MessageFormat messageFormat = null;
347: synchronized (this .cachedMessageFormats) {
348: messageFormat = (MessageFormat) this .cachedMessageFormats
349: .get(msg);
350: if (messageFormat == null) {
351: messageFormat = createMessageFormat(msg, locale);
352: this .cachedMessageFormats.put(msg, messageFormat);
353: }
354: }
355: synchronized (messageFormat) {
356: return messageFormat.format(resolveArguments(args, locale));
357: }
358: }
359:
360: /**
361: * Create a MessageFormat for the given message and Locale.
362: * <p>This implementation creates an empty MessageFormat first,
363: * populating it with Locale and pattern afterwards, to stay
364: * compatible with J2SE 1.3.
365: * @param msg the message to create a MessageFormat for
366: * @param locale the Locale to create a MessageFormat for
367: * @return the MessageFormat instance
368: */
369: protected MessageFormat createMessageFormat(String msg,
370: Locale locale) {
371: if (logger.isDebugEnabled()) {
372: logger.debug("Creating MessageFormat for pattern [" + msg
373: + "] and locale '" + locale + "'");
374: }
375: MessageFormat messageFormat = new MessageFormat("");
376: messageFormat.setLocale(locale);
377: if (msg != null) {
378: messageFormat.applyPattern(msg);
379: }
380: return messageFormat;
381: }
382:
383: /**
384: * Search through the given array of objects, find any
385: * MessageSourceResolvable objects and resolve them.
386: * <p>Allows for messages to have MessageSourceResolvables as arguments.
387: * @param args array of arguments for a message
388: * @param locale the locale to resolve through
389: * @return an array of arguments with any MessageSourceResolvables resolved
390: */
391: protected Object[] resolveArguments(Object[] args, Locale locale) {
392: if (args == null) {
393: return new Object[0];
394: }
395: List resolvedArgs = new ArrayList(args.length);
396: for (int i = 0; i < args.length; i++) {
397: if (args[i] instanceof MessageSourceResolvable) {
398: resolvedArgs.add(getMessage(
399: (MessageSourceResolvable) args[i], locale));
400: } else {
401: resolvedArgs.add(args[i]);
402: }
403: }
404: return resolvedArgs.toArray(new Object[resolvedArgs.size()]);
405: }
406:
407: /**
408: * Subclasses can override this method to resolve a message without
409: * arguments in an optimized fashion, that is, to resolve a message
410: * without involving a MessageFormat.
411: * <p>The default implementation <i>does</i> use MessageFormat,
412: * through delegating to the <code>resolveCode</code> method.
413: * Subclasses are encouraged to replace this with optimized resolution.
414: * <p>Unfortunately, <code>java.text.MessageFormat</code> is not
415: * implemented in an efficient fashion. In particular, it does not
416: * detect that a message pattern doesn't contain argument placeholders
417: * in the first place. Therefore, it's advisable to circumvent
418: * MessageFormat completely for messages without arguments.
419: * @param code the code of the message to resolve
420: * @param locale the Locale to resolve the code for
421: * (subclasses are encouraged to support internationalization)
422: * @return the message String, or <code>null</code> if not found
423: * @see #resolveCode
424: * @see java.text.MessageFormat
425: */
426: protected String resolveCodeWithoutArguments(String code,
427: Locale locale) {
428: MessageFormat messageFormat = resolveCode(code, locale);
429: if (messageFormat != null) {
430: synchronized (messageFormat) {
431: return messageFormat.format(new Object[0]);
432: }
433: }
434: return null;
435: }
436:
437: /**
438: * Subclasses must implement this method to resolve a message.
439: * <p>Returns a MessageFormat instance rather than a message String,
440: * to allow for appropriate caching of MessageFormats in subclasses.
441: * <p><b>Subclasses are encouraged to provide optimized resolution
442: * for messages without arguments, not involving MessageFormat.</b>
443: * See <code>resolveCodeWithoutArguments</code> javadoc for details.
444: * @param code the code of the message to resolve
445: * @param locale the Locale to resolve the code for
446: * (subclasses are encouraged to support internationalization)
447: * @return the MessageFormat for the message, or <code>null</code> if not found
448: * @see #resolveCodeWithoutArguments(String, java.util.Locale)
449: */
450: protected abstract MessageFormat resolveCode(String code,
451: Locale locale);
452:
453: }
|