View Javadoc

1   /*
2    * Copyright 1999-2004 The Apache Software Foundation.
3    * Modifications, Copyright 2005 Stephen Colebourne
4    * 
5    * Licensed under the Apache License, Version 2.0 (the "License");
6    * you may not use this file except in compliance with the License.
7    * 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.joda.time.contrib.jsptag;
18  
19  import java.text.DateFormat;
20  import java.text.NumberFormat;
21  import java.util.Enumeration;
22  import java.util.Locale;
23  import java.util.MissingResourceException;
24  import java.util.ResourceBundle;
25  import java.util.Vector;
26  
27  import javax.servlet.ServletResponse;
28  import javax.servlet.http.HttpServletRequest;
29  import javax.servlet.jsp.PageContext;
30  import javax.servlet.jsp.jstl.core.Config;
31  import javax.servlet.jsp.jstl.fmt.LocalizationContext;
32  import javax.servlet.jsp.tagext.Tag;
33  
34  /**
35   * <p>
36   * Utilities in support of tag-handler classes.
37   * </p>
38   * 
39   * @author Jan Luehe
40   * @author Jim Newsham
41   */
42  public class Util {
43  
44      private static final String REQUEST = "request";
45  
46      private static final String SESSION = "session";
47  
48      private static final String APPLICATION = "application";
49  
50      private static final char HYPHEN = '-';
51  
52      private static final char UNDERSCORE = '_';
53  
54      private static final Locale EMPTY_LOCALE = new Locale("", "");
55  
56      static final String REQUEST_CHAR_SET = "javax.servlet.jsp.jstl.fmt.request.charset";
57  
58      /**
59       * Converts the given string description of a scope to the corresponding
60       * PageContext constant.
61       * 
62       * The validity of the given scope has already been checked by the
63       * appropriate TLV.
64       * 
65       * @param scope String description of scope
66       * 
67       * @return PageContext constant corresponding to given scope description
68       */
69      public static int getScope(String scope) {
70          int ret = PageContext.PAGE_SCOPE; // default
71  
72          if (REQUEST.equalsIgnoreCase(scope)) {
73              ret = PageContext.REQUEST_SCOPE;
74          } else if (SESSION.equalsIgnoreCase(scope)) {
75              ret = PageContext.SESSION_SCOPE;
76          } else if (APPLICATION.equalsIgnoreCase(scope)) {
77              ret = PageContext.APPLICATION_SCOPE;
78          }
79          return ret;
80      }
81  
82      /**
83       * HttpServletRequest.getLocales() returns the server's default locale if
84       * the request did not specify a preferred language. We do not want this
85       * behavior, because it prevents us from using the fallback locale. We
86       * therefore need to return an empty Enumeration if no preferred locale has
87       * been specified. This way, the logic for the fallback locale will be able
88       * to kick in.
89       */
90      public static Enumeration getRequestLocales(HttpServletRequest request) {
91          Enumeration values = request.getHeaders("accept-language");
92          if (values.hasMoreElements()) {
93              // At least one "accept-language". Simply return
94              // the enumeration returned by request.getLocales().
95              // System.out.println("At least one accept-language");
96              return request.getLocales();
97          } else {
98              // No header for "accept-language". Simply return
99              // 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 }