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    }