001 /*
002 * Copyright 1999-2004 The Apache Software Foundation.
003 * Modifications, Copyright 2005 Stephen Colebourne
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * 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.joda.time.contrib.jsptag;
018
019 import java.text.DateFormat;
020 import java.text.NumberFormat;
021 import java.util.Enumeration;
022 import java.util.Locale;
023 import java.util.MissingResourceException;
024 import java.util.ResourceBundle;
025 import java.util.Vector;
026
027 import javax.servlet.ServletResponse;
028 import javax.servlet.http.HttpServletRequest;
029 import javax.servlet.jsp.PageContext;
030 import javax.servlet.jsp.jstl.core.Config;
031 import javax.servlet.jsp.jstl.fmt.LocalizationContext;
032 import javax.servlet.jsp.tagext.Tag;
033
034 /**
035 * <p>
036 * Utilities in support of tag-handler classes.
037 * </p>
038 *
039 * @author Jan Luehe
040 * @author Jim Newsham
041 */
042 public class Util {
043
044 private static final String REQUEST = "request";
045
046 private static final String SESSION = "session";
047
048 private static final String APPLICATION = "application";
049
050 private static final char HYPHEN = '-';
051
052 private static final char UNDERSCORE = '_';
053
054 private static final Locale EMPTY_LOCALE = new Locale("", "");
055
056 static final String REQUEST_CHAR_SET = "javax.servlet.jsp.jstl.fmt.request.charset";
057
058 /**
059 * Converts the given string description of a scope to the corresponding
060 * PageContext constant.
061 *
062 * The validity of the given scope has already been checked by the
063 * appropriate TLV.
064 *
065 * @param scope String description of scope
066 *
067 * @return PageContext constant corresponding to given scope description
068 */
069 public static int getScope(String scope) {
070 int ret = PageContext.PAGE_SCOPE; // default
071
072 if (REQUEST.equalsIgnoreCase(scope)) {
073 ret = PageContext.REQUEST_SCOPE;
074 } else if (SESSION.equalsIgnoreCase(scope)) {
075 ret = PageContext.SESSION_SCOPE;
076 } else if (APPLICATION.equalsIgnoreCase(scope)) {
077 ret = PageContext.APPLICATION_SCOPE;
078 }
079 return ret;
080 }
081
082 /**
083 * HttpServletRequest.getLocales() returns the server's default locale if
084 * the request did not specify a preferred language. We do not want this
085 * behavior, because it prevents us from using the fallback locale. We
086 * therefore need to return an empty Enumeration if no preferred locale has
087 * been specified. This way, the logic for the fallback locale will be able
088 * to kick in.
089 */
090 public static Enumeration getRequestLocales(HttpServletRequest request) {
091 Enumeration values = request.getHeaders("accept-language");
092 if (values.hasMoreElements()) {
093 // At least one "accept-language". Simply return
094 // the enumeration returned by request.getLocales().
095 // System.out.println("At least one accept-language");
096 return request.getLocales();
097 } else {
098 // No header for "accept-language". Simply return
099 // the empty enumeration.
100 // System.out.println("No accept-language");
101 return values;
102 }
103 }
104
105 /**
106 * See parseLocale(String, String) for details.
107 */
108 public static Locale parseLocale(String locale) {
109 return parseLocale(locale, null);
110 }
111
112 /**
113 * Parses the given locale string into its language and (optionally) country
114 * components, and returns the corresponding <tt>java.util.Locale</tt>
115 * object.
116 *
117 * If the given locale string is null or empty, the runtime's default locale
118 * is returned.
119 *
120 * @param locale the locale string to parse
121 * @param variant the variant
122 *
123 * @return <tt>java.util.Locale</tt> object corresponding to the given
124 * locale string, or the runtime's default locale if the locale string is
125 * null or empty
126 *
127 * @throws IllegalArgumentException if the given locale does not have a
128 * language component or has an empty country component
129 */
130 public static Locale parseLocale(String locale, String variant) {
131 Locale ret = null;
132 String language = locale;
133 String country = null;
134 int index = -1;
135
136 if (((index = locale.indexOf(HYPHEN)) > -1)
137 || ((index = locale.indexOf(UNDERSCORE)) > -1)) {
138 language = locale.substring(0, index);
139 country = locale.substring(index + 1);
140 }
141
142 if ((language == null) || (language.length() == 0)) {
143 throw new IllegalArgumentException(Resources
144 .getMessage("LOCALE_NO_LANGUAGE"));
145 }
146
147 if (country == null) {
148 if (variant != null) {
149 ret = new Locale(language, "", variant);
150 } else {
151 ret = new Locale(language, "");
152 }
153 } else if (country.length() > 0) {
154 if (variant != null) {
155 ret = new Locale(language, country, variant);
156 } else {
157 ret = new Locale(language, country);
158 }
159 } else {
160 throw new IllegalArgumentException(Resources
161 .getMessage("LOCALE_EMPTY_COUNTRY"));
162 }
163
164 return ret;
165 }
166
167 /**
168 * Stores the given locale in the response object of the given page context,
169 * and stores the locale's associated charset in the
170 * javax.servlet.jsp.jstl.fmt.request.charset session attribute, which may
171 * be used by the <requestEncoding> action in a page invoked by a form
172 * included in the response to set the request charset to the same as the
173 * response charset (this makes it possible for the container to decode the
174 * form parameter values properly, since browsers typically encode form
175 * field values using the response's charset).
176 *
177 * @param pc the page context whose response object is assigned the
178 * given locale
179 * @param locale the response locale
180 */
181 static void setResponseLocale(PageContext pc, Locale locale) {
182 // set response locale
183 ServletResponse response = pc.getResponse();
184 response.setLocale(locale);
185
186 // get response character encoding and store it in session attribute
187 if (pc.getSession() != null) {
188 try {
189 pc.setAttribute(REQUEST_CHAR_SET, response
190 .getCharacterEncoding(), PageContext.SESSION_SCOPE);
191 } catch (IllegalStateException ex) {
192 // invalidated session ignored
193 }
194 }
195 }
196
197 /**
198 * Returns the formatting locale to use with the given formatting action in
199 * the given page.
200 *
201 * @param pc The page context containing the formatting action @param
202 * fromTag The formatting action @param format <tt>true</tt> if the
203 * formatting action is of type <formatXXX> (as opposed to <parseXXX>), and
204 * <tt>false</tt> otherwise (if set to <tt>true</tt>, the formatting
205 * locale that is returned by this method is used to set the response
206 * locale).
207 *
208 * @param avail the array of available locales
209 *
210 * @return the formatting locale to use
211 */
212 static Locale getFormattingLocale(PageContext pc, Tag fromTag,
213 boolean format, Locale[] avail) {
214
215 LocalizationContext locCtxt = null;
216
217 /*
218 * // Get formatting locale from enclosing <fmt:bundle> Tag parent =
219 * findAncestorWithClass(fromTag, BundleSupport.class); if (parent !=
220 * null) { /* use locale from localization context established by parent
221 * <fmt:bundle> action, unless that locale is null / locCtxt =
222 * ((BundleSupport) parent).getLocalizationContext(); if
223 * (locCtxt.getLocale() != null) { if (format) { setResponseLocale(pc,
224 * locCtxt.getLocale()); } return locCtxt.getLocale(); } }
225 */
226
227 // Use locale from default I18N localization context, unless it is null
228 if ((locCtxt = getLocalizationContext(pc)) != null) {
229 if (locCtxt.getLocale() != null) {
230 if (format) {
231 setResponseLocale(pc, locCtxt.getLocale());
232 }
233 return locCtxt.getLocale();
234 }
235 }
236
237 /*
238 * Establish formatting locale by comparing the preferred locales (in
239 * order of preference) against the available formatting locales, and
240 * determining the best matching locale.
241 */
242 Locale match = null;
243 Locale pref = getLocale(pc, Config.FMT_LOCALE);
244 if (pref != null) {
245 // Preferred locale is application-based
246 match = findFormattingMatch(pref, avail);
247 } else {
248 // Preferred locales are browser-based
249 match = findFormattingMatch(pc, avail);
250 }
251 if (match == null) {
252 // Use fallback locale.
253 pref = getLocale(pc, Config.FMT_FALLBACK_LOCALE);
254 if (pref != null) {
255 match = findFormattingMatch(pref, avail);
256 }
257 }
258 if (format && (match != null)) {
259 setResponseLocale(pc, match);
260 }
261
262 return match;
263 }
264
265 /**
266 * Setup the available formatting locales that will be used by
267 * getFormattingLocale(PageContext).
268 */
269 static Locale[] availableFormattingLocales;
270 static {
271 Locale[] dateLocales = DateFormat.getAvailableLocales();
272 Locale[] numberLocales = NumberFormat.getAvailableLocales();
273 Vector vec = new Vector(dateLocales.length);
274 for (int i = 0; i < dateLocales.length; i++) {
275 for (int j = 0; j < numberLocales.length; j++) {
276 if (dateLocales[i].equals(numberLocales[j])) {
277 vec.add(dateLocales[i]);
278 break;
279 }
280 }
281 }
282 availableFormattingLocales = new Locale[vec.size()];
283 availableFormattingLocales = (Locale[]) vec
284 .toArray(availableFormattingLocales);
285 /*
286 * for (int i=0; i<availableFormattingLocales.length; i++) {
287 * System.out.println("AvailableLocale[" + i + "] " +
288 * availableFormattingLocales[i]); }
289 */
290 }
291
292 /**
293 * Returns the formatting locale to use when <fmt:message> is used with a
294 * locale-less localization context.
295 *
296 * @param pc The page context containing the formatting action @return the
297 * formatting locale to use
298 */
299 static Locale getFormattingLocale(PageContext pc) {
300 /*
301 * Establish formatting locale by comparing the preferred locales (in
302 * order of preference) against the available formatting locales, and
303 * determining the best matching locale.
304 */
305 Locale match = null;
306 Locale pref = getLocale(pc, Config.FMT_LOCALE);
307 if (pref != null) {
308 // Preferred locale is application-based
309 match = findFormattingMatch(pref, availableFormattingLocales);
310 } else {
311 // Preferred locales are browser-based
312 match = findFormattingMatch(pc, availableFormattingLocales);
313 }
314 if (match == null) {
315 // Use fallback locale.
316 pref = getLocale(pc, Config.FMT_FALLBACK_LOCALE);
317 if (pref != null) {
318 match = findFormattingMatch(pref, availableFormattingLocales);
319 }
320 }
321 if (match != null) {
322 setResponseLocale(pc, match);
323 }
324
325 return match;
326 }
327
328 /**
329 * Returns the locale specified by the named scoped attribute or context
330 * configuration parameter.
331 *
332 * <p> The named scoped attribute is searched in the page, request, session
333 * (if valid), and application scope(s) (in this order). If no such
334 * attribute exists in any of the scopes, the locale is taken from the named
335 * context configuration parameter.
336 *
337 * @param pageContext the page in which to search for the named scoped
338 * attribute or context configuration parameter @param name the name of the
339 * scoped attribute or context configuration parameter
340 *
341 * @return the locale specified by the named scoped attribute or context
342 * configuration parameter, or <tt>null</tt> if no scoped attribute or
343 * configuration parameter with the given name exists
344 */
345 static Locale getLocale(PageContext pageContext, String name) {
346 Locale loc = null;
347
348 Object obj = Config.find(pageContext, name);
349 if (obj != null) {
350 if (obj instanceof Locale) {
351 loc = (Locale) obj;
352 } else {
353 loc = parseLocale((String) obj);
354 }
355 }
356
357 return loc;
358 }
359
360 // *********************************************************************
361 // Private utility methods
362
363 /**
364 * Determines the client's preferred locales from the request, and compares
365 * each of the locales (in order of preference) against the available
366 * locales in order to determine the best matching locale.
367 *
368 * @param pageContext Page containing the formatting action @param avail
369 * Available formatting locales
370 *
371 * @return Best matching locale, or <tt>null</tt> if no match was found
372 */
373 private static Locale findFormattingMatch(PageContext pageContext,
374 Locale[] avail) {
375 Locale match = null;
376 for (Enumeration enum_ = Util
377 .getRequestLocales((HttpServletRequest) pageContext
378 .getRequest()); enum_.hasMoreElements();) {
379 Locale locale = (Locale) enum_.nextElement();
380 match = findFormattingMatch(locale, avail);
381 if (match != null) {
382 break;
383 }
384 }
385
386 return match;
387 }
388
389 /**
390 * Returns the best match between the given preferred locale and the given
391 * available locales.
392 *
393 * The best match is given as the first available locale that exactly
394 * matches the given preferred locale ("exact match"). If no exact match
395 * exists, the best match is given to an available locale that meets the
396 * following criteria (in order of priority): - available locale's variant
397 * is empty and exact match for both language and country - available
398 * locale's variant and country are empty, and exact match for language.
399 *
400 * @param pref the preferred locale @param avail the available formatting
401 * locales
402 *
403 * @return Available locale that best matches the given preferred locale, or
404 * <tt>null</tt> if no match exists
405 */
406 private static Locale findFormattingMatch(Locale pref, Locale[] avail) {
407 Locale match = null;
408 boolean langAndCountryMatch = false;
409 for (int i = 0; i < avail.length; i++) {
410 if (pref.equals(avail[i])) {
411 // Exact match
412 match = avail[i];
413 break;
414 } else if (!"".equals(pref.getVariant())
415 && "".equals(avail[i].getVariant())
416 && pref.getLanguage().equals(avail[i].getLanguage())
417 && pref.getCountry().equals(avail[i].getCountry())) {
418 // Language and country match; different variant
419 match = avail[i];
420 langAndCountryMatch = true;
421 } else if (!langAndCountryMatch
422 && pref.getLanguage().equals(avail[i].getLanguage())
423 && ("".equals(avail[i].getCountry()))) {
424 // Language match
425 if (match == null) {
426 match = avail[i];
427 }
428 }
429 }
430 return match;
431 }
432
433 /**
434 * Gets the default I18N localization context.
435 *
436 * @param pc Page in which to look up the default I18N localization context
437 */
438 public static LocalizationContext getLocalizationContext(PageContext pc) {
439 LocalizationContext locCtxt = null;
440
441 Object obj = Config.find(pc, Config.FMT_LOCALIZATION_CONTEXT);
442 if (obj == null) {
443 return null;
444 }
445
446 if (obj instanceof LocalizationContext) {
447 locCtxt = (LocalizationContext) obj;
448 } else {
449 // localization context is a bundle basename
450 locCtxt = getLocalizationContext(pc, (String) obj);
451 }
452
453 return locCtxt;
454 }
455
456 /**
457 * Gets the resource bundle with the given base name, whose locale is
458 * determined as follows:
459 *
460 * Check if a match exists between the ordered set of preferred locales and
461 * the available locales, for the given base name. The set of preferred
462 * locales consists of a single locale (if the
463 * <tt>javax.servlet.jsp.jstl.fmt.locale</tt> configuration setting is
464 * present) or is equal to the client's preferred locales determined from
465 * the client's browser settings.
466 *
467 * <p>
468 * If no match was found in the previous step, check if a match exists
469 * between the fallback locale (given by the
470 * <tt>javax.servlet.jsp.jstl.fmt.fallbackLocale</tt> configuration
471 * setting) and the available locales, for the given base name.
472 *
473 * @param pc Page in which the resource bundle with the given base
474 * name is requested
475 * @param basename Resource bundle base name
476 *
477 * @return Localization context containing the resource bundle with the
478 * given base name and the locale that led to the resource bundle match, or
479 * the empty localization context if no resource bundle match was found
480 */
481 public static LocalizationContext getLocalizationContext(PageContext pc,
482 String basename) {
483 LocalizationContext locCtxt = null;
484 ResourceBundle bundle = null;
485
486 if ((basename == null) || basename.equals("")) {
487 return new LocalizationContext();
488 }
489
490 // Try preferred locales
491 Locale pref = getLocale(pc, Config.FMT_LOCALE);
492 if (pref != null) {
493 // Preferred locale is application-based
494 bundle = findMatch(basename, pref);
495 if (bundle != null) {
496 locCtxt = new LocalizationContext(bundle, pref);
497 }
498 } else {
499 // Preferred locales are browser-based
500 locCtxt = findMatch(pc, basename);
501 }
502
503 if (locCtxt == null) {
504 // No match found with preferred locales, try using fallback locale
505 pref = getLocale(pc, Config.FMT_FALLBACK_LOCALE);
506 if (pref != null) {
507 bundle = findMatch(basename, pref);
508 if (bundle != null) {
509 locCtxt = new LocalizationContext(bundle, pref);
510 }
511 }
512 }
513
514 if (locCtxt == null) {
515 // try using the root resource bundle with the given basename
516 try {
517 bundle = ResourceBundle.getBundle(basename, EMPTY_LOCALE,
518 Thread.currentThread().getContextClassLoader());
519 if (bundle != null) {
520 locCtxt = new LocalizationContext(bundle, null);
521 }
522 } catch (MissingResourceException mre) {
523 // do nothing
524 }
525 }
526
527 if (locCtxt != null) {
528 // set response locale
529 if (locCtxt.getLocale() != null) {
530 setResponseLocale(pc, locCtxt.getLocale());
531 }
532 } else {
533 // create empty localization context
534 locCtxt = new LocalizationContext();
535 }
536
537 return locCtxt;
538 }
539
540 /**
541 * Determines the client's preferred locales from the request, and compares
542 * each of the locales (in order of preference) against the available
543 * locales in order to determine the best matching locale.
544 *
545 * @param pageContext the page in which the resource bundle with the given
546 * base name is requested @param basename the resource bundle's base name
547 *
548 * @return the localization context containing the resource bundle with the
549 * given base name and best matching locale, or <tt>null</tt> if no
550 * resource bundle match was found
551 */
552 private static LocalizationContext findMatch(PageContext pageContext,
553 String basename) {
554 LocalizationContext locCtxt = null;
555
556 // Determine locale from client's browser settings.
557 for (Enumeration enum_ = Util
558 .getRequestLocales((HttpServletRequest) pageContext
559 .getRequest()); enum_.hasMoreElements();) {
560 Locale pref = (Locale) enum_.nextElement();
561 ResourceBundle match = findMatch(basename, pref);
562 if (match != null) {
563 locCtxt = new LocalizationContext(match, pref);
564 break;
565 }
566 }
567
568 return locCtxt;
569 }
570
571 /**
572 * Gets the resource bundle with the given base name and preferred locale.
573 *
574 * This method calls java.util.ResourceBundle.getBundle(), but ignores its
575 * return value unless its locale represents an exact or language match with
576 * the given preferred locale.
577 *
578 * @param basename the resource bundle base name @param pref the preferred
579 * locale
580 *
581 * @return the requested resource bundle, or <tt>null</tt> if no resource
582 * bundle with the given base name exists or if there is no exact- or
583 * language-match between the preferred locale and the locale of the bundle
584 * returned by java.util.ResourceBundle.getBundle().
585 */
586 private static ResourceBundle findMatch(String basename, Locale pref) {
587 ResourceBundle match = null;
588
589 try {
590 ResourceBundle bundle = ResourceBundle.getBundle(basename, pref,
591 Thread.currentThread().getContextClassLoader());
592 Locale avail = bundle.getLocale();
593 if (pref.equals(avail)) {
594 // Exact match
595 match = bundle;
596 } else {
597 /*
598 * We have to make sure that the match we got is for the
599 * specified locale. The way ResourceBundle.getBundle() works,
600 * if a match is not found with (1) the specified locale, it
601 * tries to match with (2) the current default locale as
602 * returned by Locale.getDefault() or (3) the root resource
603 * bundle (basename). We must ignore any match that could have
604 * worked with (2) or (3). So if an exact match is not found, we
605 * make the following extra tests: - avail locale must be equal
606 * to preferred locale - avail country must be empty or equal to
607 * preferred country (the equality match might have failed on
608 * the variant)
609 */
610 if (pref.getLanguage().equals(avail.getLanguage())
611 && ("".equals(avail.getCountry()) || pref.getCountry()
612 .equals(avail.getCountry()))) {
613 /*
614 * Language match. By making sure the available locale does
615 * not have a country and matches the preferred locale's
616 * language, we rule out "matches" based on the container's
617 * default locale. For example, if the preferred locale is
618 * "en-US", the container's default locale is "en-UK", and
619 * there is a resource bundle (with the requested base name)
620 * available for "en-UK", ResourceBundle.getBundle() will
621 * return it, but even though its language matches that of
622 * the preferred locale, we must ignore it, because matches
623 * based on the container's default locale are not portable
624 * across different containers with different default
625 * locales.
626 */
627 match = bundle;
628 }
629 }
630 } catch (MissingResourceException mre) {
631 }
632
633 return match;
634 }
635
636 }