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 }