001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * Portions Copyright Apache Software Foundation.
007: *
008: * The contents of this file are subject to the terms of either the GNU
009: * General Public License Version 2 only ("GPL") or the Common Development
010: * and Distribution License("CDDL") (collectively, the "License"). You
011: * may not use this file except in compliance with the License. You can obtain
012: * a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html
013: * or glassfish/bootstrap/legal/LICENSE.txt. See the License for the specific
014: * language governing permissions and limitations under the License.
015: *
016: * When distributing the software, include this License Header Notice in each
017: * file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
018: * Sun designates this particular file as subject to the "Classpath" exception
019: * as provided by Sun in the GPL Version 2 section of the License file that
020: * accompanied this code. If applicable, add the following below the License
021: * Header, with the fields enclosed by brackets [] replaced by your own
022: * identifying information: "Portions Copyrighted [year]
023: * [name of copyright owner]"
024: *
025: * Contributor(s):
026: *
027: * If you wish your version of this file to be governed by only the CDDL or
028: * only the GPL Version 2, indicate your decision by adding "[Contributor]
029: * elects to include this software in this distribution under the [CDDL or GPL
030: * Version 2] license." If you don't indicate a single choice of license, a
031: * recipient has the option to distribute your version of this file under
032: * either the CDDL, the GPL Version 2 or to extend the choice of license to
033: * its licensees as provided above. However, if you add GPL Version 2 code
034: * and therefore, elected the GPL Version 2 license, then the option applies
035: * only if the new code is made subject to such option by the copyright
036: * holder.
037: */
038:
039: package javax.servlet.jsp.jstl.fmt;
040:
041: import java.text.MessageFormat;
042: import java.util.Enumeration;
043: import java.util.Locale;
044: import java.util.MissingResourceException;
045: import java.util.ResourceBundle;
046:
047: import javax.servlet.ServletResponse;
048: import javax.servlet.http.HttpServletRequest;
049: import javax.servlet.jsp.PageContext;
050: import javax.servlet.jsp.jstl.core.Config;
051:
052: /**
053: * Class which exposes the locale-determination logic for resource bundles
054: * through convenience methods.
055: *
056: * <p> This class may be useful to any tag handler implementation that needs
057: * to produce localized messages. For example, this might be useful for
058: * exception messages that are intended directly for user consumption on an
059: * error page.
060: *
061: * @author Jan Luehe
062: */
063:
064: public class LocaleSupport {
065:
066: private static final String UNDEFINED_KEY = "???";
067: private static final char HYPHEN = '-';
068: private static final char UNDERSCORE = '_';
069: private static final String REQUEST_CHAR_SET = "javax.servlet.jsp.jstl.fmt.request.charset";
070: private static final Locale EMPTY_LOCALE = new Locale("", "");
071:
072: /**
073: * Retrieves the localized message corresponding to the given key.
074: *
075: * <p> The given key is looked up in the resource bundle of the default
076: * I18N localization context, which is retrieved from the
077: * <tt>javax.servlet.jsp.jstl.fmt.localizationContext</tt> configuration
078: * setting.
079: *
080: * <p> If the configuration setting is empty, or the default I18N
081: * localization context does not contain any resource bundle, or the given
082: * key is undefined in its resource bundle, the string "???<key>???" is
083: * returned, where "<key>" is replaced with the given key.
084: *
085: * @param pageContext the page in which to get the localized message
086: * corresponding to the given key
087: * @param key the message key
088: *
089: * @return the localized message corresponding to the given key
090: */
091: public static String getLocalizedMessage(PageContext pageContext,
092: String key) {
093: return getLocalizedMessage(pageContext, key, null, null);
094: }
095:
096: /**
097: * Retrieves the localized message corresponding to the given key.
098: *
099: * <p> The given key is looked up in the resource bundle with the given
100: * base name.
101: *
102: * <p> If no resource bundle with the given base name exists, or the given
103: * key is undefined in the resource bundle, the string "???<key>???" is
104: * returned, where "<key>" is replaced with the given key.
105: *
106: * @param pageContext the page in which to get the localized message
107: * corresponding to the given key
108: * @param key the message key
109: * @param basename the resource bundle base name
110: *
111: * @return the localized message corresponding to the given key
112: */
113: public static String getLocalizedMessage(PageContext pageContext,
114: String key, String basename) {
115: return getLocalizedMessage(pageContext, key, null, basename);
116: }
117:
118: /**
119: * Retrieves the localized message corresponding to the given key, and
120: * performs parametric replacement using the arguments specified via
121: * <tt>args</tt>.
122: *
123: * <p> See the specification of the <fmt:message> action for a description
124: * of how parametric replacement is implemented.
125: *
126: * <p> The localized message is retrieved as in
127: * {@link #getLocalizedMessage(javax.servlet.jsp.PageContext,java.lang.String) getLocalizedMessage(pageContext, key)}.
128: *
129: * @param pageContext the page in which to get the localized message
130: * corresponding to the given key
131: * @param key the message key
132: * @param args the arguments for parametric replacement
133: *
134: * @return the localized message corresponding to the given key
135: */
136: public static String getLocalizedMessage(PageContext pageContext,
137: String key, Object[] args) {
138: return getLocalizedMessage(pageContext, key, args, null);
139: }
140:
141: /**
142: * Retrieves the localized message corresponding to the given key, and
143: * performs parametric replacement using the arguments specified via
144: * <tt>args</tt>.
145: *
146: * <p> See the specification of the <fmt:message> action for a description
147: * of how parametric replacement is implemented.
148: *
149: * <p> The localized message is retrieved as in
150: * {@link #getLocalizedMessage(javax.servlet.jsp.PageContext,java.lang.String, java.lang.String) getLocalizedMessage(pageContext, key, basename)}.
151: *
152: * @param pageContext the page in which to get the localized message
153: * corresponding to the given key
154: * @param key the message key
155: * @param args the arguments for parametric replacement
156: * @param basename the resource bundle base name
157: *
158: * @return the localized message corresponding to the given key
159: */
160: public static String getLocalizedMessage(PageContext pageContext,
161: String key, Object[] args, String basename) {
162: LocalizationContext locCtxt = null;
163: String message = UNDEFINED_KEY + key + UNDEFINED_KEY;
164:
165: if (basename != null) {
166: locCtxt = getLocalizationContext(pageContext, basename);
167: } else {
168: locCtxt = getLocalizationContext(pageContext);
169: }
170:
171: if (locCtxt != null) {
172: ResourceBundle bundle = locCtxt.getResourceBundle();
173: if (bundle != null) {
174: try {
175: message = bundle.getString(key);
176: if (args != null) {
177: MessageFormat formatter = new MessageFormat("");
178: if (locCtxt.getLocale() != null) {
179: formatter.setLocale(locCtxt.getLocale());
180: }
181: formatter.applyPattern(message);
182: message = formatter.format(args);
183: }
184: } catch (MissingResourceException mre) {
185: }
186: }
187: }
188:
189: return message;
190: }
191:
192: /**
193: * Gets the default I18N localization context.
194: *
195: * @param pc Page in which to look up the default I18N localization context
196: */
197: private static LocalizationContext getLocalizationContext(
198: PageContext pc) {
199: LocalizationContext locCtxt = null;
200:
201: Object obj = Config.find(pc, Config.FMT_LOCALIZATION_CONTEXT);
202: if (obj == null) {
203: return null;
204: }
205:
206: if (obj instanceof LocalizationContext) {
207: locCtxt = (LocalizationContext) obj;
208: } else {
209: // localization context is a bundle basename
210: locCtxt = getLocalizationContext(pc, (String) obj);
211: }
212:
213: return locCtxt;
214: }
215:
216: /**
217: * Gets the resource bundle with the given base name, whose locale is
218: * determined as follows:
219: *
220: * Check if a match exists between the ordered set of preferred
221: * locales and the available locales, for the given base name.
222: * The set of preferred locales consists of a single locale
223: * (if the <tt>javax.servlet.jsp.jstl.fmt.locale</tt> configuration
224: * setting is present) or is equal to the client's preferred locales
225: * determined from the client's browser settings.
226: *
227: * <p> If no match was found in the previous step, check if a match
228: * exists between the fallback locale (given by the
229: * <tt>javax.servlet.jsp.jstl.fmt.fallbackLocale</tt> configuration
230: * setting) and the available locales, for the given base name.
231: *
232: * @param pageContext Page in which the resource bundle with the
233: * given base name is requested
234: * @param basename Resource bundle base name
235: *
236: * @return Localization context containing the resource bundle with the
237: * given base name and the locale that led to the resource bundle match,
238: * or the empty localization context if no resource bundle match was found
239: */
240: private static LocalizationContext getLocalizationContext(
241: PageContext pc, String basename) {
242: LocalizationContext locCtxt = null;
243: ResourceBundle bundle = null;
244:
245: if ((basename == null) || basename.equals("")) {
246: return new LocalizationContext();
247: }
248:
249: // Try preferred locales
250: Locale pref = getLocale(pc, Config.FMT_LOCALE);
251: if (pref != null) {
252: // Preferred locale is application-based
253: bundle = findMatch(basename, pref);
254: if (bundle != null) {
255: locCtxt = new LocalizationContext(bundle, pref);
256: }
257: } else {
258: // Preferred locales are browser-based
259: locCtxt = findMatch(pc, basename);
260: }
261:
262: if (locCtxt == null) {
263: // No match found with preferred locales, try using fallback locale
264: pref = getLocale(pc, Config.FMT_FALLBACK_LOCALE);
265: if (pref != null) {
266: bundle = findMatch(basename, pref);
267: if (bundle != null) {
268: locCtxt = new LocalizationContext(bundle, pref);
269: }
270: }
271: }
272:
273: if (locCtxt == null) {
274: // try using the root resource bundle with the given basename
275: try {
276: bundle = ResourceBundle.getBundle(basename,
277: EMPTY_LOCALE, Thread.currentThread()
278: .getContextClassLoader());
279: if (bundle != null) {
280: locCtxt = new LocalizationContext(bundle, null);
281: }
282: } catch (MissingResourceException mre) {
283: // do nothing
284: }
285: }
286:
287: if (locCtxt != null) {
288: // set response locale
289: if (locCtxt.getLocale() != null) {
290: setResponseLocale(pc, locCtxt.getLocale());
291: }
292: } else {
293: // create empty localization context
294: locCtxt = new LocalizationContext();
295: }
296:
297: return locCtxt;
298: }
299:
300: /*
301: * Determines the client's preferred locales from the request, and compares
302: * each of the locales (in order of preference) against the available
303: * locales in order to determine the best matching locale.
304: *
305: * @param pageContext the page in which the resource bundle with the
306: * given base name is requested
307: * @param basename the resource bundle's base name
308: *
309: * @return the localization context containing the resource bundle with
310: * the given base name and best matching locale, or <tt>null</tt> if no
311: * resource bundle match was found
312: */
313: private static LocalizationContext findMatch(
314: PageContext pageContext, String basename) {
315: LocalizationContext locCtxt = null;
316:
317: // Determine locale from client's browser settings.
318:
319: for (Enumeration enum_ = getRequestLocales((HttpServletRequest) pageContext
320: .getRequest()); enum_.hasMoreElements();) {
321: Locale pref = (Locale) enum_.nextElement();
322: ResourceBundle match = findMatch(basename, pref);
323: if (match != null) {
324: locCtxt = new LocalizationContext(match, pref);
325: break;
326: }
327: }
328:
329: return locCtxt;
330: }
331:
332: /*
333: * Gets the resource bundle with the given base name and preferred locale.
334: *
335: * This method calls java.util.ResourceBundle.getBundle(), but ignores
336: * its return value unless its locale represents an exact or language match
337: * with the given preferred locale.
338: *
339: * @param basename the resource bundle base name
340: * @param pref the preferred locale
341: *
342: * @return the requested resource bundle, or <tt>null</tt> if no resource
343: * bundle with the given base name exists or if there is no exact- or
344: * language-match between the preferred locale and the locale of
345: * the bundle returned by java.util.ResourceBundle.getBundle().
346: */
347: private static ResourceBundle findMatch(String basename, Locale pref) {
348: ResourceBundle match = null;
349:
350: try {
351: ResourceBundle bundle = ResourceBundle.getBundle(basename,
352: pref, Thread.currentThread()
353: .getContextClassLoader());
354: Locale avail = bundle.getLocale();
355: if (pref.equals(avail)) {
356: // Exact match
357: match = bundle;
358: } else {
359: /*
360: * We have to make sure that the match we got is for
361: * the specified locale. The way ResourceBundle.getBundle()
362: * works, if a match is not found with (1) the specified locale,
363: * it tries to match with (2) the current default locale as
364: * returned by Locale.getDefault() or (3) the root resource
365: * bundle (basename).
366: * We must ignore any match that could have worked with (2) or (3).
367: * So if an exact match is not found, we make the following extra
368: * tests:
369: * - avail locale must be equal to preferred locale
370: * - avail country must be empty or equal to preferred country
371: * (the equality match might have failed on the variant)
372: */
373: if (pref.getLanguage().equals(avail.getLanguage())
374: && ("".equals(avail.getCountry()) || pref
375: .getCountry()
376: .equals(avail.getCountry()))) {
377: /*
378: * Language match.
379: * By making sure the available locale does not have a
380: * country and matches the preferred locale's language, we
381: * rule out "matches" based on the container's default
382: * locale. For example, if the preferred locale is
383: * "en-US", the container's default locale is "en-UK", and
384: * there is a resource bundle (with the requested base
385: * name) available for "en-UK", ResourceBundle.getBundle()
386: * will return it, but even though its language matches
387: * that of the preferred locale, we must ignore it,
388: * because matches based on the container's default locale
389: * are not portable across different containers with
390: * different default locales.
391: */
392: match = bundle;
393: }
394: }
395: } catch (MissingResourceException mre) {
396: }
397:
398: return match;
399: }
400:
401: /*
402: * Returns the locale specified by the named scoped attribute or context
403: * configuration parameter.
404: *
405: * <p> The named scoped attribute is searched in the page, request,
406: * session (if valid), and application scope(s) (in this order). If no such
407: * attribute exists in any of the scopes, the locale is taken from the
408: * named context configuration parameter.
409: *
410: * @param pageContext the page in which to search for the named scoped
411: * attribute or context configuration parameter
412: * @param name the name of the scoped attribute or context configuration
413: * parameter
414: *
415: * @return the locale specified by the named scoped attribute or context
416: * configuration parameter, or <tt>null</tt> if no scoped attribute or
417: * configuration parameter with the given name exists
418: */
419: private static Locale getLocale(PageContext pageContext, String name) {
420: Locale loc = null;
421:
422: Object obj = Config.find(pageContext, name);
423: if (obj != null) {
424: if (obj instanceof Locale) {
425: loc = (Locale) obj;
426: } else {
427: loc = parseLocale((String) obj);
428: }
429: }
430:
431: return loc;
432: }
433:
434: /*
435: * Stores the given locale in the response object of the given page
436: * context, and stores the locale's associated charset in the
437: * javax.servlet.jsp.jstl.fmt.request.charset session attribute, which
438: * may be used by the <requestEncoding> action in a page invoked by a
439: * form included in the response to set the request charset to the same as
440: * the response charset (this makes it possible for the container to
441: * decode the form parameter values properly, since browsers typically
442: * encode form field values using the response's charset).
443: *
444: * @param pageContext the page context whose response object is assigned
445: * the given locale
446: * @param locale the response locale
447: */
448: private static void setResponseLocale(PageContext pc, Locale locale) {
449: // set response locale
450: ServletResponse response = pc.getResponse();
451: response.setLocale(locale);
452:
453: // get response character encoding and store it in session attribute
454: if (pc.getSession() != null) {
455: try {
456: pc.setAttribute(REQUEST_CHAR_SET, response
457: .getCharacterEncoding(),
458: PageContext.SESSION_SCOPE);
459: } catch (IllegalStateException ex) {
460: } // invalidated session ignored
461: }
462: }
463:
464: /**
465: * See parseLocale(String, String) for details.
466: */
467: private static Locale parseLocale(String locale) {
468: return parseLocale(locale, null);
469: }
470:
471: /**
472: * Parses the given locale string into its language and (optionally)
473: * country components, and returns the corresponding
474: * <tt>java.util.Locale</tt> object.
475: *
476: * If the given locale string is null or empty, the runtime's default
477: * locale is returned.
478: *
479: * @param locale the locale string to parse
480: * @param variant the variant
481: *
482: * @return <tt>java.util.Locale</tt> object corresponding to the given
483: * locale string, or the runtime's default locale if the locale string is
484: * null or empty
485: *
486: * @throws IllegalArgumentException if the given locale does not have a
487: * language component or has an empty country component
488: */
489: private static Locale parseLocale(String locale, String variant) {
490:
491: Locale ret = null;
492: String language = locale;
493: String country = null;
494: int index = -1;
495:
496: if (((index = locale.indexOf(HYPHEN)) > -1)
497: || ((index = locale.indexOf(UNDERSCORE)) > -1)) {
498: language = locale.substring(0, index);
499: country = locale.substring(index + 1);
500: }
501:
502: if ((language == null) || (language.length() == 0)) {
503: throw new IllegalArgumentException(
504: "Missing language component in 'value' attribute in setLocale");
505: }
506:
507: if (country == null) {
508: if (variant != null)
509: ret = new Locale(language, "", variant);
510: else
511: ret = new Locale(language, "");
512: } else if (country.length() > 0) {
513: if (variant != null)
514: ret = new Locale(language, country, variant);
515: else
516: ret = new Locale(language, country);
517: } else {
518: throw new IllegalArgumentException(
519: "Empty country component in 'value' attribute in setLocale");
520: }
521:
522: return ret;
523: }
524:
525: /**
526: * HttpServletRequest.getLocales() returns the server's default locale
527: * if the request did not specify a preferred language.
528: * We do not want this behavior, because it prevents us from using
529: * the fallback locale.
530: * We therefore need to return an empty Enumeration if no preferred
531: * locale has been specified. This way, the logic for the fallback
532: * locale will be able to kick in.
533: */
534: private static Enumeration getRequestLocales(
535: HttpServletRequest request) {
536: Enumeration values = request.getHeaders("accept-language");
537: if (values.hasMoreElements()) {
538: // At least one "accept-language". Simply return
539: // the enumeration returned by request.getLocales().
540: // System.out.println("At least one accept-language");
541: return request.getLocales();
542: } else {
543: // No header for "accept-language". Simply return
544: // the empty enumeration.
545: // System.out.println("No accept-language");
546: return values;
547: }
548: }
549:
550: }
|