1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. 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 18 package java.util; 19 20 import dalvik.system.VMStack; 21 import java.io.File; 22 import java.io.IOException; 23 import java.io.InputStream; 24 import java.io.InputStreamReader; 25 import java.net.URL; 26 import java.net.URLConnection; 27 import java.nio.charset.Charsets; 28 import static java.nio.charset.Charsets.UTF_8; 29 import libcore.io.IoUtils; 30 31 /** 32 * {@code ResourceBundle} is an abstract class which is the superclass of classes which 33 * provide {@code Locale}-specific resources. A bundle contains a number of named 34 * resources, where the names are {@code Strings}. A bundle may have a parent bundle, 35 * and when a resource is not found in a bundle, the parent bundle is searched for 36 * the resource. If the fallback mechanism reaches the base bundle and still 37 * can't find the resource it throws a {@code MissingResourceException}. 38 * 39 * <ul> 40 * <li>All bundles for the same group of resources share a common base bundle. 41 * This base bundle acts as the root and is the last fallback in case none of 42 * its children was able to respond to a request.</li> 43 * <li>The first level contains changes between different languages. Only the 44 * differences between a language and the language of the base bundle need to be 45 * handled by a language-specific {@code ResourceBundle}.</li> 46 * <li>The second level contains changes between different countries that use 47 * the same language. Only the differences between a country and the country of 48 * the language bundle need to be handled by a country-specific {@code ResourceBundle}. 49 * </li> 50 * <li>The third level contains changes that don't have a geographic reason 51 * (e.g. changes that where made at some point in time like {@code PREEURO} where the 52 * currency of come countries changed. The country bundle would return the 53 * current currency (Euro) and the {@code PREEURO} variant bundle would return the old 54 * currency (e.g. DM for Germany).</li> 55 * </ul> 56 * 57 * <strong>Examples</strong> 58 * <ul> 59 * <li>BaseName (base bundle) 60 * <li>BaseName_de (german language bundle) 61 * <li>BaseName_fr (french language bundle) 62 * <li>BaseName_de_DE (bundle with Germany specific resources in german) 63 * <li>BaseName_de_CH (bundle with Switzerland specific resources in german) 64 * <li>BaseName_fr_CH (bundle with Switzerland specific resources in french) 65 * <li>BaseName_de_DE_PREEURO (bundle with Germany specific resources in german of 66 * the time before the Euro) 67 * <li>BaseName_fr_FR_PREEURO (bundle with France specific resources in french of 68 * the time before the Euro) 69 * </ul> 70 * 71 * It's also possible to create variants for languages or countries. This can be 72 * done by just skipping the country or language abbreviation: 73 * BaseName_us__POSIX or BaseName__DE_PREEURO. But it's not allowed to 74 * circumvent both language and country: BaseName___VARIANT is illegal. 75 * 76 * @see Properties 77 * @see PropertyResourceBundle 78 * @see ListResourceBundle 79 * @since 1.1 80 */ 81 public abstract class ResourceBundle { 82 83 private static final String UNDER_SCORE = "_"; 84 85 private static final String EMPTY_STRING = ""; 86 87 /** 88 * The parent of this {@code ResourceBundle} that is used if this bundle doesn't 89 * include the requested resource. 90 */ 91 protected ResourceBundle parent; 92 93 private Locale locale; 94 95 private long lastLoadTime = 0; 96 97 static class MissingBundle extends ResourceBundle { 98 @Override getKeys()99 public Enumeration<String> getKeys() { 100 return null; 101 } 102 103 @Override handleGetObject(String name)104 public Object handleGetObject(String name) { 105 return null; 106 } 107 } 108 109 private static final ResourceBundle MISSING = new MissingBundle(); 110 111 private static final ResourceBundle MISSINGBASE = new MissingBundle(); 112 113 private static final WeakHashMap<Object, Hashtable<String, ResourceBundle>> cache 114 = new WeakHashMap<Object, Hashtable<String, ResourceBundle>>(); 115 116 private static Locale cacheLocale = Locale.getDefault(); 117 118 /** 119 * Constructs a new instance of this class. 120 */ ResourceBundle()121 public ResourceBundle() { 122 /* empty */ 123 } 124 125 /** 126 * Finds the named resource bundle for the default {@code Locale} and the caller's 127 * {@code ClassLoader}. 128 * 129 * @param bundleName 130 * the name of the {@code ResourceBundle}. 131 * @return the requested {@code ResourceBundle}. 132 * @throws MissingResourceException 133 * if the {@code ResourceBundle} cannot be found. 134 */ getBundle(String bundleName)135 public static ResourceBundle getBundle(String bundleName) throws MissingResourceException { 136 ClassLoader classLoader = VMStack.getCallingClassLoader(); 137 if (classLoader == null) { 138 classLoader = getLoader(); 139 } 140 return getBundle(bundleName, Locale.getDefault(), classLoader); 141 } 142 143 /** 144 * Finds the named {@code ResourceBundle} for the specified {@code Locale} and the caller 145 * {@code ClassLoader}. 146 * 147 * @param bundleName 148 * the name of the {@code ResourceBundle}. 149 * @param locale 150 * the {@code Locale}. 151 * @return the requested resource bundle. 152 * @throws MissingResourceException 153 * if the resource bundle cannot be found. 154 */ getBundle(String bundleName, Locale locale)155 public static ResourceBundle getBundle(String bundleName, Locale locale) { 156 ClassLoader classLoader = VMStack.getCallingClassLoader(); 157 if (classLoader == null) { 158 classLoader = getLoader(); 159 } 160 return getBundle(bundleName, locale, classLoader); 161 } 162 163 /** 164 * Finds the named resource bundle for the specified {@code Locale} and {@code ClassLoader}. 165 * 166 * The passed base name and {@code Locale} are used to create resource bundle names. 167 * The first name is created by concatenating the base name with the result 168 * of {@link Locale#toString()}. From this name all parent bundle names are 169 * derived. Then the same thing is done for the default {@code Locale}. This results 170 * in a list of possible bundle names. 171 * 172 * <strong>Example</strong> For the basename "BaseName", the {@code Locale} of the 173 * German part of Switzerland (de_CH) and the default {@code Locale} en_US the list 174 * would look something like this: 175 * 176 * <ol> 177 * <li>BaseName_de_CH</li> 178 * <li>BaseName_de</li> 179 * <li>Basename_en_US</li> 180 * <li>Basename_en</li> 181 * <li>BaseName</li> 182 * </ol> 183 * 184 * This list also shows the order in which the bundles will be searched for a requested 185 * resource in the German part of Switzerland (de_CH). 186 * 187 * As a first step, this method tries to instantiate 188 * a {@code ResourceBundle} with the names provided. 189 * If such a class can be instantiated and initialized, it is returned and 190 * all the parent bundles are instantiated too. If no such class can be 191 * found this method tries to load a {@code .properties} file with the names by 192 * replacing dots in the base name with a slash and by appending 193 * "{@code .properties}" at the end of the string. If such a resource can be found 194 * by calling {@link ClassLoader#getResource(String)} it is used to 195 * initialize a {@link PropertyResourceBundle}. If this succeeds, it will 196 * also load the parents of this {@code ResourceBundle}. 197 * 198 * For compatibility with older code, the bundle name isn't required to be 199 * a fully qualified class name. It's also possible to directly pass 200 * the path to a properties file (without a file extension). 201 * 202 * @param bundleName 203 * the name of the {@code ResourceBundle}. 204 * @param locale 205 * the {@code Locale}. 206 * @param loader 207 * the {@code ClassLoader} to use. 208 * @return the requested {@code ResourceBundle}. 209 * @throws MissingResourceException 210 * if the {@code ResourceBundle} cannot be found. 211 */ getBundle(String bundleName, Locale locale, ClassLoader loader)212 public static ResourceBundle getBundle(String bundleName, Locale locale, 213 ClassLoader loader) throws MissingResourceException { 214 if (loader == null || bundleName == null) { 215 throw new NullPointerException(); 216 } 217 Locale defaultLocale = Locale.getDefault(); 218 if (!cacheLocale.equals(defaultLocale)) { 219 cache.clear(); 220 cacheLocale = defaultLocale; 221 } 222 ResourceBundle bundle = null; 223 if (!locale.equals(defaultLocale)) { 224 bundle = handleGetBundle(false, bundleName, locale, loader); 225 } 226 if (bundle == null) { 227 bundle = handleGetBundle(true, bundleName, defaultLocale, loader); 228 if (bundle == null) { 229 throw missingResourceException(bundleName + '_' + locale, ""); 230 } 231 } 232 return bundle; 233 } 234 missingResourceException(String className, String key)235 private static MissingResourceException missingResourceException(String className, String key) { 236 String detail = "Can't find resource for bundle '" + className + "', key '" + key + "'"; 237 throw new MissingResourceException(detail, className, key); 238 } 239 240 /** 241 * Finds the named resource bundle for the specified base name and control. 242 * 243 * @param baseName 244 * the base name of a resource bundle 245 * @param control 246 * the control that control the access sequence 247 * @return the named resource bundle 248 * 249 * @since 1.6 250 */ getBundle(String baseName, ResourceBundle.Control control)251 public static ResourceBundle getBundle(String baseName, ResourceBundle.Control control) { 252 return getBundle(baseName, Locale.getDefault(), getLoader(), control); 253 } 254 255 /** 256 * Finds the named resource bundle for the specified base name and control. 257 * 258 * @param baseName 259 * the base name of a resource bundle 260 * @param targetLocale 261 * the target locale of the resource bundle 262 * @param control 263 * the control that control the access sequence 264 * @return the named resource bundle 265 * 266 * @since 1.6 267 */ getBundle(String baseName, Locale targetLocale, ResourceBundle.Control control)268 public static ResourceBundle getBundle(String baseName, 269 Locale targetLocale, ResourceBundle.Control control) { 270 return getBundle(baseName, targetLocale, getLoader(), control); 271 } 272 getLoader()273 private static ClassLoader getLoader() { 274 ClassLoader cl = ResourceBundle.class.getClassLoader(); 275 if (cl == null) { 276 cl = ClassLoader.getSystemClassLoader(); 277 } 278 return cl; 279 } 280 281 /** 282 * Finds the named resource bundle for the specified base name and control. 283 * 284 * @param baseName 285 * the base name of a resource bundle 286 * @param targetLocale 287 * the target locale of the resource bundle 288 * @param loader 289 * the class loader to load resource 290 * @param control 291 * the control that control the access sequence 292 * @return the named resource bundle 293 * 294 * @since 1.6 295 */ getBundle(String baseName, Locale targetLocale, ClassLoader loader, ResourceBundle.Control control)296 public static ResourceBundle getBundle(String baseName, 297 Locale targetLocale, ClassLoader loader, 298 ResourceBundle.Control control) { 299 boolean expired = false; 300 String bundleName = control.toBundleName(baseName, targetLocale); 301 Object cacheKey = loader != null ? loader : "null"; 302 Hashtable<String, ResourceBundle> loaderCache = getLoaderCache(cacheKey); 303 ResourceBundle result = loaderCache.get(bundleName); 304 if (result != null) { 305 long time = control.getTimeToLive(baseName, targetLocale); 306 if (time == 0 || time == Control.TTL_NO_EXPIRATION_CONTROL 307 || time + result.lastLoadTime < System.currentTimeMillis()) { 308 if (MISSING == result) { 309 throw new MissingResourceException(null, bundleName + '_' 310 + targetLocale, EMPTY_STRING); 311 } 312 return result; 313 } 314 expired = true; 315 } 316 // try to load 317 ResourceBundle ret = processGetBundle(baseName, targetLocale, loader, 318 control, expired, result); 319 320 if (ret != null) { 321 loaderCache.put(bundleName, ret); 322 ret.lastLoadTime = System.currentTimeMillis(); 323 return ret; 324 } 325 loaderCache.put(bundleName, MISSING); 326 throw new MissingResourceException(null, bundleName + '_' + targetLocale, EMPTY_STRING); 327 } 328 processGetBundle(String baseName, Locale targetLocale, ClassLoader loader, ResourceBundle.Control control, boolean expired, ResourceBundle result)329 private static ResourceBundle processGetBundle(String baseName, 330 Locale targetLocale, ClassLoader loader, 331 ResourceBundle.Control control, boolean expired, 332 ResourceBundle result) { 333 List<Locale> locales = control.getCandidateLocales(baseName, targetLocale); 334 if (locales == null) { 335 throw new IllegalArgumentException(); 336 } 337 List<String> formats = control.getFormats(baseName); 338 if (Control.FORMAT_CLASS == formats 339 || Control.FORMAT_PROPERTIES == formats 340 || Control.FORMAT_DEFAULT == formats) { 341 throw new IllegalArgumentException(); 342 } 343 ResourceBundle ret = null; 344 ResourceBundle currentBundle = null; 345 ResourceBundle bundle = null; 346 for (Locale locale : locales) { 347 for (String format : formats) { 348 try { 349 if (expired) { 350 bundle = control.newBundle(baseName, locale, format, 351 loader, control.needsReload(baseName, locale, 352 format, loader, result, System 353 .currentTimeMillis())); 354 355 } else { 356 try { 357 bundle = control.newBundle(baseName, locale, 358 format, loader, false); 359 } catch (IllegalArgumentException e) { 360 // do nothing 361 } 362 } 363 } catch (IllegalAccessException e) { 364 // do nothing 365 } catch (InstantiationException e) { 366 // do nothing 367 } catch (IOException e) { 368 // do nothing 369 } 370 if (bundle != null) { 371 if (currentBundle != null) { 372 currentBundle.setParent(bundle); 373 currentBundle = bundle; 374 } else { 375 if (ret == null) { 376 ret = bundle; 377 currentBundle = ret; 378 } 379 } 380 } 381 if (bundle != null) { 382 break; 383 } 384 } 385 } 386 387 if ((ret == null) 388 || (Locale.ROOT.equals(ret.getLocale()) && (!(locales.size() == 1 && locales 389 .contains(Locale.ROOT))))) { 390 Locale nextLocale = control.getFallbackLocale(baseName, targetLocale); 391 if (nextLocale != null) { 392 ret = processGetBundle(baseName, nextLocale, loader, control, 393 expired, result); 394 } 395 } 396 397 return ret; 398 } 399 400 /** 401 * Returns the names of the resources contained in this {@code ResourceBundle}. 402 * 403 * @return an {@code Enumeration} of the resource names. 404 */ getKeys()405 public abstract Enumeration<String> getKeys(); 406 407 /** 408 * Gets the {@code Locale} of this {@code ResourceBundle}. In case a bundle was not 409 * found for the requested {@code Locale}, this will return the actual {@code Locale} of 410 * this resource bundle that was found after doing a fallback. 411 * 412 * @return the {@code Locale} of this {@code ResourceBundle}. 413 */ getLocale()414 public Locale getLocale() { 415 return locale; 416 } 417 418 /** 419 * Returns the named resource from this {@code ResourceBundle}. If the resource 420 * cannot be found in this bundle, it falls back to the parent bundle (if 421 * it's not null) by calling the {@link #handleGetObject} method. If the resource still 422 * can't be found it throws a {@code MissingResourceException}. 423 * 424 * @param key 425 * the name of the resource. 426 * @return the resource object. 427 * @throws MissingResourceException 428 * if the resource is not found. 429 */ getObject(String key)430 public final Object getObject(String key) { 431 ResourceBundle last, theParent = this; 432 do { 433 Object result = theParent.handleGetObject(key); 434 if (result != null) { 435 return result; 436 } 437 last = theParent; 438 theParent = theParent.parent; 439 } while (theParent != null); 440 throw missingResourceException(last.getClass().getName(), key); 441 } 442 443 /** 444 * Returns the named string resource from this {@code ResourceBundle}. 445 * 446 * @param key 447 * the name of the resource. 448 * @return the resource string. 449 * @throws MissingResourceException 450 * if the resource is not found. 451 * @throws ClassCastException 452 * if the resource found is not a string. 453 * @see #getObject(String) 454 */ getString(String key)455 public final String getString(String key) { 456 return (String) getObject(key); 457 } 458 459 /** 460 * Returns the named resource from this {@code ResourceBundle}. 461 * 462 * @param key 463 * the name of the resource. 464 * @return the resource string array. 465 * @throws MissingResourceException 466 * if the resource is not found. 467 * @throws ClassCastException 468 * if the resource found is not an array of strings. 469 * @see #getObject(String) 470 */ getStringArray(String key)471 public final String[] getStringArray(String key) { 472 return (String[]) getObject(key); 473 } 474 handleGetBundle(boolean loadBase, String base, Locale locale, ClassLoader loader)475 private static ResourceBundle handleGetBundle(boolean loadBase, String base, Locale locale, 476 ClassLoader loader) { 477 String localeName = locale.toString(); 478 String bundleName = localeName.isEmpty() 479 ? base 480 : (base + "_" + localeName); 481 Object cacheKey = loader != null ? loader : "null"; 482 Hashtable<String, ResourceBundle> loaderCache = getLoaderCache(cacheKey); 483 ResourceBundle cached = loaderCache.get(bundleName); 484 if (cached != null) { 485 if (cached == MISSINGBASE) { 486 return null; 487 } else if (cached == MISSING) { 488 if (!loadBase) { 489 return null; 490 } 491 Locale newLocale = strip(locale); 492 if (newLocale == null) { 493 return null; 494 } 495 return handleGetBundle(loadBase, base, newLocale, loader); 496 } 497 return cached; 498 } 499 500 ResourceBundle bundle = null; 501 try { 502 Class<?> bundleClass = Class.forName(bundleName, true, loader); 503 if (ResourceBundle.class.isAssignableFrom(bundleClass)) { 504 bundle = (ResourceBundle) bundleClass.newInstance(); 505 } 506 } catch (LinkageError ignored) { 507 } catch (Exception ignored) { 508 } 509 510 if (bundle != null) { 511 bundle.setLocale(locale); 512 } else { 513 String fileName = bundleName.replace('.', '/') + ".properties"; 514 InputStream stream = loader != null 515 ? loader.getResourceAsStream(fileName) 516 : ClassLoader.getSystemResourceAsStream(fileName); 517 if (stream != null) { 518 try { 519 bundle = new PropertyResourceBundle(new InputStreamReader(stream, UTF_8)); 520 bundle.setLocale(locale); 521 } catch (IOException ignored) { 522 } finally { 523 IoUtils.closeQuietly(stream); 524 } 525 } 526 } 527 528 Locale strippedLocale = strip(locale); 529 if (bundle != null) { 530 if (strippedLocale != null) { 531 ResourceBundle parent = handleGetBundle(loadBase, base, strippedLocale, loader); 532 if (parent != null) { 533 bundle.setParent(parent); 534 } 535 } 536 loaderCache.put(bundleName, bundle); 537 return bundle; 538 } 539 540 if (strippedLocale != null && (loadBase || !strippedLocale.toString().isEmpty())) { 541 bundle = handleGetBundle(loadBase, base, strippedLocale, loader); 542 if (bundle != null) { 543 loaderCache.put(bundleName, bundle); 544 return bundle; 545 } 546 } 547 loaderCache.put(bundleName, loadBase ? MISSINGBASE : MISSING); 548 return null; 549 } 550 getLoaderCache(Object cacheKey)551 private static Hashtable<String, ResourceBundle> getLoaderCache(Object cacheKey) { 552 synchronized (cache) { 553 Hashtable<String, ResourceBundle> loaderCache = cache.get(cacheKey); 554 if (loaderCache == null) { 555 loaderCache = new Hashtable<String, ResourceBundle>(); 556 cache.put(cacheKey, loaderCache); 557 } 558 return loaderCache; 559 } 560 } 561 562 /** 563 * Returns the named resource from this {@code ResourceBundle}, or null if the 564 * resource is not found. 565 * 566 * @param key 567 * the name of the resource. 568 * @return the resource object. 569 */ handleGetObject(String key)570 protected abstract Object handleGetObject(String key); 571 572 /** 573 * Sets the parent resource bundle of this {@code ResourceBundle}. The parent is 574 * searched for resources which are not found in this {@code ResourceBundle}. 575 * 576 * @param bundle 577 * the parent {@code ResourceBundle}. 578 */ setParent(ResourceBundle bundle)579 protected void setParent(ResourceBundle bundle) { 580 parent = bundle; 581 } 582 583 /** 584 * Returns a locale with the most-specific field removed, or null if this 585 * locale had an empty language, country and variant. 586 */ strip(Locale locale)587 private static Locale strip(Locale locale) { 588 String language = locale.getLanguage(); 589 String country = locale.getCountry(); 590 String variant = locale.getVariant(); 591 if (!variant.isEmpty()) { 592 variant = ""; 593 } else if (!country.isEmpty()) { 594 country = ""; 595 } else if (!language.isEmpty()) { 596 language = ""; 597 } else { 598 return null; 599 } 600 return new Locale(language, country, variant); 601 } 602 setLocale(Locale locale)603 private void setLocale(Locale locale) { 604 this.locale = locale; 605 } 606 clearCache()607 public static void clearCache() { 608 cache.remove(ClassLoader.getSystemClassLoader()); 609 } 610 clearCache(ClassLoader loader)611 public static void clearCache(ClassLoader loader) { 612 if (loader == null) { 613 throw new NullPointerException(); 614 } 615 cache.remove(loader); 616 } 617 containsKey(String key)618 public boolean containsKey(String key) { 619 if (key == null) { 620 throw new NullPointerException(); 621 } 622 return keySet().contains(key); 623 } 624 keySet()625 public Set<String> keySet() { 626 Set<String> ret = new HashSet<String>(); 627 Enumeration<String> keys = getKeys(); 628 while (keys.hasMoreElements()) { 629 ret.add(keys.nextElement()); 630 } 631 return ret; 632 } 633 handleKeySet()634 protected Set<String> handleKeySet() { 635 Set<String> set = keySet(); 636 Set<String> ret = new HashSet<String>(); 637 for (String key : set) { 638 if (handleGetObject(key) != null) { 639 ret.add(key); 640 } 641 } 642 return ret; 643 } 644 645 private static class NoFallbackControl extends Control { 646 647 static final Control NOFALLBACK_FORMAT_PROPERTIES_CONTROL = new NoFallbackControl( 648 JAVAPROPERTIES); 649 650 static final Control NOFALLBACK_FORMAT_CLASS_CONTROL = new NoFallbackControl( 651 JAVACLASS); 652 653 static final Control NOFALLBACK_FORMAT_DEFAULT_CONTROL = new NoFallbackControl( 654 listDefault); 655 NoFallbackControl(String format)656 public NoFallbackControl(String format) { 657 listClass = new ArrayList<String>(); 658 listClass.add(format); 659 super.format = Collections.unmodifiableList(listClass); 660 } 661 NoFallbackControl(List<String> list)662 public NoFallbackControl(List<String> list) { 663 super.format = list; 664 } 665 666 @Override getFallbackLocale(String baseName, Locale locale)667 public Locale getFallbackLocale(String baseName, Locale locale) { 668 if (baseName == null || locale == null) { 669 throw new NullPointerException(); 670 } 671 return null; 672 } 673 } 674 675 private static class SimpleControl extends Control { SimpleControl(String format)676 public SimpleControl(String format) { 677 listClass = new ArrayList<String>(); 678 listClass.add(format); 679 super.format = Collections.unmodifiableList(listClass); 680 } 681 } 682 683 /** 684 * ResourceBundle.Control is a static utility class defines ResourceBundle 685 * load access methods, its default access order is as the same as before. 686 * However users can implement their own control. 687 * 688 * @since 1.6 689 */ 690 public static class Control { 691 static List<String> listDefault = new ArrayList<String>(); 692 693 static List<String> listClass = new ArrayList<String>(); 694 695 static List<String> listProperties = new ArrayList<String>(); 696 697 static String JAVACLASS = "java.class"; 698 699 static String JAVAPROPERTIES = "java.properties"; 700 701 static { 702 listDefault.add(JAVACLASS); 703 listDefault.add(JAVAPROPERTIES); 704 listClass.add(JAVACLASS); 705 listProperties.add(JAVAPROPERTIES); 706 } 707 708 /** 709 * a list defines default format 710 */ 711 public static final List<String> FORMAT_DEFAULT = Collections 712 .unmodifiableList(listDefault); 713 714 /** 715 * a list defines java class format 716 */ 717 public static final List<String> FORMAT_CLASS = Collections 718 .unmodifiableList(listClass); 719 720 /** 721 * a list defines property format 722 */ 723 public static final List<String> FORMAT_PROPERTIES = Collections 724 .unmodifiableList(listProperties); 725 726 /** 727 * a constant that indicates cache will not be used. 728 */ 729 public static final long TTL_DONT_CACHE = -1L; 730 731 /** 732 * a constant that indicates cache will not be expired. 733 */ 734 public static final long TTL_NO_EXPIRATION_CONTROL = -2L; 735 736 private static final Control FORMAT_PROPERTIES_CONTROL = new SimpleControl( 737 JAVAPROPERTIES); 738 739 private static final Control FORMAT_CLASS_CONTROL = new SimpleControl( 740 JAVACLASS); 741 742 private static final Control FORMAT_DEFAULT_CONTROL = new Control(); 743 744 List<String> format; 745 746 /** 747 * default constructor 748 * 749 */ Control()750 protected Control() { 751 listClass = new ArrayList<String>(); 752 listClass.add(JAVACLASS); 753 listClass.add(JAVAPROPERTIES); 754 format = Collections.unmodifiableList(listClass); 755 } 756 757 /** 758 * Returns a control according to {@code formats}. 759 */ getControl(List<String> formats)760 public static Control getControl(List<String> formats) { 761 switch (formats.size()) { 762 case 1: 763 if (formats.contains(JAVACLASS)) { 764 return FORMAT_CLASS_CONTROL; 765 } 766 if (formats.contains(JAVAPROPERTIES)) { 767 return FORMAT_PROPERTIES_CONTROL; 768 } 769 break; 770 case 2: 771 if (formats.equals(FORMAT_DEFAULT)) { 772 return FORMAT_DEFAULT_CONTROL; 773 } 774 break; 775 } 776 throw new IllegalArgumentException(); 777 } 778 779 /** 780 * Returns a control according to {@code formats} whose fallback 781 * locale is null. 782 */ getNoFallbackControl(List<String> formats)783 public static Control getNoFallbackControl(List<String> formats) { 784 switch (formats.size()) { 785 case 1: 786 if (formats.contains(JAVACLASS)) { 787 return NoFallbackControl.NOFALLBACK_FORMAT_CLASS_CONTROL; 788 } 789 if (formats.contains(JAVAPROPERTIES)) { 790 return NoFallbackControl.NOFALLBACK_FORMAT_PROPERTIES_CONTROL; 791 } 792 break; 793 case 2: 794 if (formats.equals(FORMAT_DEFAULT)) { 795 return NoFallbackControl.NOFALLBACK_FORMAT_DEFAULT_CONTROL; 796 } 797 break; 798 } 799 throw new IllegalArgumentException(); 800 } 801 802 /** 803 * Returns a list of candidate locales according to {@code baseName} in 804 * {@code locale}. 805 */ getCandidateLocales(String baseName, Locale locale)806 public List<Locale> getCandidateLocales(String baseName, Locale locale) { 807 if (baseName == null || locale == null) { 808 throw new NullPointerException(); 809 } 810 List<Locale> retList = new ArrayList<Locale>(); 811 String language = locale.getLanguage(); 812 String country = locale.getCountry(); 813 String variant = locale.getVariant(); 814 if (!EMPTY_STRING.equals(variant)) { 815 retList.add(new Locale(language, country, variant)); 816 } 817 if (!EMPTY_STRING.equals(country)) { 818 retList.add(new Locale(language, country)); 819 } 820 if (!EMPTY_STRING.equals(language)) { 821 retList.add(new Locale(language)); 822 } 823 retList.add(Locale.ROOT); 824 return retList; 825 } 826 827 /** 828 * Returns a list of strings of formats according to {@code baseName}. 829 */ getFormats(String baseName)830 public List<String> getFormats(String baseName) { 831 if (baseName == null) { 832 throw new NullPointerException(); 833 } 834 return format; 835 } 836 837 /** 838 * Returns the fallback locale for {@code baseName} in {@code locale}. 839 */ getFallbackLocale(String baseName, Locale locale)840 public Locale getFallbackLocale(String baseName, Locale locale) { 841 if (baseName == null || locale == null) { 842 throw new NullPointerException(); 843 } 844 if (Locale.getDefault() != locale) { 845 return Locale.getDefault(); 846 } 847 return null; 848 } 849 850 /** 851 * Returns a new ResourceBundle. 852 * 853 * @param baseName 854 * the base name to use 855 * @param locale 856 * the given locale 857 * @param format 858 * the format, default is "java.class" or "java.properties" 859 * @param loader 860 * the classloader to use 861 * @param reload 862 * whether to reload the resource 863 * @return a new ResourceBundle according to the give parameters 864 * @throws IllegalAccessException 865 * if we can not access resources 866 * @throws InstantiationException 867 * if we can not instantiate a resource class 868 * @throws IOException 869 * if other I/O exception happens 870 */ newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload)871 public ResourceBundle newBundle(String baseName, Locale locale, 872 String format, ClassLoader loader, boolean reload) 873 throws IllegalAccessException, InstantiationException, 874 IOException { 875 if (format == null || loader == null) { 876 throw new NullPointerException(); 877 } 878 final String bundleName = toBundleName(baseName, locale); 879 final ClassLoader clsloader = loader; 880 ResourceBundle ret; 881 if (format.equals(JAVACLASS)) { 882 Class<?> cls = null; 883 try { 884 cls = clsloader.loadClass(bundleName); 885 } catch (Exception e) { 886 } catch (NoClassDefFoundError e) { 887 } 888 if (cls == null) { 889 return null; 890 } 891 try { 892 ResourceBundle bundle = (ResourceBundle) cls.newInstance(); 893 bundle.setLocale(locale); 894 return bundle; 895 } catch (NullPointerException e) { 896 return null; 897 } 898 } 899 if (format.equals(JAVAPROPERTIES)) { 900 InputStream streams = null; 901 final String resourceName = toResourceName(bundleName, "properties"); 902 if (reload) { 903 URL url = null; 904 try { 905 url = loader.getResource(resourceName); 906 } catch (NullPointerException e) { 907 // do nothing 908 } 909 if (url != null) { 910 URLConnection con = url.openConnection(); 911 con.setUseCaches(false); 912 streams = con.getInputStream(); 913 } 914 } else { 915 try { 916 streams = clsloader.getResourceAsStream(resourceName); 917 } catch (NullPointerException e) { 918 // do nothing 919 } 920 } 921 if (streams != null) { 922 try { 923 ret = new PropertyResourceBundle(new InputStreamReader(streams)); 924 ret.setLocale(locale); 925 streams.close(); 926 } catch (IOException e) { 927 return null; 928 } 929 return ret; 930 } 931 return null; 932 } 933 throw new IllegalArgumentException(); 934 } 935 936 /** 937 * Returns the time to live of the ResourceBundle {@code baseName} in {@code locale}, 938 * default is TTL_NO_EXPIRATION_CONTROL. 939 */ getTimeToLive(String baseName, Locale locale)940 public long getTimeToLive(String baseName, Locale locale) { 941 if (baseName == null || locale == null) { 942 throw new NullPointerException(); 943 } 944 return TTL_NO_EXPIRATION_CONTROL; 945 } 946 947 /** 948 * Returns true if the ResourceBundle needs to reload. 949 * 950 * @param baseName 951 * the base name of the ResourceBundle 952 * @param locale 953 * the locale of the ResourceBundle 954 * @param format 955 * the format to load 956 * @param loader 957 * the ClassLoader to load resource 958 * @param bundle 959 * the ResourceBundle 960 * @param loadTime 961 * the expired time 962 * @return if the ResourceBundle needs to reload 963 */ needsReload(String baseName, Locale locale, String format, ClassLoader loader, ResourceBundle bundle, long loadTime)964 public boolean needsReload(String baseName, Locale locale, 965 String format, ClassLoader loader, ResourceBundle bundle, 966 long loadTime) { 967 if (bundle == null) { 968 // FIXME what's the use of bundle? 969 throw new NullPointerException(); 970 } 971 String bundleName = toBundleName(baseName, locale); 972 String suffix = format; 973 if (format.equals(JAVACLASS)) { 974 suffix = "class"; 975 } 976 if (format.equals(JAVAPROPERTIES)) { 977 suffix = "properties"; 978 } 979 String urlname = toResourceName(bundleName, suffix); 980 URL url = loader.getResource(urlname); 981 if (url != null) { 982 String fileName = url.getFile(); 983 long lastModified = new File(fileName).lastModified(); 984 if (lastModified > loadTime) { 985 return true; 986 } 987 } 988 return false; 989 } 990 991 /** 992 * a utility method to answer the name of a resource bundle according to 993 * the given base name and locale 994 * 995 * @param baseName 996 * the given base name 997 * @param locale 998 * the locale to use 999 * @return the name of a resource bundle according to the given base 1000 * name and locale 1001 */ toBundleName(String baseName, Locale locale)1002 public String toBundleName(String baseName, Locale locale) { 1003 final String emptyString = EMPTY_STRING; 1004 final String preString = UNDER_SCORE; 1005 final String underline = UNDER_SCORE; 1006 if (baseName == null) { 1007 throw new NullPointerException(); 1008 } 1009 StringBuilder ret = new StringBuilder(); 1010 StringBuilder prefix = new StringBuilder(); 1011 ret.append(baseName); 1012 if (!locale.getLanguage().equals(emptyString)) { 1013 ret.append(underline); 1014 ret.append(locale.getLanguage()); 1015 } else { 1016 prefix.append(preString); 1017 } 1018 if (!locale.getCountry().equals(emptyString)) { 1019 ret.append((CharSequence) prefix); 1020 ret.append(underline); 1021 ret.append(locale.getCountry()); 1022 prefix = new StringBuilder(); 1023 } else { 1024 prefix.append(preString); 1025 } 1026 if (!locale.getVariant().equals(emptyString)) { 1027 ret.append((CharSequence) prefix); 1028 ret.append(underline); 1029 ret.append(locale.getVariant()); 1030 } 1031 return ret.toString(); 1032 } 1033 1034 /** 1035 * a utility method to answer the name of a resource according to the 1036 * given bundleName and suffix 1037 * 1038 * @param bundleName 1039 * the given bundle name 1040 * @param suffix 1041 * the suffix 1042 * @return the name of a resource according to the given bundleName and 1043 * suffix 1044 */ toResourceName(String bundleName, String suffix)1045 public final String toResourceName(String bundleName, String suffix) { 1046 if (suffix == null) { 1047 throw new NullPointerException(); 1048 } 1049 StringBuilder ret = new StringBuilder(bundleName.replace('.', '/')); 1050 ret.append('.'); 1051 ret.append(suffix); 1052 return ret.toString(); 1053 } 1054 } 1055 } 1056